Merge branch 'master' into emailNotifications

This commit is contained in:
David Baldwynn 2017-10-26 08:37:03 -07:00 committed by GitHub
commit 8de93d4413
96 changed files with 1783 additions and 2825 deletions

8
.dockerignore Normal file
View file

@ -0,0 +1,8 @@
.git
.idea
.vagrant
coverage
design
e2e_coverage
Vagrantfile
Procfile

View file

@ -1,25 +1,17 @@
# Build:
# docker build -t tellform -f ./Dockerfile .
# docker build -t tellform-prod -f ./Dockerfile-production .
#
# Run:
# docker run -it tellform
# docker run -it tellform-prod
FROM phusion/baseimage:0.9.19
MAINTAINER David Baldwynn <team@tellform.com>
# 3000 = TellForm server, 35729 = livereload, 8080 = node-inspector
EXPOSE 3000 35729 8080
# Set development environment as default
ENV NODE_ENV development
ENV BASE_URL tellform.dev
ENV PORT 3000
# Install Utilities
RUN apt-get update -q \
RUN apt-get update -q \
&& apt-get install -yqq \
curl \
ant \
default-jdk \
git \
gcc \
make \
@ -37,34 +29,31 @@ RUN sudo apt-get install -yq nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Install MEAN.JS Prerequisites
RUN npm install --quiet -g grunt bower && npm cache clean
# Install NPM Global Libraries
RUN npm install --quiet -g grunt bower pm2 && npm cache clean
RUN mkdir -p /opt/tellform/public/lib
WORKDIR /opt/tellform
RUN mkdir -p /opt/tellform/public/lib
# Add bower.json
COPY bower.json /opt/tellform/bower.json
COPY .bowerrc /opt/tellform/.bowerrc
COPY ./process.yml /opt/tellform/process.yml
COPY ./app /opt/tellform/app
COPY ./public /opt/tellform/public
COPY ./config /opt/tellform/config
COPY ./gruntfile.js /opt/tellform/gruntfile.js
COPY ./server.js /opt/tellform/server.js
COPY ./scripts/create_admin.js /opt/tellform/scripts/create_admin.js
# Copies the local package.json file to the container
# and utilities docker container cache to not needing to rebuild
# and install node_modules/ everytime we build the docker, but only
# when the local package.json file changes.
# Add npm package.json
COPY package.json /opt/tellform/package.json
RUN npm install
COPY ./package.json /opt/tellform/package.json
RUN npm install --only=production --quiet
# Add bower.json
COPY bower.json /opt/tellform/bower.json
COPY .bowerrc /opt/tellform/.bowerrc
COPY ./app /opt/tellform/app
COPY ./public /opt/tellform/public
COPY ./config /opt/tellform/config
COPY ./gruntfile.js /opt/tellform/gruntfile.js
COPY ./server.js /opt/tellform/server.js
COPY ./.env /opt/tellform/.env
COPY ./scripts/create_admin.js /opt/tellform/scripts/create_admin.js
# Run Development TellForm server
COPY ./dev_entrypoint.sh /dev_entrypoint.sh
RUN chmod +x /dev_entrypoint.sh
ENTRYPOINT ["/dev_entrypoint.sh"]
# Run TellForm server
CMD ["node", "server.js"]

View file

@ -1,71 +0,0 @@
# Build:
# docker build -t tellform-prod -f ./Dockerfile-production .
#
# Run:
# docker run -it tellform-prod
FROM phusion/baseimage:0.9.19
MAINTAINER David Baldwynn <team@tellform.com>
# 4545 = TellForm server
EXPOSE 4545
# Set development environment as default
ENV NODE_ENV production
ENV PORT 4545
ENV BASE_URL tellform.com
# Install Utilities
RUN apt-get update -q \
&& apt-get install -yqq \
curl \
ant \
default-jdk \
git \
gcc \
make \
build-essential \
libkrb5-dev \
python \
sudo \
apt-utils \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Install nodejs
RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
RUN sudo apt-get install -yq nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Install MEAN.JS Prerequisites
RUN npm install --quiet -g grunt bower pm2 && npm cache clean
RUN mkdir -p /opt/tellform/public/lib
WORKDIR /opt/tellform
# Copies the local package.json file to the container
# and utilities docker container cache to not needing to rebuild
# and install node_modules/ everytime we build the docker, but only
# when the local package.json file changes.
# Add npm package.json
COPY package.json /opt/tellform/package.json
RUN npm install --production
RUN mv ./node_modules ./node_modules.tmp && mv ./node_modules.tmp ./node_modules && npm install
# Add bower.json
COPY bower.json /opt/tellform/bower.json
COPY .bowerrc /opt/tellform/.bowerrc
COPY ./app /opt/tellform/app
COPY ./public /opt/tellform/public
COPY ./config /opt/tellform/config
COPY ./gruntfile.js /opt/tellform/gruntfile.js
COPY ./server.js /opt/tellform/server.js
COPY ./.env /opt/tellform/.env
COPY ./scripts/create_admin.js /opt/tellform/scripts/create_admin.js
RUN grunt build
# Run TellForm server
CMD ["pm2-docker","process.yml"]

View file

@ -1,8 +1,8 @@
TellForm 2.0.0
TellForm 2.1.0
========
[![Build Status](https://travis-ci.org/tellform/tellform.svg?branch=master)](https://travis-ci.org/tellform/tellform)
![Project Status](https://img.shields.io/badge/status-2.0.0-green.svg)
![Project Status](https://img.shields.io/badge/status-2.1.0-green.svg)
[![Code Climate](https://codeclimate.com/github/whitef0x0/tellform/badges/gpa.svg)](https://codeclimate.com/github/whitef0x0/tellform)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/3491e86eb7194308b8fc80711d736ede)](https://www.codacy.com/app/david-baldwin/tellform?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=whitef0x0/tellform&amp;utm_campaign=Badge_Grade)
[![Gitter](https://badges.gitter.im/whitef0x0/tellform.svg)](https://gitter.im/whitef0x0/tellform?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
@ -18,6 +18,7 @@ TellForm 2.0.0
- [Quickstart](#quickstart)
- [Deploying with Docker](#deploying-with-docker)
- [Testing your Application](#testing-your-application)
- [Advanced Configuration](#configuration)
- [Where to Get Help](#where-to-get-help)
- [Sponsors](#sponsors)
- [Backers](#backers)
@ -166,6 +167,39 @@ To calculate your client-side test coverage with Istanbul, run the coverage task
$ grunt coverage:client
```
## Configuration
TellForm's configuration is done with environment variables. To set an option for TellForm, open/create your .env file and set add `ENV_VAR=somevalue` to set the ENV_VAR variable to the value `somevalue`.
| Property | Valid Values | Default Value | Description | Required? |
|-------------------------|--------------------------------------------------------|----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------|
| NODE_ENV | "development", "production", "test" or "secure" | development | Set which version of the app you want to run (either secure/SSL, dev, prod or test) | No |
| SESSION_SECRET | Any string | CHANGE_ME_PLEASE | Value used to compute session hash | No |
| BASE_URL | A valid URL | localhost:3000 | URL where the admin panel will live | Yes |
| MONGODB_URI | A valid MongoDB URI | localhost/mean | URI of the MONGODB server/db that your server will use | Yes |
| REDIS_URL | A valid Redis URI | redis://127.0.0.1:6379 | URI of the Redis instance that your server will use | Only if ENABLE_CLUSTER_MODE=TRUE |
| SOCKET_PORT | A valid port number from 0 - 65535 | 20523 | Port that your SocketIO server will bind to | No |
| SOCKET_URL | A valid URL | ws.tellform.com | Url that your SocketIO server will bind to | No |
| SIGNUP_DISABLED | "TRUE" or "FALSE" | FALSE | Set this flag to disable signups. | No |
| SUBDOMAINS_DISABLED | "TRUE" or "FALSE" | FALSE | Set this flag to disable subdomains. (Useful if hosting behind an uncontrolled domain or without a wildcard SSL cert) | No |
| ENABLE_CLUSTER_MODE | "TRUE" or "FALSE" | FALSE | Disable support for running TellForm with pm2's cluster mode. Disabling this allows you to not run a Redis instance. | No |
| MAILER_EMAIL_ID | A string | N/A | Username credential for the SMTP MAIL service used to send signup/verification/lost password emails. | Yes |
| MAILER_PASSWORD | A string | | Password credential for the SMTP MAIL service used to send signup/verification/lost password emails. | Yes |
| MAILER_FROM | A valid email | noreply@tellform.com | Email address that all mail should be sent from. | No |
| MAILER_SERVICE_PROVIDER | A service from https://nodemailer.com/smtp/well-known/ | | A "well-known" email service that is supported by nodemail. If MAILER_SMTP_HOST is enabled, this is ignored. | Only if MAILER_SMTP_HOST is not set |
| MAILER_SMTP_HOST | A valid URL | | URL to the SMTP server of your choice | Only if MAILER_SERVICE_PROVIDER is not set |
| MAILER_SMTP_PORT | A valid port number from 0 - 65535 | | Port of the SMTP server of your choice. | Only if MAILER_SMTP_HOST is set |
| MAILER_SMTP_SECURE | "TRUE" or "FALSE" | FALSE | Boolean that enables/disables SSL support for your SMTP client. | Only if MAILER_SMTP_HOST is set |
| CREATE_ADMIN | "TRUE" or "FALSE" | FALSE | Setting this variable will create a admin user on startup with credentials as specified below | No |
| ADMIN_EMAIL | A valid email | admin@admin.com | Email of generated admin user | No |
| ADMIN_USERNAME | A string | root | Username of generated admin user | No |
| ADMIN_PASSWORD | A string | root | Password of generated admin user | No |
| APP_NAME | A string | TellForm | Sets the <title> property of your webapp. | No |
| APP_DESC | A string | Opensource form builder alternative to TypeForm | Sets the,property of your webapp. | No |
| APP_KEYWORDS | A comma-seperated list of phrases/words | typeform, pdfs, forms, opensource, formbuilder, google forms, nodejs | Sets the value of the <meta> description attribute. | No |
| RAVEN_DSN | A valid Sentry.io DSN | N/A | Set this to your Sentry.io Public DSN to enable remote logging | No |
| GOOGLE_ANALYTICS_ID | A valid Google Analytics ID | N/A | Set this to your GA id to enable GA tracking on your TellForm instance | No |
## Where to get help
[Gitter Chat](https://gitter.im/whitef0x0/tellform)

View file

@ -15,7 +15,7 @@ exports.index = function(req, res) {
exports.form = function(req, res) {
//Allow form to be embedded
res.set('X-Frame-Options', 'GOFORIT');
res.removeHeader('X-Frame-Options');
res.render('form', {
user: req.user || null,

View file

@ -57,7 +57,6 @@ exports.createSubmission = function(req, res) {
}
var submission = new FormSubmission({
form: req.body._id,
title: req.body.title,
form_fields: req.body.form_fields,
timeElapsed: timeElapsed,
percentageComplete: req.body.percentageComplete,
@ -66,7 +65,6 @@ exports.createSubmission = function(req, res) {
device: req.body.device
});
submission.save(function(err, submission){
if (err) {
console.error(err.message);
@ -120,9 +118,10 @@ exports.listSubmissions = function(req, res) {
* Create a new form
*/
exports.create = function(req, res) {
debugger;
if(!req.body.form){
return res.status(400).send({
return res.status(401).send({
message: 'Invalid Input'
});
}
@ -131,13 +130,14 @@ exports.create = function(req, res) {
form.admin = req.user._id;
form.save(function(err) {
debugger;
if (err) {
return res.status(405).send({
return res.status(500).send({
message: errorHandler.getErrorMessage(err)
});
}
res.json(form);
return res.json(form);
});
};
@ -186,7 +186,7 @@ var readForRender = exports.readForRender = function(req, res) {
delete newForm.__v;
delete newForm.created;
if(!newForm.startPage.showStart){
if(newForm.startPage && !newForm.startPage.showStart){
delete newForm.startPage;
}
@ -197,20 +197,31 @@ var readForRender = exports.readForRender = function(req, res) {
* Update a form
*/
exports.update = function(req, res) {
var form = req.form;
var updatedForm = req.body.form;
if(form.form_fields === undefined){
form.form_fields = [];
}
delete updatedForm.__v;
delete updatedForm.created;
if(form.analytics === undefined){
form.analytics = {
visitors: [],
gaCode: ''
}
}
if (req.body.changes) {
var formChanges = req.body.changes;
formChanges.forEach(function (change) {
diff.applyChange(form, true, change);
diff.applyChange(form._doc, true, change);
});
} else {
//Unless we have 'admin' privileges, updating form admin is disabled
delete updatedForm.__v;
delete updatedForm.created;
//Unless we have 'admin' privileges, updating the form's admin is disabled
if(updatedForm && req.user.roles.indexOf('admin') === -1) {
delete updatedForm.admin;
}
@ -222,7 +233,7 @@ exports.update = function(req, res) {
//Do this so we can create duplicate fields
var checkForValidId = new RegExp('^[0-9a-fA-F]{24}$');
for(var i=0; i<req.body.form.form_fields.length; i++){
for(var i=0; i < req.body.form.form_fields.length; i++){
var field = req.body.form.form_fields[i];
if(!checkForValidId.exec(field._id+'')){
delete field._id;
@ -233,7 +244,7 @@ exports.update = function(req, res) {
form.save(function(err, savedForm) {
if (err) {
res.status(405).send({
res.status(500).send({
message: errorHandler.getErrorMessage(err)
});
} else {
@ -333,8 +344,7 @@ exports.formByIDFast = function(req, res, next, id) {
}
Form.findById(id)
.lean()
.cache()
.select('emailNotifications title language form_fields startPage endPage hideFooter isLive design analytics.gaCode emailNotifications')
.select('title language form_fields startPage endPage hideFooter isLive design analytics.gaCode')
.exec(function(err, form) {
if (err) {
return next(err);

View file

@ -8,7 +8,16 @@ var errorHandler = require('../errors.server.controller'),
passport = require('passport'),
config = require('../../../config/config'),
User = mongoose.model('User'),
tokgen = require('../../libs/tokenGenerator');
tokgen = require('../../libs/tokenGenerator'),
fs = require('fs');
require.extensions['.html'] = function (module, filename) {
module.exports = fs.readFileSync(filename, 'utf8');
};
var welcomeEmail = require("../../views/welcome.email.view.html");
var verificationEmail = require("../../views/verification.email.view.html");
var nev = require('email-verification')(mongoose);
@ -26,16 +35,15 @@ var config_nev = function () {
transportOptions: config.mailer.options,
verifyMailOptions: {
from: config.mailer.from,
subject: 'Confirm your account',
html: '<p>Please verify your account by clicking <a href="http://${URL}">this link</a>. If you are unable to do so, copy and ' +
'paste the following link into your browser:</p><p>${URL}</p>',
subject: '✔ Activate your new TellForm account!',
html: verificationEmail,
text: 'Please verify your account by clicking the following link, or by copying and pasting it into your browser: ${URL}'
},
confirmMailOptions: {
from: config.mailer.from,
subject: 'Account successfully verified!',
html: '<p>Your account has been successfully verified.</p>',
subject: '✔ Welcome to {{app.title}}!',
html: welcomeEmail,
text: 'Your account has been successfully verified.'
},
verifySendMailCallback: function(err, info) {

View file

@ -18,7 +18,7 @@ var smtpTransport = nodemailer.createTransport(config.mailer.options);
/**
* Forgot for reset password (forgot POST)
*/
exports.forgot = function(req, res, next) {
exports.forgot = function(req, res) {
async.waterfall([
// Generate random token
function(done) {
@ -81,22 +81,33 @@ exports.forgot = function(req, res, next) {
subject: 'Password Reset',
html: emailHTML
};
smtpTransport.sendMail(mailOptions, function(err) {
if (!err) {
res.send({
message: 'An email has been sent to ' + user.email + ' with further instructions.'
});
} else {
return res.status(400).send({
message: 'Failure sending email'
});
}
done(err);
var userEmail = user.email;
var user = userEmail.split('@')[0];
var domain = userEmail.split('@')[1];
var obfuscatedUser = user.substring(0, 1) + user.substring(1).replace(/./g, '*');
var domainName = domain.split('.')[0];
var tld = domain.split('.')[1];
var obfuscatedDomainName = domainName.replace(/./g, '*');
var obfuscatedEmail = obfuscatedUser + '@' + obfuscatedDomainName + '.' + tld;
smtpTransport.sendMail(mailOptions, function(err) {
done(err, obfuscatedEmail);
});
}
], function(err, obfuscatedEmail) {
if (err) {
console.log(err);
return res.status(400).send({
message: 'Couldn\'t send reset password email due to internal server errors. Please contact support at team@tellform.com.'
});
} else {
return res.send({
message: 'An email has been sent to ' + obfuscatedEmail + ' with further instructions.'
});
}
], function(err) {
if (err) return next(err);
});
};

View file

@ -0,0 +1,39 @@
'use strict';
// Plugin
module.exports = function timestamp (schema, options) {
options || (options = {})
// Options
var fields = {}
, createdPath = options.createdPath || 'created'
, modifiedPath = options.modifiedPath || 'modified'
, useVirtual = (options.useVirtual !== undefined)
? options.useVirtual
: true
// Add paths to schema if not present
if (!schema.paths[createdPath]) {
fields[modifiedPath] = { type: Date }
}
if (useVirtual) {
// Use the ObjectID for extracting the created time
schema.virtual(createdPath).get(function () {
return new Date(this._id.generationTime * 1000)
})
} else {
if (!schema.paths[createdPath]) {
fields[createdPath] = {
type: Date
, default: Date.now
}
}
}
schema.add(fields)
// Update the modified timestamp on save
schema.pre('save', function (next) {
this[modifiedPath] = new Date
next()
})
}

View file

@ -6,7 +6,7 @@
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
_ = require('lodash'),
mUtilities = require('mongoose-utilities'),
timeStampPlugin = require('../libs/timestamp.server.plugin'),
async = require('async'),
Random = require('random-js'),
mt = Random.engines.mt19937();
@ -292,7 +292,7 @@ FormSchema.virtual('analytics.fields').get(function () {
return fieldDropoffs;
});
FormSchema.plugin(mUtilities.timestamp, {
FormSchema.plugin(timeStampPlugin, {
createdPath: 'created',
modifiedPath: 'lastModified',
useVirtual: false
@ -313,7 +313,6 @@ FormSchema.pre('save', function (next) {
this.language = 'de';
break;
default:
this.language = 'en';
break;
}
next();
@ -345,54 +344,55 @@ FormSchema.pre('save', function (next) {
var that = this;
var _original;
async.series([function(cb) {
that.constructor
.findOne({_id: that._id}).exec(function (err, original) {
if (err) {
return cb(err);
} else if (!original){
return next();
} else {
_original = original;
return cb(null);
}
});
},
function(cb) {
if(that.form_fields && that.isModified('form_fields') && formFieldsAllHaveIds(that.toObject().form_fields)){
async.series([
function(cb) {
that.constructor
.findOne({_id: that._id}).exec(function (err, original) {
if (err) {
return cb(err);
} else if (!original){
return next();
} else {
_original = original;
return cb(null);
}
});
},
function(cb) {
if(that.form_fields && that.isModified('form_fields') && formFieldsAllHaveIds(that.toObject().form_fields)){
var current_form = that.toObject(),
old_form_fields = _original.toObject().form_fields,
new_ids = _.map(_.map(current_form.form_fields, 'globalId'), function(id){ return ''+id;}),
old_ids = _.map(_.map(old_form_fields, 'globalId'), function(id){ return ''+id;}),
deletedIds = getDeletedIndexes(old_ids, new_ids);
var current_form = that.toObject(),
old_form_fields = _original.toObject().form_fields,
new_ids = _.map(_.map(current_form.form_fields, 'globalId'), function(id){ return ''+id;}),
old_ids = _.map(_.map(old_form_fields, 'globalId'), function(id){ return ''+id;}),
deletedIds = getDeletedIndexes(old_ids, new_ids);
//Check if any form_fileds were deleted
if( deletedIds.length > 0 ){
//Check if any form_fileds were deleted
if( deletedIds.length > 0 ){
var modifiedSubmissions = [];
var modifiedSubmissions = [];
async.forEachOfSeries(deletedIds,
function (deletedIdIndex, key, cb_id) {
async.forEachOfSeries(deletedIds,
function (deletedIdIndex, key, cb_id) {
var deleted_id = old_ids[deletedIdIndex];
//Find FormSubmissions that contain field with _id equal to 'deleted_id'
FormSubmission.
find({ form: that, form_fields: {$elemMatch: {globalId: deleted_id} } }).
exec(function(err, submissions){
if(err) {
return cb_id(err);
}
var deleted_id = old_ids[deletedIdIndex];
//Find FormSubmissions that contain field with _id equal to 'deleted_id'
FormSubmission.
find({ form: that, form_fields: {$elemMatch: {globalId: deleted_id} } }).
exec(function(err, submissions){
if(err) {
return cb_id(err);
}
//Preserve fields that have at least one submission
if (submissions.length) {
//Add submissions
modifiedSubmissions.push.apply(modifiedSubmissions, submissions);
}
//Preserve fields that have at least one submission
if (submissions.length) {
//Add submissions
modifiedSubmissions.push.apply(modifiedSubmissions, submissions);
}
return cb_id(null);
});
},
return cb_id(null);
});
},
function (err) {
if(err){
console.error(err.message);
@ -434,17 +434,20 @@ FormSchema.pre('save', function (next) {
}, function (err) {
return cb(err);
});
}
);
});
} else {
return cb(null);
}
} else {
return cb(null);
}
} else {
return cb(null);
}
}],
function(err, results){
next(err);
],
function(err){
if(err){
return next(err);
}
next();
});
});

View file

@ -5,13 +5,11 @@
*/
var mongoose = require('mongoose'),
util = require('util'),
mUtilities = require('mongoose-utilities'),
timeStampPlugin = require('../libs/timestamp.server.plugin'),
_ = require('lodash'),
Schema = mongoose.Schema,
LogicJumpSchema = require('./logic_jump.server.model');
const UIDGenerator = require('uid-generator');
const uidgen3 = new UIDGenerator(256, UIDGenerator.BASE62);
LogicJumpSchema = require('./logic_jump.server.model'),
tokgen = require('../libs/tokenGenerator');
var FieldOptionSchema = new Schema({
option_id: {
@ -76,8 +74,7 @@ function BaseFieldSchema(){
},
title: {
type: String,
trim: true,
required: 'Field Title cannot be blank'
trim: true
},
description: {
type: String,
@ -106,7 +103,6 @@ function BaseFieldSchema(){
},
fieldType: {
type: String,
required: true,
enum: [
'textfield',
'date',
@ -134,7 +130,7 @@ function BaseFieldSchema(){
fieldValue: Schema.Types.Mixed
});
this.plugin(mUtilities.timestamp, {
this.plugin(timeStampPlugin, {
createdPath: 'created',
modifiedPath: 'lastModified',
useVirtual: false
@ -196,11 +192,8 @@ FormFieldSchema.pre('validate', function(next) {
//LogicJump Save
FormFieldSchema.pre('save', function(next) {
if(this.logicJump && this.logicJump.fieldA) {
if(this.logicJump.jumpTo === '') delete this.logicJump.jumpTo;
}
if(!this.globalId){
this.globalId = uidgen3.generateSync();
this.globalId = tokgen();
}
next();
});

View file

@ -5,17 +5,13 @@
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
mUtilities = require('mongoose-utilities'),
timeStampPlugin = require('../libs/timestamp.server.plugin'),
FieldSchema = require('./form_field.server.model.js');
/**
* Form Submission Schema
*/
var FormSubmissionSchema = new Schema({
title: {
type: String
},
form_fields: [FieldSchema],
form: {
@ -58,6 +54,19 @@ FormSubmissionSchema.pre('save', function (next) {
if(this.form_fields[i].fieldType === 'dropdown'){
this.form_fields[i].fieldValue = this.form_fields[i].fieldValue.option_value;
}
delete form_fields[i].validFieldTypes;
delete form_fields[i].disabled;
delete form_fields[i].required;
delete form_fields[i].isSubmission;
delete form_fields[i].title;
delete form_fields[i].fieldOptions;
delete form_fields[i].ratingOptions;
delete form_fields[i].logicJump;
delete form_fields[i].description;
delete form_fields[i].created;
delete form_fields[i].lastModified;
delete form_fields[i].deletePreserved;
}
next();
});
@ -68,6 +77,17 @@ FormSubmissionSchema.path('form_fields', {
form_fields[i].isSubmission = true;
form_fields[i]._id = new mongoose.mongo.ObjectID();
delete form_fields[i].validFieldTypes;
delete form_fields[i].disabled;
delete form_fields[i].required;
delete form_fields[i].isSubmission;
delete form_fields[i].title;
delete form_fields[i].fieldOptions;
delete form_fields[i].ratingOptions;
delete form_fields[i].logicJump;
delete form_fields[i].description;
delete form_fields[i].created;
delete form_fields[i].lastModified;
delete form_fields[i].deletePreserved;
}
@ -75,7 +95,7 @@ FormSubmissionSchema.path('form_fields', {
}
});
FormSubmissionSchema.plugin(mUtilities.timestamp, {
FormSubmissionSchema.plugin(timeStampPlugin, {
createdPath: 'created',
modifiedPath: 'lastModified',
useVirtual: false

View file

@ -44,6 +44,10 @@ var LogicJumpSchema = new Schema({
jumpTo: {
type: Schema.Types.ObjectId,
ref: 'FormField'
},
enabled: {
type: Schema.Types.Boolean,
default: false
}
}, schemaOptions);

View file

@ -7,8 +7,7 @@ var mongoose = require('mongoose'),
Schema = mongoose.Schema,
crypto = require('crypto'),
config = require('../../config/config'),
fs = require('fs-extra'),
mUtilities = require('mongoose-utilities'),
timeStampPlugin = require('../libs/timestamp.server.plugin'),
path = require('path'),
querystring = require('querystring'),
nodemailer = require('nodemailer');
@ -143,7 +142,7 @@ UserSchema.virtual('displayName').get(function () {
return this.firstName + ' ' + this.lastName;
});
UserSchema.plugin(mUtilities.timestamp, {
UserSchema.plugin(timeStampPlugin, {
createdPath: 'created',
modifiedPath: 'lastModified',
useVirtual: false

View file

@ -34,34 +34,7 @@ module.exports = function(app) {
app.route('/auth/signout').get(users.signout);
app.route('/auth/genkey').get(users.requiresLogin, users.generateAPIKey);
// // Setting the facebook oauth routes
// app.route('/auth/facebook').get(passport.authenticate('facebook', {
// scope: ['email']
// }));
// app.route('/auth/facebook/callback').get(users.oauthCallback('facebook'));
// // Setting the twitter oauth routes
// app.route('/auth/twitter').get(passport.authenticate('twitter'));
// app.route('/auth/twitter/callback').get(users.oauthCallback('twitter'));
// // Setting the google oauth routes
// app.route('/auth/google').get(passport.authenticate('google', {
// scope: [
// 'https://www.googleapis.com/auth/userinfo.profile',
// 'https://www.googleapis.com/auth/userinfo.email'
// ]
// }));
// app.route('/auth/google/callback').get(users.oauthCallback('google'));
// // Setting the linkedin oauth routes
// app.route('/auth/linkedin').get(passport.authenticate('linkedin'));
// app.route('/auth/linkedin/callback').get(users.oauthCallback('linkedin'));
// // Setting the github oauth routes
// app.route('/auth/github').get(passport.authenticate('github'));
// app.route('/auth/github/callback').get(users.oauthCallback('github'));
// Finish by binding the user middleware
app.param('userId', users.userByID);
};

View file

@ -5,13 +5,14 @@
*/
var mongoose = require('mongoose'),
errorHandler = require('../controllers/errors.server.controller'),
Form = mongoose.model('Form');
Form = mongoose.model('Form'),
request = require('request');
// Create the chat configuration
module.exports = function (io, socket) {
var visitorsData = {};
var saveVisitorData = function (data, cb){
var saveVisitorData = function (data, socket, cb){
Form.findById(data.formId, function(err, form) {
if (err) {
console.error(err);
@ -25,22 +26,25 @@ module.exports = function (io, socket) {
timeElapsed: data.timeElapsed,
isSubmitted: data.isSubmitted,
language: data.language,
ipAddr: data.ipAddr,
ipAddr: '',
deviceType: data.deviceType
};
form.analytics.visitors.push(newVisitor);
form.save(function (formSaveErr) {
if (err) {
console.error(err);
throw new Error(errorHandler.getErrorMessage(formSaveErr));
}
if(cb){
return cb();
}
});
form.form_fields = form.form_fields.map(v => Object.assign({}, v, { fieldValue: null }));
form.save(function (formSaveErr) {
if (err) {
console.error(err);
throw new Error(errorHandler.getErrorMessage(formSaveErr));
}
if(cb){
return cb();
}
});
});
};
@ -50,6 +54,8 @@ module.exports = function (io, socket) {
visitorsData[current_socket.id] = data;
visitorsData[current_socket.id].socketId = current_socket.id;
visitorsData[current_socket.id].isSaved = false;
if (data.isSubmitted && !data.isSaved) {
visitorsData[current_socket.id].isSaved = true;
saveVisitorData(data, function() {
@ -71,3 +77,4 @@ module.exports = function (io, socket) {
});
});
};

View file

@ -1,5 +1,7 @@
'use strict';
require('../../server.js');
/**
* Module dependencies.
*/

View file

@ -1,5 +1,4 @@
'use strict';
process.env.NODE_ENV = 'test';
var should = require('should'),
lodash = require('lodash'),
@ -19,8 +18,8 @@ var user, myForm, userSession;
// Create user credentials
var credentials = {
username: 'test1234',
email: 'test1234@test.com',
username: 'aeokjqjqkqaeoaoe',
email: 'aeoaekjqjqqjkoeoa@test.com',
password: 'password'
};
@ -30,18 +29,17 @@ var credentials = {
describe('Form Routes Unit tests', function() {
beforeEach(function(done) {
// Create a new user
user = new User({
firstName: 'Full',
lastName: 'Name',
displayName: 'Full Name',
email: credentials.email,
username: credentials.username,
password: credentials.password,
provider: 'local'
});
// Save a user to the test db and create new Form
user.save(function(err) {
should.not.exist(err);
@ -65,10 +63,12 @@ describe('Form Routes Unit tests', function() {
});
it(' > should not be able to create a Form if not logged in', function(done) {
userSession.post('/forms')
.send({form: myForm})
.expect(401)
.end(function(FormSaveErr, FormSaveRes) {
// Call the assertion callback
done(FormSaveErr);
});
@ -153,7 +153,7 @@ describe('Form Routes Unit tests', function() {
// Save a new Form
authenticatedSession.post('/forms')
.send({form: myForm})
.expect(405)
.expect(500)
.end(function(FormSaveErr, FormSaveRes) {
// Handle Form save error
if (FormSaveErr) {

View file

@ -168,7 +168,7 @@ describe('FormSubmission Model Unit Tests:', function() {
});
it('should be able to find FormSubmission by $elemMatch on form_fields id', function(done){
FormSubmission.findOne({ form: myForm._id, admin: user, form_fields: {$elemMatch: {_id: myForm.form_fields[0]._id} } })
FormSubmission.findOne({ form: myForm._id, form_fields: {$elemMatch: {globalId: myForm.form_fields[0].globalId} } })
.exec(function(err, submission){
should.not.exist(err);
should.exist(submission);

View file

@ -21,7 +21,8 @@ var credentials, user;
* Form routes tests
*/
describe('Form Submission Routes Unit tests', function() {
var FormObj, _Submission, submissionSession;
var FormObj, _Submission, submissionSession, _SubmissionBody
beforeEach(function(done) {
@ -60,15 +61,43 @@ describe('Form Submission Routes Unit tests', function() {
if (formSaveErr) done(formSaveErr);
_Submission = {
form_fields: [
{'fieldType':'textfield', 'title':'First Name', 'fieldValue': 'David'},
{'fieldType':'checkbox', 'title':'nascar', 'fieldValue': true},
{'fieldType':'checkbox', 'title':'hockey', 'fieldValue': false}
],
form: form._id,
admin: user._id,
form_fields: [
{'fieldType':'textfield', 'title':'First Name', 'fieldValue': 'David', _id: '', isSubmission: false, deletePreserved: false},
{'fieldType':'checkbox', 'title':'nascar', 'fieldValue': true, _id: '', isSubmission: false, deletePreserved: true},
{'fieldType':'checkbox', 'title':'hockey', 'fieldValue': false, _id: '', isSubmission: false, deletePreserved: false}
],
percentageComplete: 100,
timeElapsed: 11.55
timeElapsed: 11.55,
ipAddr: '123.233.232.232',
geoLocation: {
Country: 'Canada',
City: 'Vancouver'
},
device:{
type: 'Mobile',
name: 'iPhone'
}
};
_SubmissionBody ={
_id: form._id,
form_fields: [
{'fieldType':'textfield', 'title':'First Name', 'fieldValue': 'David', _id: '', isSubmission: false, deletePreserved: false},
{'fieldType':'checkbox', 'title':'nascar', 'fieldValue': true, _id: '', isSubmission: false, deletePreserved: true},
{'fieldType':'checkbox', 'title':'hockey', 'fieldValue': false, _id: '', isSubmission: false, deletePreserved: false}
],
percentageComplete: 100,
timeElapsed: 11.55,
ipAddr: '123.233.232.232',
geoLocation: {
Country: 'Canada',
City: 'Vancouver'
},
device:{
type: 'Mobile',
name: 'iPhone'
}
};
FormObj = form;
@ -86,12 +115,11 @@ describe('Form Submission Routes Unit tests', function() {
//Create Submission
submissionSession.post('/forms/' + FormObj._id)
.send(_Submission)
.send(_SubmissionBody)
.expect(200)
.end(function(err, res) {
should.not.exist(err);
done();
});
});
@ -99,7 +127,7 @@ describe('Form Submission Routes Unit tests', function() {
it(' > should be able to get Form Submissions if signed in', function(done) {
//Create Submission
submissionSession.post('/forms/' + FormObj._id)
.send(_Submission)
.send(_SubmissionBody)
.expect(200)
.end(function(err, res) {

View file

@ -0,0 +1,70 @@
// Dependencies
var util = require('util')
, assert = require('assert')
, mongoose = require('mongoose')
, timestamp = require('../../libs/timestamp.server.plugin')
, Schema = mongoose.Schema
, ObjectId = Schema.ObjectId
// Run tests
describe('Timestamp', function () {
describe('#default()', function () {
var FooSchema = new Schema()
FooSchema.plugin(timestamp)
var FooModel = mongoose.model('timeFoo', FooSchema)
, bar = new FooModel()
before(function () {
FooModel.remove(function (err) {
assert.strictEqual(err, null)
})
})
it('should have custom properties', function (done) {
assert.strictEqual(typeof FooSchema.virtuals.created, 'object')
assert.strictEqual(typeof FooSchema.paths.modified, 'object')
done()
})
it('should create the default attributes', function (done) {
bar.save(function (err, doc) {
assert.strictEqual(err, null)
assert.strictEqual(util.isDate(doc.created), true)
assert.strictEqual(util.isDate(doc.modified), true)
done()
})
})
})
describe('#custom()', function () {
var FooSchema = new Schema()
FooSchema.plugin(timestamp, {
createdPath: 'oh'
, modifiedPath: 'hai'
, useVirtual: false
})
var BarModel = mongoose.model('timeBar', FooSchema)
, bar = new BarModel()
before(function () {
BarModel.remove(function (err) {
assert.strictEqual(err, null)
})
})
it('should have custom properties', function (done) {
assert.strictEqual(typeof FooSchema.paths.oh, 'object')
assert.strictEqual(typeof FooSchema.paths.hai, 'object')
done()
})
it('should create custom attributes', function (done) {
bar.save(function (err, doc) {
assert.strictEqual(err, null)
assert.strictEqual(util.isDate(doc.oh), true)
assert.strictEqual(util.isDate(doc.hai), true)
done()
})
})
})
})

View file

@ -76,11 +76,6 @@
<script src="/static/lib/jquery/dist/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(window).on("load", function() {
$(".loader").fadeOut("slow");
});
</script>
<link rel="stylesheet" href="/static/lib/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="/static/lib/bootstrap/dist/css/bootstrap.min.css">

View file

@ -1,13 +1,64 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
</head>
<body>
<p>Dear {{name}},</p>
<p></p>
<p>This is a confirmation that the password for your account has just been changed</p>
<br>
<br>
<p>The {{appName}} Support Team</p>
</body>
</html>
<html>
<head>@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700&subset=latin,latin-ext);
<style>
a {
color: #007ee6;
text-decoration: none;
}
</style>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body style="padding: 0; width: 100% !important; -webkit-text-size-adjust: 100%; margin: 0; -ms-text-size-adjust: 100%;" marginheight="0" marginwidth="0">
<center>
<table cellpadding="8" cellspacing="0" style="*width: 540px; padding: 0; width: 100% !important; background: #ffffff; margin: 0; background-color: #ffffff;" border="0">
<tr>
<td valign="top">
<table cellpadding="0" cellspacing="0" style="border-radius: 6px; -webkit-border-radius: 6px; border: 1px #c0c0c0 solid; -moz-border-radius: 6px;" border="0" align="center">
<tr>
<td colspan="3" height="6"></td>
</tr>
<tr>
<td>
<table cellpadding="0" cellspacing="0" style="line-height: 25px;" border="0" align="center">
<tr>
<td colspan="3" height="30"></td>
</tr>
<tr>
<td width="36"></td>
<td width="454" align="left" style="color: #444444; border-collapse: collapse; font-size: 11pt; font-family: 'Open Sans', 'Lucida Grande', 'Segoe UI', Arial, Verdana, 'Lucida Sans Unicode', Tahoma, 'Sans Serif'; max-width: 454px;" valign="top">
<p>Hello there!</p>
<p>This is a courtesy message to confirm that your password was just changed.</p>
<p>Thanks so much for using our services! If you have any questions, or suggestions, please feel free to email us here at&nbsp;<a href="mailto:team@tellform.com">team@tellform.com</a>.</p>
<p> - The {{appName}} team</p>
</td>
<td width="36"></td>
</tr>
<tr>
<td colspan="3" height="36"></td>
</tr>
</table>
</td>
</tr>
</table>
<table cellpadding="0" cellspacing="0" align="center" border="0">
<tr>
<td height="10"></td>
</tr>
<tr>
<td style="padding: 0; border-collapse: collapse;">
<table cellpadding="0" cellspacing="0" align="center" border="0">
<tr style="color: #c0c0c0; font-size: 11px; font-family: 'Open Sans', 'Lucida Grande', 'Segoe UI', Arial, Verdana, 'Lucida Sans Unicode', Tahoma, 'Sans Serif'; -webkit-text-size-adjust: none;">
<td width="400" align="left"></td>
<td width="128" align="right">© TellForm 2017</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</center>
</body>
</html>

View file

@ -1,18 +1,66 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
</head>
<body>
<p>Dear {{name}},</p>
<br>
<p>
You have requested to have your password reset for your account at {{appName}}
</p>
<p>Please visit this url to reset your password:</p>
<p>{{url}}</p>
<strong>If you didn't make this request, you can ignore this email.</strong>
<br>
<br>
<p>The {{appName}} Support Team</p>
</body>
</html>
<html>
<head>@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700&subset=latin,latin-ext);
<style>
a {
color: #007ee6;
text-decoration: none;
}
</style>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body style="padding: 0; width: 100% !important; -webkit-text-size-adjust: 100%; margin: 0; -ms-text-size-adjust: 100%;" marginheight="0" marginwidth="0">
<center>
<table cellpadding="8" cellspacing="0" style="*width: 540px; padding: 0; width: 100% !important; background: #ffffff; margin: 0; background-color: #ffffff;" border="0">
<tr>
<td valign="top">
<table cellpadding="0" cellspacing="0" style="border-radius: 6px; -webkit-border-radius: 6px; border: 1px #c0c0c0 solid; -moz-border-radius: 6px;" border="0" align="center">
<tr>
<td colspan="3" height="6"></td>
</tr>
<tr>
<td>
<table cellpadding="0" cellspacing="0" style="line-height: 25px;" border="0" align="center">
<tr>
<td colspan="3" height="30"></td>
</tr>
<tr>
<td width="36"></td>
<td width="454" align="left" style="color: #444444; border-collapse: collapse; font-size: 11pt; font-family: 'Open Sans', 'Lucida Grande', 'Segoe UI', Arial, Verdana, 'Lucida Sans Unicode', Tahoma, 'Sans Serif'; max-width: 454px;" valign="top">
<p>Hello there!</p>
<p>Here is a special link that will allow you to reset your password. Please note it will expire in one hour for your protection:</p>
<p><a href="{{url}}">Reset Your Password</a></p>
<p>If you did not request this, please ignore this email and your password will remain unchanged.</p>
<p>Thanks so much for using our services! If you have any questions, or suggestions, please feel free to email us here at&nbsp;<a href="mailto:team@tellform.com">team@tellform.com</a>.</p>
<p> - The {{appName}} team</p>
</td>
<td width="36"></td>
</tr>
<tr>
<td colspan="3" height="36"></td>
</tr>
</table>
</td>
</tr>
</table>
<table cellpadding="0" cellspacing="0" align="center" border="0">
<tr>
<td height="10"></td>
</tr>
<tr>
<td style="padding: 0; border-collapse: collapse;">
<table cellpadding="0" cellspacing="0" align="center" border="0">
<tr style="color: #c0c0c0; font-size: 11px; font-family: 'Open Sans', 'Lucida Grande', 'Segoe UI', Arial, Verdana, 'Lucida Sans Unicode', Tahoma, 'Sans Serif'; -webkit-text-size-adjust: none;">
<td width="400" align="left"></td>
<td width="128" align="right">© TellForm 2017</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</center>
</body>
</html>

View file

@ -1,71 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
</head>
<body style="padding: 0; width: 100% !important; -webkit-text-size-adjust: 100%; margin: 0; -ms-text-size-adjust: 100%;" marginheight="0" marginwidth="0">
<center>
<table cellpadding="8" cellspacing="0" style="*width: 540px; padding: 0; width: 100% !important; background: #ffffff; margin: 0; background-color: #ffffff;" border="0">
<tr>
<td valign="top">
<table cellpadding="0" cellspacing="0" style="border-radius: 6px; -webkit-border-radius: 6px; border: 1px #c0c0c0 solid; -moz-border-radius: 6px;" border="0" align="center">
<tr>
<td colspan="3" height="6"></td>
</tr>
<tr>
<td>
<table cellpadding="0" cellspacing="0" style="line-height: 25px;" border="0" align="center">
<tr>
<td colspan="3" height="30"></td>
</tr>
<tr>
<td width="36"></td>
<td width="454" align="left" style="color: #444444; border-collapse: collapse; font-size: 11pt; font-family: 'Open Sans', 'Lucida Grande', 'Segoe UI', Arial, Verdana, 'Lucida Sans Unicode', Tahoma, 'Sans Serif'; max-width: 454px;" valign="top">
//- -----------------------------------------------------------------------------------
//- Only Edit Here
//- -----------------------------------------------------------------------------------
p Hello {{name}}!
p
| Welcome to {{appName}}! Here is a special link to activate your new account:
p
a(href='${URL}') Activate my account
p
| Thanks so much for using our services! If you have any questions, or suggestions, please feel free to email us here at&nbsp;
a(href='mailto:{{contactEmail}}') {{contactEmail}}
| .
p   - The {{appName}} team
//- ----------------------------------------------------------------------------------
</td>
<td width="36"></td>
</tr>
<tr>
<td colspan="3" height="36"></td>
</tr>
</table>
</td>
</tr>
</table>
<table cellpadding="0" cellspacing="0" align="center" border="0">
<tr>
<td height="10"></td>
</tr>
<tr>
<td style="padding: 0; border-collapse: collapse;">
<table cellpadding="0" cellspacing="0" align="center" border="0">
<tr style="color: #c0c0c0; font-size: 11px; font-family: 'Open Sans', 'Lucida Grande', 'Segoe UI', Arial, Verdana, 'Lucida Sans Unicode', Tahoma, 'Sans Serif'; -webkit-text-size-adjust: none;"></tr>
<td width="400" align="left">
<td width="128" align="right">© </td>
</td>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</center>
</body>
</html>

View file

@ -0,0 +1,65 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<head>
@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700&subset=latin,latin-ext);
<style>
a {
color: #007ee6;
text-decoration: none;
}
</style>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
</head>
<body style="padding: 0; width: 100% !important; -webkit-text-size-adjust: 100%; margin: 0; -ms-text-size-adjust: 100%;" marginheight="0" marginwidth="0">
<center>
<table cellpadding="8" cellspacing="0" style="*width: 540px; padding: 0; width: 100% !important; background: #ffffff; margin: 0; background-color: #ffffff;" border="0">
<tr>
<td valign="top">
<table cellpadding="0" cellspacing="0" style="border-radius: 6px; -webkit-border-radius: 6px; border: 1px #c0c0c0 solid; -moz-border-radius: 6px;" border="0" align="center">
<tr>
<td colspan="3" height="6"></td>
</tr>
<tr>
<td>
<table cellpadding="0" cellspacing="0" style="line-height: 25px;" border="0" align="center">
<tr>
<td colspan="3" height="30"></td>
</tr>
<tr>
<td width="36"></td>
<td width="454" align="left" style="color: #444444; border-collapse: collapse; font-size: 11pt; font-family: 'Open Sans', 'Lucida Grande', 'Segoe UI', Arial, Verdana, 'Lucida Sans Unicode', Tahoma, 'Sans Serif'; max-width: 454px;" valign="top">
<p>Hello there!</p>
<p>Welcome to TellForm! Here is a special link to activate your new account:</p>
<p><a href="https://${URL}">Activate my account</a></p>
<p>Thanks so much for using our services! If you have any questions, or suggestions, please feel free to email us here at&nbsp;<a href="mailto:team@tellform.com">team@tellform.com</a>.</p>
<p> - The TellForm team</p>
</td>
<td width="36"></td>
</tr>
<tr>
<td colspan="3" height="36"></td>
</tr>
</table>
</td>
</tr>
</table>
<table cellpadding="0" cellspacing="0" align="center" border="0">
<tr>
<td height="10"></td>
</tr>
<tr>
<td style="padding: 0; border-collapse: collapse;">
<table cellpadding="0" cellspacing="0" align="center" border="0">
<tr style="color: #c0c0c0; font-size: 11px; font-family: 'Open Sans', 'Lucida Grande', 'Segoe UI', Arial, Verdana, 'Lucida Sans Unicode', Tahoma, 'Sans Serif'; -webkit-text-size-adjust: none;">
<td width="400" align="left"></td>
<td width="128" align="right">© TellForm 2017</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</center>
</body>
</head>

View file

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700&subset=latin,latin-ext);
<style>
a {
color: #007ee6;
text-decoration: none;
}
</style>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body style="padding: 0; width: 100% !important; -webkit-text-size-adjust: 100%; margin: 0; -ms-text-size-adjust: 100%;" marginheight="0" marginwidth="0">
<center>
<table cellpadding="8" cellspacing="0" style="*width: 540px; padding: 0; width: 100% !important; background: #ffffff; margin: 0; background-color: #ffffff;" border="0">
<tr>
<td valign="top">
<table cellpadding="0" cellspacing="0" style="border-radius: 6px; -webkit-border-radius: 6px; border: 1px #c0c0c0 solid; -moz-border-radius: 6px;" border="0" align="center">
<tr>
<td colspan="3" height="6"></td>
</tr>
<tr>
<td>
<table cellpadding="0" cellspacing="0" style="line-height: 25px;" border="0" align="center">
<tr>
<td colspan="3" height="30"></td>
</tr>
<tr>
<td width="36"></td>
<td width="454" align="left" style="color: #444444; border-collapse: collapse; font-size: 11pt; font-family: 'Open Sans', 'Lucida Grande', 'Segoe UI', Arial, Verdana, 'Lucida Sans Unicode', Tahoma, 'Sans Serif'; max-width: 454px;" valign="top">
<p>Hello there!</p>
<p>We would like to welcome you as our newest member!</p>
<p>Thanks so much for using TellForm! If you have any questions, or suggestions, please feel free to email us here at&nbsp;<a href="mailto:team@tellform.com">team@tellform.com</a>.</p>
<p> - The TellForm team</p>
</td>
<td width="36"></td>
</tr>
<tr>
<td colspan="3" height="36"></td>
</tr>
</table>
</td>
</tr>
</table>
<table cellpadding="0" cellspacing="0" align="center" border="0">
<tr>
<td height="10"></td>
</tr>
<tr>
<td style="padding: 0; border-collapse: collapse;">
<table cellpadding="0" cellspacing="0" align="center" border="0">
<tr style="color: #c0c0c0; font-size: 11px; font-family: 'Open Sans', 'Lucida Grande', 'Segoe UI', Arial, Verdana, 'Lucida Sans Unicode', Tahoma, 'Sans Serif'; -webkit-text-size-adjust: none;">
<td width="400" align="left"></td>
<td width="128" align="right">© TellForm 2017</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</center>
</body>
</html>

48
config/env/all.js vendored
View file

@ -8,25 +8,52 @@ module.exports = {
keywords: process.env.APP_KEYWORDS || 'typeform, pdfs, forms, opensource, formbuilder, google forms, nodejs'
},
db: {
uri: 'mongodb://'+ (process.env.DB_PORT_27017_TCP_ADDR || process.env.DB_HOST || 'localhost')+'/mean',
uri: process.env.MONGOLAB_URI || process.env.MONGODB_URI || 'mongodb://'+ (process.env.DB_PORT_27017_TCP_ADDR || '127.0.0.1') + '/mean',
options: {
user: '',
pass: ''
}
},
admin:{
email: process.env.ADMIN_EMAIL || 'admin@admin.com',
username: process.env.ADMIN_USERNAME || 'root',
password: process.env.ADMIN_PASSWORD || 'root',
},
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
port: process.env.PORT || 3000,
socketPort: process.env.SOCKET_PORT || 20523,
socketPortExternallyVisible: (process.env.SOCKET_PORT_EXTERN_VISIBLE === 'TRUE'),
templateEngine: 'swig',
reCAPTCHA_Key: process.env.reCAPTCHA_KEY || '',
signupDisabled: (process.env.SIGNUP_DISABLED === 'TRUE'),
signupDisabled: (process.env.SIGNUP_DISABLED === 'TRUE'),
enableClusterMode: (process.env.ENABLE_CLUSTER_MODE === 'TRUE'),
baseUrl: '',
baseUrl: process.env.BASE_URL || 'localhost:3000',
tempUserCollection: 'temporary_users',
mailer: {
from: process.env.MAILER_FROM || 'testing@'+process.env.SPARKPOST_SANDBOX_DOMAIN || 'no-reply@tellform.com',
options: process.env.MAILER_SMTP_HOST ? { //Uses custom SMTP if MAILER_SMTP_HOST is set
host: process.env.MAILER_SMTP_HOST || '',
port: process.env.MAILER_SMTP_PORT || 465,
secure: process.env.MAILER_SMTP_SECURE || true,
auth: {
user: process.env.MAILER_EMAIL_ID || '',
pass: process.env.MAILER_PASSWORD || ''
}
} : {
service: process.env.MAILER_SERVICE_PROVIDER || '',
auth: {
user: process.env.MAILER_EMAIL_ID || '',
pass: process.env.MAILER_PASSWORD || ''
}
}
},
subdomainsDisabled: (process.env.SUBDOMAINS_DISABLED === 'TRUE'),
//Sentry DSN Client Key
@ -52,17 +79,6 @@ module.exports = {
// To set the cookie in a specific domain uncomment the following
// setting:
},
/*
* Upload Configuration
*/
//Global upload path
uploadPath : 'uploads/',
//PDF storage path
pdfUploadPath: 'uploads/pdfs/',
//Temp files storage path
tmpUploadPath: 'uploads/tmp/',
// The session cookie name
sessionName: 'connect.sid',
log: {

View file

@ -4,7 +4,7 @@ module.exports = {
baseUrl: process.env.BASE_URL || 'http://localhost:5000',
port: process.env.PORT || 5000,
db: {
uri: 'mongodb://'+( process.env.DB_PORT_27017_TCP_ADDR || process.env.DB_HOST || '0.0.0.0') +'/mean',
uri: process.env.MONGODB_URI || 'mongodb://'+( process.env.DB_PORT_27017_TCP_ADDR || '127.0.0.1') +'/mean',
options: {
user: '',
pass: ''
@ -16,31 +16,6 @@ module.exports = {
// Stream defaults to process.stdout
// Uncomment to enable logging to a log on the file system
},
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@tellform.com',
options: process.env.MAILER_SMTP_HOST ? { //Uses custom SMTP if MAILER_SMTP_HOST is set

View file

@ -3,10 +3,11 @@
module.exports = {
baseUrl: process.env.BASE_URL || process.env.HEROKU_APP_NAME + '.herokuapp.com' || 'tellform.com',
db: {
uri: process.env.MONGODB_URI || process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || '0.0.0.0') + '/mean',
uri: process.env.MONGODB_URI || process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_PORT_27017_TCP_ADDR || '127.0.0.1') + '/mean',
},
port: process.env.PORT || 5000,
socketUrl: process.env.SOCKET_URL || 'ws.tellform.com',
socketPortExternallyVisible: (process.env.SOCKET_PORT_EXTERN_VISIBLE === 'TRUE' || !process.env.SOCKET_PORT_EXTERN_VISIBLE), //socketPortExternallyVisible set to true in production config by default
socketPort: process.env.SOCKET_PORT || 20523,
log: {
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny'
@ -31,48 +32,5 @@ module.exports = {
css: 'public/dist/application.min.css',
js: 'public/dist/application.min.js',
form_js: 'public/dist/form-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 || 'testing@'+process.env.SPARKPOST_SANDBOX_DOMAIN || 'no-reply@tellform.com',
options: process.env.MAILER_SMTP_HOST ? { //Uses custom SMTP if MAILER_SMTP_HOST is set
host: process.env.MAILER_SMTP_HOST || '',
port: process.env.MAILER_SMTP_PORT || 465,
secure: process.env.MAILER_SMTP_SECURE || true,
auth: {
user: process.env.MAILER_EMAIL_ID || '',
pass: process.env.MAILER_PASSWORD || ''
}
} : {
service: process.env.MAILER_SERVICE_PROVIDER || '',
auth: {
user: process.env.MAILER_EMAIL_ID || '',
pass: process.env.MAILER_PASSWORD || ''
}
}
}
};

29
config/env/secure.js vendored
View file

@ -4,7 +4,7 @@ module.exports = {
baseUrl: 'https://forms.polydaic.com',
port: 8443,
db: {
uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://localhost/mean',
uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || process.env.MONGODB_URI || 'mongodb://127.0.0.1/mean',
options: {
user: '',
pass: ''
@ -33,37 +33,12 @@ module.exports = {
maxAge: 7200,
// To set the cookie in a specific domain uncomment the following
// setting:
domain: 'forms.polydaic.com'
domain: process.env.BASE_URL || 'localhost:3000'
},
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: 'https://localhost:443/auth/facebook/callback'
},
twitter: {
clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY',
clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET',
callbackURL: 'https://localhost:443/auth/twitter/callback'
},
google: {
clientID: process.env.GOOGLE_ID || 'APP_ID',
clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET',
callbackURL: 'https://localhost:443/auth/google/callback'
},
linkedin: {
clientID: process.env.LINKEDIN_ID || 'APP_ID',
clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
callbackURL: 'https://localhost:443/auth/linkedin/callback'
},
github: {
clientID: process.env.GITHUB_ID || 'APP_ID',
clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET',
callbackURL: 'https://localhost:443/auth/github/callback'
},
mailer: {
from: process.env.MAILER_FROM || '',
options: process.env.MAILER_SMTP_HOST ? { //Uses custom SMTP if MAILER_SMTP_HOST is set

25
config/env/test.js vendored
View file

@ -25,31 +25,6 @@ module.exports = {
sessionCookie: {
maxAge: 24 * 60 * 60 * 1000 // 24 hours
},
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 || 'MAILER_FROM',
options: process.env.MAILER_SMTP_HOST ? { //Uses custom SMTP if MAILER_SMTP_HOST is set

View file

@ -3,7 +3,7 @@
/**
* Module dependencies.
*/
var fs = require('fs-extra'),
var fs = require('fs'),
https = require('https'),
express = require('express'),
morgan = require('morgan'),
@ -17,22 +17,13 @@ var fs = require('fs-extra'),
passport = require('passport'),
raven = require('raven'),
MongoStore = require('connect-mongo')(session),
flash = require('connect-flash'),
config = require('./config'),
consolidate = require('consolidate'),
path = require('path'),
device = require('express-device'),
client = new raven.Client(config.DSN);
var mongoose = require('mongoose');
var cacheOpts = {
max:100000,
maxAge:1000*60
};
require('mongoose-cache').install(mongoose, cacheOpts);
/**
* Configure Socket.io
*/
@ -63,7 +54,7 @@ module.exports = function(db) {
app.locals.subdomainsDisabled = config.subdomainsDisabled;
if(config.socketPort && process.env.NODE_ENV !== 'production'){
if(config.socketPortExternallyVisible){
app.locals.socketPort = config.socketPort;
} else {
app.locals.socketPort = '';
@ -246,12 +237,6 @@ module.exports = function(db) {
app.use(passport.initialize());
app.use(passport.session());
// setup express-device
app.use(device.capture({ parseUserAgent: true }));
// connect flash for flash messages
app.use(flash());
// Globbing routing files
config.getGlobbedFiles('./app/routes/**/*.js').forEach(function(routePath) {
require(path.resolve(routePath))(app);
@ -295,16 +280,16 @@ module.exports = function(db) {
// Log it
client.captureError(err);
/*if(process.env.NODE_ENV === 'production'){
if(process.env.NODE_ENV === 'production'){
res.status(500).render('500', {
error: 'Internal Server Error'
});
} else {*/
error: 'Internal Server Error'
});
} else {
// Error page
res.status(500).render('500', {
error: err.stack
});
//}
}
});
// Assume 404 since no middleware responded

View file

@ -13,7 +13,12 @@ module.exports = function (app, db) {
if(config.enableClusterMode){
var redis = require('socket.io-redis');
io.adapter(redis( process.env.REDIS_URL || { host: process.env.REDIS_DB_PORT_6379_TCP_ADDR || '127.0.0.1' , port: process.env.REDIS_DB_PORT_6379_TCP_PORT || 6379 }));
if( process.env.REDIS_DB_PORT_6379_TCP_ADDR ){
io.adapter(redis({ host: process.env.REDIS_DB_PORT_6379_TCP_ADDR || '127.0.0.1' , port: process.env.REDIS_DB_PORT_6379_TCP_PORT || 6379 }));
} else {
io.adapter(redis( config.redisUrl ));
}
}
// Add an event listener to the 'connection' event
io.on('connection', function (socket) {
@ -23,4 +28,4 @@ module.exports = function (app, db) {
});
return server;
};
};

View file

@ -1,41 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
FacebookStrategy = require('passport-facebook').Strategy,
config = require('../config'),
users = require('../../app/controllers/users.server.controller');
module.exports = function() {
// Use facebook strategy
passport.use(new FacebookStrategy({
clientID: config.facebook.clientID,
clientSecret: config.facebook.clientSecret,
callbackURL: config.facebook.callbackURL,
passReqToCallback: true
},
function(req, accessToken, refreshToken, profile, done) {
// Set the provider data and include tokens
var providerData = profile._json;
providerData.accessToken = accessToken;
providerData.refreshToken = refreshToken;
// Create the user OAuth profile
var providerUserProfile = {
firstName: profile.name.givenName,
lastName: profile.name.familyName,
displayName: profile.displayName,
email: profile.emails[0].value,
username: profile.username,
provider: 'facebook',
providerIdentifierField: 'id',
providerData: providerData
};
// Save the user OAuth profile
users.saveOAuthUserProfile(req, providerUserProfile, done);
}
));
};

View file

@ -1,46 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
GithubStrategy = require('passport-github').Strategy,
config = require('../config'),
users = require('../../app/controllers/users.server.controller');
module.exports = function() {
// Use github strategy
passport.use(new GithubStrategy({
clientID: config.github.clientID,
clientSecret: config.github.clientSecret,
callbackURL: config.github.callbackURL,
passReqToCallback: true
},
function(req, accessToken, refreshToken, profile, done) {
// Set the provider data and include tokens
var providerData = profile._json;
providerData.accessToken = accessToken;
providerData.refreshToken = refreshToken;
// Create the user OAuth profile
var displayName = profile.displayName.trim();
var iSpace = displayName.indexOf(' '); // index of the whitespace following the firstName
var firstName = iSpace !== -1 ? displayName.substring(0, iSpace) : displayName;
var lastName = iSpace !== -1 ? displayName.substring(iSpace + 1) : '';
var providerUserProfile = {
firstName: firstName,
lastName: lastName,
displayName: displayName,
email: profile.emails[0].value,
username: profile.username,
provider: 'github',
providerIdentifierField: 'id',
providerData: providerData
};
// Save the user OAuth profile
users.saveOAuthUserProfile(req, providerUserProfile, done);
}
));
};

View file

@ -1,41 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
GoogleStrategy = require('passport-google-oauth').OAuth2Strategy,
config = require('../config'),
users = require('../../app/controllers/users.server.controller');
module.exports = function() {
// Use google strategy
passport.use(new GoogleStrategy({
clientID: config.google.clientID,
clientSecret: config.google.clientSecret,
callbackURL: config.google.callbackURL,
passReqToCallback: true
},
function(req, accessToken, refreshToken, profile, done) {
// Set the provider data and include tokens
var providerData = profile._json;
providerData.accessToken = accessToken;
providerData.refreshToken = refreshToken;
// Create the user OAuth profile
var providerUserProfile = {
firstName: profile.name.givenName,
lastName: profile.name.familyName,
displayName: profile.displayName,
email: profile.emails[0].value,
username: profile.username,
provider: 'google',
providerIdentifierField: 'id',
providerData: providerData
};
// Save the user OAuth profile
users.saveOAuthUserProfile(req, providerUserProfile, done);
}
));
};

View file

@ -1,42 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
LinkedInStrategy = require('passport-linkedin').Strategy,
config = require('../config'),
users = require('../../app/controllers/users.server.controller');
module.exports = function() {
// Use linkedin strategy
passport.use(new LinkedInStrategy({
consumerKey: config.linkedin.clientID,
consumerSecret: config.linkedin.clientSecret,
callbackURL: config.linkedin.callbackURL,
passReqToCallback: true,
profileFields: ['id', 'first-name', 'last-name', 'email-address']
},
function(req, accessToken, refreshToken, profile, done) {
// Set the provider data and include tokens
var providerData = profile._json;
providerData.accessToken = accessToken;
providerData.refreshToken = refreshToken;
// Create the user OAuth profile
var providerUserProfile = {
firstName: profile.name.givenName,
lastName: profile.name.familyName,
displayName: profile.displayName,
email: profile.emails[0].value,
username: profile.username,
provider: 'linkedin',
providerIdentifierField: 'id',
providerData: providerData
};
// Save the user OAuth profile
users.saveOAuthUserProfile(req, providerUserProfile, done);
}
));
};

View file

@ -1,45 +0,0 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
TwitterStrategy = require('passport-twitter').Strategy,
config = require('../config'),
users = require('../../app/controllers/users.server.controller');
module.exports = function() {
// Use twitter strategy
passport.use(new TwitterStrategy({
consumerKey: config.twitter.clientID,
consumerSecret: config.twitter.clientSecret,
callbackURL: config.twitter.callbackURL,
passReqToCallback: true
},
function(req, token, tokenSecret, profile, done) {
// Set the provider data and include tokens
var providerData = profile._json;
providerData.token = token;
providerData.tokenSecret = tokenSecret;
// Create the user OAuth profile
var displayName = profile.displayName.trim();
var iSpace = displayName.indexOf(' '); // index of the whitespace following the firstName
var firstName = iSpace !== -1 ? displayName.substring(0, iSpace) : displayName;
var lastName = iSpace !== -1 ? displayName.substring(iSpace + 1) : '';
var providerUserProfile = {
firstName: firstName,
lastName: lastName,
displayName: displayName,
username: profile.username,
provider: 'twitter',
providerIdentifierField: 'id_str',
providerData: providerData
};
// Save the user OAuth profile
users.saveOAuthUserProfile(req, providerUserProfile, done);
}
));
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

BIN
design/tellform_mascot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -1,22 +0,0 @@
version: "3"
services:
tellform:
build:
context: ./
dockerfile: dockerfile
image: tellform
ports:
- 3000:3000
links:
- tellform-redis:redis-db
- tellform-mongo:db
env_file:
- .env
tellform-redis:
image: redis
ports:
- 6379
tellform-mongo:
image: mongo
ports:
- 27017

View file

@ -1,96 +0,0 @@
[
[{
"fieldType": "textfield",
"fieldValue": "snthsnth",
"_id": "55aec5d284bae1a1996210bd",
"disabled": false,
"fieldOptions": [],
"description": "",
"title": "Short Text2",
"lastModified": "2015-07-21T22:21:06.653Z",
"created": "2015-07-21T22:21:06.653Z",
"$$hashKey": "02J"
}, {
"fieldType": "textfield",
"fieldValue": "duieedi",
"_id": "55aec5b084bae1a1996210b4",
"disabled": false,
"fieldOptions": [],
"description": "",
"title": "Last Name",
"lastModified": "2015-07-21T22:20:32.053Z",
"created": "2015-07-21T22:20:32.053Z",
"$$hashKey": "02K"
}],
[{
"fieldType": "textfield",
"fieldValue": "snthsnth",
"_id": "55aec5d284bae1a1996210bd",
"disabled": false,
"fieldOptions": [],
"description": "",
"title": "Short Text2",
"lastModified": "2015-07-21T22:21:06.653Z",
"created": "2015-07-21T22:21:06.653Z",
"$$hashKey": "02J"
}, {
"fieldType": "textfield",
"fieldValue": "duieedi",
"_id": "55aec5b084bae1a1996210b4",
"disabled": false,
"fieldOptions": [],
"description": "",
"title": "Last Name",
"lastModified": "2015-07-21T22:20:32.053Z",
"created": "2015-07-21T22:20:32.053Z",
"$$hashKey": "02K"
}],
[{
"fieldType": "textfield",
"fieldValue": "snthsnth",
"_id": "55aec5d284bae1a1996210bd",
"disabled": false,
"fieldOptions": [],
"description": "",
"title": "Short Text2",
"lastModified": "2015-07-21T22:21:06.653Z",
"created": "2015-07-21T22:21:06.653Z",
"$$hashKey": "02J"
}, {
"fieldType": "textfield",
"fieldValue": "duieedi",
"_id": "55aec5b084bae1a1996210b4",
"disabled": false,
"fieldOptions": [],
"description": "",
"title": "Last Name",
"lastModified": "2015-07-21T22:20:32.053Z",
"created": "2015-07-21T22:20:32.053Z",
"$$hashKey": "02K"
}],
[{
"fieldType": "textfield",
"fieldValue": "snthsnth",
"_id": "55aec5d284bae1a1996210bd",
"disabled": false,
"fieldOptions": [],
"description": "",
"title": "Short Text2",
"lastModified": "2015-07-21T22:21:06.653Z",
"created": "2015-07-21T22:21:06.653Z",
"$$hashKey": "02J"
}, {
"fieldType": "textfield",
"fieldValue": "duieedi",
"_id": "55aec5b084bae1a1996210b4",
"disabled": false,
"fieldOptions": [],
"description": "",
"title": "Last Name",
"lastModified": "2015-07-21T22:20:32.053Z",
"created": "2015-07-21T22:20:32.053Z",
"$$hashKey": "02K"
}]
]

View file

@ -1,758 +0,0 @@
[
{
"_id": "56450d120761e9d7d68d3543",
"lastModified": "2015-11-12T22:05:06.380Z",
"fdfData": null,
"admin": "55d270df2749c1ceb47c0f8b",
"form": "5644dde5507b2572635dcd50",
"title": "Sample Form",
"timeElapsed": 11.925,
"percentageComplete": 75,
"__v": 0,
"created": "2015-11-12T22:05:06.378Z",
"form_fields": [
{
"_id": "5644e0d5507b2572635dcd56",
"fieldValue": "",
"fieldType": "statement",
"title": "Statement2",
"lastModified": "2015-11-12T18:56:21.730Z",
"created": "2015-11-12T18:56:21.660Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": true,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.769Z",
"title": "Short Text2",
"fieldType": "textfield",
"fieldValue": "first",
"_id": "5644dde7507b2572635dcd51",
"created": "2015-11-12T18:43:51.290Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.770Z",
"title": "Short Text3",
"fieldType": "textfield",
"fieldValue": "first",
"_id": "5644dde7507b2572635dcd52",
"created": "2015-11-12T18:43:51.977Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.771Z",
"title": "Paragraph Text2",
"fieldType": "textarea",
"fieldValue": "first",
"_id": "5644ddea507b2572635dcd54",
"created": "2015-11-12T18:43:54.594Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.771Z",
"title": "Rating2",
"fieldType": "rating",
"fieldValue": 1,
"_id": "5644e0d4507b2572635dcd55",
"created": "2015-11-12T18:56:20.324Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
}
]
},
{
"_id": "56450d1d0761e9d7d68d3544",
"lastModified": "2015-11-12T22:05:17.806Z",
"fdfData": null,
"admin": "55d270df2749c1ceb47c0f8b",
"form": "5644dde5507b2572635dcd50",
"title": "Sample Form",
"timeElapsed": 10.301,
"percentageComplete": 75,
"__v": 0,
"created": "2015-11-12T22:05:17.805Z",
"form_fields": [
{
"_id": "5644e0d5507b2572635dcd56",
"fieldValue": "",
"fieldType": "statement",
"title": "Statement2",
"lastModified": "2015-11-12T18:56:21.730Z",
"created": "2015-11-12T18:56:21.660Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": true,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.769Z",
"title": "Short Text2",
"fieldType": "textfield",
"fieldValue": "second",
"_id": "5644dde7507b2572635dcd51",
"created": "2015-11-12T18:43:51.290Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.770Z",
"title": "Short Text3",
"fieldType": "textfield",
"fieldValue": "second",
"_id": "5644dde7507b2572635dcd52",
"created": "2015-11-12T18:43:51.977Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.771Z",
"title": "Paragraph Text2",
"fieldType": "textarea",
"fieldValue": "second",
"_id": "5644ddea507b2572635dcd54",
"created": "2015-11-12T18:43:54.594Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.771Z",
"title": "Rating2",
"fieldType": "rating",
"fieldValue": 3,
"_id": "5644e0d4507b2572635dcd55",
"created": "2015-11-12T18:56:20.324Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
}
]
},
{
"_id": "56450d260761e9d7d68d3545",
"lastModified": "2015-11-12T22:05:26.874Z",
"fdfData": null,
"admin": "55d270df2749c1ceb47c0f8b",
"form": "5644dde5507b2572635dcd50",
"title": "Sample Form",
"timeElapsed": 7.975,
"percentageComplete": 75,
"__v": 0,
"created": "2015-11-12T22:05:26.874Z",
"form_fields": [
{
"_id": "5644e0d5507b2572635dcd56",
"fieldValue": "",
"fieldType": "statement",
"title": "Statement2",
"lastModified": "2015-11-12T18:56:21.730Z",
"created": "2015-11-12T18:56:21.660Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": true,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.769Z",
"title": "Short Text2",
"fieldType": "textfield",
"fieldValue": "third",
"_id": "5644dde7507b2572635dcd51",
"created": "2015-11-12T18:43:51.290Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.770Z",
"title": "Short Text3",
"fieldType": "textfield",
"fieldValue": "third",
"_id": "5644dde7507b2572635dcd52",
"created": "2015-11-12T18:43:51.977Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.771Z",
"title": "Paragraph Text2",
"fieldType": "textarea",
"fieldValue": "third",
"_id": "5644ddea507b2572635dcd54",
"created": "2015-11-12T18:43:54.594Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.771Z",
"title": "Rating2",
"fieldType": "rating",
"fieldValue": 2,
"_id": "5644e0d4507b2572635dcd55",
"created": "2015-11-12T18:56:20.324Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
}
]
},
{
"_id": "56450d320761e9d7d68d3546",
"lastModified": "2015-11-12T22:05:38.717Z",
"fdfData": null,
"admin": "55d270df2749c1ceb47c0f8b",
"form": "5644dde5507b2572635dcd50",
"title": "Sample Form",
"timeElapsed": 10.47,
"percentageComplete": 75,
"__v": 0,
"created": "2015-11-12T22:05:38.716Z",
"form_fields": [
{
"_id": "5644e0d5507b2572635dcd56",
"fieldValue": "",
"fieldType": "statement",
"title": "Statement2",
"lastModified": "2015-11-12T18:56:21.730Z",
"created": "2015-11-12T18:56:21.660Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": true,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.769Z",
"title": "Short Text2",
"fieldType": "textfield",
"fieldValue": "fourth",
"_id": "5644dde7507b2572635dcd51",
"created": "2015-11-12T18:43:51.290Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.770Z",
"title": "Short Text3",
"fieldType": "textfield",
"fieldValue": "fourth",
"_id": "5644dde7507b2572635dcd52",
"created": "2015-11-12T18:43:51.977Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.771Z",
"title": "Paragraph Text2",
"fieldType": "textarea",
"fieldValue": "fourth",
"_id": "5644ddea507b2572635dcd54",
"created": "2015-11-12T18:43:54.594Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
},
{
"lastModified": "2015-11-12T21:30:20.771Z",
"title": "Rating2",
"fieldType": "rating",
"fieldValue": 2,
"_id": "5644e0d4507b2572635dcd55",
"created": "2015-11-12T18:56:20.324Z",
"validFieldTypes": [
"textfield",
"date",
"email",
"link",
"legal",
"url",
"textarea",
"statement",
"welcome",
"thankyou",
"file",
"dropdown",
"scale",
"rating",
"radio",
"checkbox",
"hidden",
"yes_no",
"natural",
"number"
],
"deletePreserved": false,
"disabled": false,
"required": true,
"fieldOptions": [],
"description": ""
}
]
}
]

View file

@ -1,22 +0,0 @@
##First Form Auto-Update
1. Added field CLIENT
time: 2553
2. Finding form SERVER
time: 2841
3. Update form CLIENT
time: 2870
4.Updated form SERVER
time: 2863
##Second Form Auto-Update
1. Added field CLIENT
time: 2755
2. Finding form SERVER
time: 2898

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,249 +0,0 @@
#Installing Sentry Server
### Before You Begin
Make sure you understand what sentry server does. You can view documentation for sentry server [here](https://sentry.readthedocs.org/). This document was written for a server running *Ubuntu 14.04 LTS server* (we used Azure).
### Prerequisites
Make sure you have these installed:
* apt-get
* python2.7
* vim
### Installation Steps
1. Add non-root user.
```
sudo adduser sentry
sudo adduser sentry sudo
```
2. Update all apt-get packages
```
sudo apt-get update
sudo apt-get dist-upgrade
sudo apt-get autoremove
sudo apt-get install libxml2-dev libxslt1-dev libffi-dev libpq-dev python-dev
sudo reboot
```
3. Install easy_install and pip
```
wget https://bootstrap.pypa.io/ez_setup.py -O - | sudo python
sudo easy_install pip
```
4. Install virtualenv and lmxl
```
sudo pip install virtualenv
sudo pip install lxml
```
5. Install Sentry and Setup
```
# make server directory
mkdir ~/SentryServer; cd ~/SentryServer;
# make virtualenv
virtualenv ./
#activate virtualenv
source ./bin/activate
# install sentry and its postgresql dependencies
pip install -U sentry[postgres]
```
6. Install postgresql
```
# install postgres
sudo apt-get install postgresql postgresql-contrib libpq-dev
# install postgres adminpack
sudo -u postgres psql
CREATE EXTENSION "adminpack";
\q
```
7. Setup postgresql DB
```
# change postgres password & create database
sudo passwd postgres
sudo su - postgres
psql -d template1 -c "ALTER USER postgres WITH PASSWORD 'changeme';"
createdb sentry
createuser sentry_user --pwprompt
psql -d template1 -U postgres
GRANT ALL PRIVILEGES ON DATABASE sentry to sentry_user;
\q
exit
```
8. Setup Sentry Configuration
```
# initialize conf file
sentry init
#edit sentry configuration
vim ~/.sentry/sentry.conf.py
```
The following are the contents of my sentry.conf.py file (replace name, user and password with your that of your DB)
```
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'sentry',
'USER': 'sentry_user',
'PASSWORD': 'your_password',
'HOST': 'localhost',
}
}
# No trailing slash!
SENTRY_URL_PREFIX = 'http://sentry.example.com'
SENTRY_WEB_HOST = '0.0.0.0'
SENTRY_WEB_PORT = 9000
SENTRY_WEB_OPTIONS = {
'workers': 3, # the number of gunicorn workers
'secure_scheme_headers': {'X-FORWARDED-PROTO': 'https'}, # detect HTTPS mode from X-Forwarded-Proto header
}
#CONFIGURE REDIS
SENTRY_REDIS_OPTIONS = {
'hosts': {
0: {
'host': '127.0.0.1',
'port': 6379,
'timeout': 3,
#'password': 'redis auth password'
}
}
}
#CONFIGURE OUTGOING MAIL
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_USE_TLS = True
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'your_gmail_username@gmail.com'
EMAIL_HOST_PASSWORD = 'your_gmail_password'
DEFAULT_FROM_EMAIL = 'testing@testing.com
```
9. Setup Database and Start Sentry
```
#install and run redis-server
wget http://download.redis.io/releases/redis-stable.tar.gz
tar xzf redis-stable.tar.gz
cd redis-stable
make
make test
sudo make install
cd utils
sudo ./install_server.sh
#Go back to app directory
cd ~/SentryServer
sudo service redis_6379 start
# set up databse
sentry upgrade
# let's try it out!
sentry start
```
10. Install nginx
```
# install nginx
sudo apt-get install nginx
# remove the default symbolic link
sudo rm /etc/nginx/sites-enabled/default
# create a new blank config, and make a symlink to it
sudo touch /etc/nginx/sites-available/sentry
cd /etc/nginx/sites-enabled
sudo ln -s ../sites-available/sentry
# edit the nginx configuration file
sudo vim /etc/nginx/sites-available/sentry
```
*Here are the contents of my nginx file:*
```
server {
# listen on port 80
listen 80;
# for requests to these domains
server_name yourdomain.com www.yourdomain.com;
# keep logs in these files
access_log /var/log/nginx/sentry.access.log;
error_log /var/log/nginx/sentry.error.log;
# You need this to allow users to upload large files
# See http://wiki.nginx.org/HttpCoreModule#client_max_body_size
# I'm not sure where it goes, so I put it in twice. It works.
client_max_body_size 0;
location / {
proxy_pass http://localhost:9000;
proxy_redirect off;
proxy_read_timeout 5m;
# make sure these HTTP headers are set properly
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
```
11. Start the worker processes
```
# restart nginx
sudo nginx -t
sudo service nginx reload
#start sentry
sentry celery worker -B
```
12. Install and Configure supervisord
```
pip install supervisord
sudo echo_supervisord_conf > ~/SentryServer/etc/supervisord.conf
#Edit yuour supervisord Config
vim /etc/supervisord.conf
```
Configuration file should look like this
```
[program:sentry-web]
directory=~/SentryServer/
command=~/SentryServer/bin/sentry start
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=syslog
stderr_logfile=syslog
[program:sentry-worker]
directory=~/SentryServer/
command=~/SentryServer/bin/sentry celery worker -B
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=syslog
stderr_logfile=syslog
```
13. Run Server (with supervisord)
```
supervisord
```

View file

@ -1,29 +0,0 @@
UX of Updating Forms
====================
##Form Updating
- Action: Update Configuration
1. User clicks on Form from FormList page (visits form admin page)
2. User clicks on "Configure Form" Tab
3. User changes inputs in form page
4. Save Button is Pressed
5. Loading/Busy Indicator fills the screen
6. Loading/Busy Indicator exits the screen
7. Configuration page is shown with updated settings
- Action: Add Form Field
1. User clicks on Form from FormList page
2. User clicks on a Tab in "AddField" column
3. Loading/Busy Indicator fills the screen
4. Loading/Busy Indicator exits the screen
- Action: Edit Form Field Title
1. User clicks on Form from FormList page (visits form admin page)
2. User clicks on a current Form Input accordion (must be either the name or the caret)
3. The clicked accordion (the one interacted with in Step#2) expands
4. User clicks on 'Question Title' text input (aka focuses on said text input)
5. User starts typing in text input
6. User defocuses/clicks off of text input
7. Loading/Busy Indicator fills the screen
8. Loading/Busy Indicator exits the screen
9. Field Accordion heading is updated

View file

@ -28,10 +28,10 @@ module.exports = function(grunt) {
clientViews: ['public/modules/**/*.html', 'public/form_modules/forms/base/**/*.html', '!public/modules/forms/base/**/*.html',],
clientJS: ['public/js/*.js', 'public/form_modules/**/*.js', 'public/modules/**/*.js'],
clientCSS: ['public/modules/**/*.css', 'public/form_modules/**/*.css', '!public/modules/**/demo/**/*.css', '!public/modules/**/dist/**/*.css'],
clientCSS: ['public/modules/**/*.css'],
serverTests: ['app/tests/**/*.js'],
clientTests: ['public/modules/**/tests/*.js', '!public/modules/**/demo/**/*.js', '!public/modules/**/dist/**/*.js', '!public/modules/**/node_modules/**/*.js']
clientTests: ['public/modules/**/tests/*.js']
};
watchFiles.allTests = watchFiles.serverTests.concat(watchFiles.clientTests);
@ -127,19 +127,6 @@ module.exports = function(grunt) {
}
}
},
'closure-compiler': {
vendor_file: {
closurePath: './scripts',
js: 'public/dist/vendor_forms_uglified.js',
jsOutputFile: 'public/dist/vendor.min.js',
maxBuffer: 10000000000,
options: {
warning_level: 'QUIET',
compilation_level: 'SIMPLE_OPTIMIZATIONS',
language_in: 'ECMASCRIPT5'
}
}
},
cssmin: {
combine: {
files: {
@ -157,19 +144,6 @@ module.exports = function(grunt) {
}
}
},
'node-inspector': {
custom: {
options: {
'web-port': 1337,
'web-host': 'localhost',
'debug-port': 5858,
'save-live-edit': true,
'no-preload': true,
'stack-trace-limit': 50,
'hidden': []
}
}
},
ngAnnotate: {
production: {
files: {
@ -180,28 +154,26 @@ module.exports = function(grunt) {
},
concurrent: {
default: ['nodemon', 'watch'],
debug: ['nodemon', 'watch', 'node-inspector'],
debug: ['nodemon', 'watch'],
options: {
logConcurrentOutput: true,
limit: 10
}
},
env: {
src: '.env',
test: {
NODE_ENV: 'test',
src: '.env'
},
secure: {
NODE_ENV: 'secure',
src: '/opt/deploy/.env'
},
production: {
NODE_ENV: 'production',
src: '/opt/deploy/.env'
},
dev: {
NODE_ENV: 'development',
src: '.env'
}
},
mochaTest: {
@ -211,7 +183,7 @@ module.exports = function(grunt) {
quiet: false,
require: 'server.js',
ui: 'bdd',
debug: true
debug: false
}
},
karma: {

View file

@ -1,7 +1,7 @@
{
"name": "TellForm",
"description": "Opensource alternative to TypeForm",
"version": "2.0.0",
"version": "2.1.0",
"homepage": "https://github.com/whitef0x0/tellform",
"authors": [
"David Baldwynn <polydaic@gmail.com> (http://baldwynn.me)"
@ -13,8 +13,8 @@
"url": "https://github.com/whitef0x0/tellform.git"
},
"engines": {
"node": "6.11.2",
"npm": "3.10.10"
"node": "6.x.x",
"npm": "3.x.x"
},
"scripts": {
"addcontrib": "all-contributors add",
@ -22,82 +22,55 @@
"start": "npm run version && grunt",
"test": "npm run version && grunt test",
"postinstall": "bower install --config.interactive=false; grunt build;",
"init": "node scripts/setup.js",
"version": "check-node-version --package"
"init": "node scripts/setup.js"
},
"dependencies": {
"async": "^1.4.2",
"async-boolean-expression-evaluator": "^1.1.1",
"bcrypt": "^0.8.7",
"bcrypt-nodejs": "0.0.3",
"body-parser": "~1.14.1",
"bower": "~1.6.5",
"chalk": "^1.1.3",
"check-node-version": "^2.1.0",
"compression": "~1.6.0",
"connect": "^3.4.1",
"connect-flash": "~0.1.1",
"connect-mongo": "~0.8.2",
"consolidate": "~0.13.1",
"cookie-parser": "~1.4.0",
"deep-diff": "^0.3.4",
"dotenv": "^2.0.0",
"email-verification": "~0.4.1",
"envfile": "^2.0.1",
"express": "~4.13.3",
"express-device": "~0.4.2",
"express-session": "~1.12.1",
"forever": "~0.15.1",
"fs-extra": "~0.26.2",
"glob": "^7.0.3",
"google-cdn": "^1.1.0",
"grunt": "~0.4.1",
"grunt-cli": "~0.1.13",
"grunt-concurrent": "~2.3.0",
"grunt-contrib-csslint": "~1.0.0",
"grunt-contrib-cssmin": "~1.0.1",
"grunt-contrib-jshint": "~1.0.0",
"grunt-contrib-uglify": "~0.11.0",
"grunt-contrib-watch": "~0.6.1",
"grunt-env": "~0.4.1",
"grunt-html2js": "~0.3.5",
"grunt-karma": "~0.12.1",
"grunt-newer": "~1.1.1",
"grunt-ng-annotate": "~1.0.1",
"grunt-node-inspector": "~0.4.1",
"grunt-nodemon": "~0.4.0",
"helmet": "3.5.0",
"inquirer": "^1.0.2",
"jit-grunt": "^0.9.1",
"lodash": "^4.17.4",
"main-bower-files": "~2.9.0",
"method-override": "~2.3.0",
"mkdirp": "^0.5.1",
"mongoose": "~4.4.19",
"mongoose-cache": "^0.1.5",
"mongoose-utilities": "~0.1.1",
"morgan": "~1.8.1",
"multer": "^1.3.0",
"nodemailer": "~4.0.0",
"nodemailer-sendgrid-transport": "^0.2.0",
"nodemailer-sparkpost-transport": "^1.0.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",
"passport-linkedin": "~1.0.0",
"passport-local": "~1.0.0",
"passport-localapikey-update": "^0.5.0",
"passport-twitter": "~1.0.2",
"path-exists": "^2.1.0",
"prerender-node": "^2.2.1",
"random-js": "^1.0.8",
"raven": "^0.9.0",
"request": "^2.83.0",
"socket.io": "^1.4.6",
"socket.io-redis": "^1.0.0",
"swig": "~1.4.1",
"uid-generator": "^0.1.1",
"uuid-token-generator": "^0.5.0",
"wildcard-subdomains": "github:tellform/wildcard-subdomains",
"winston": "^2.3.1",
@ -109,7 +82,7 @@
"coveralls": "^2.11.4",
"cross-spawn": "^5.0.0",
"del": "^2.2.2",
"email-verification": "github:tellform/node-email-verification",
"grunt-cli": "~0.1.13",
"grunt-closure-compiler": "0.0.21",
"grunt-contrib-concat": "^1.0.1",
"grunt-contrib-copy": "^1.0.0",
@ -117,7 +90,11 @@
"grunt-execute": "^0.2.2",
"grunt-mocha-istanbul": "^3.0.1",
"grunt-mocha-test": "~0.12.1",
"grunt-contrib-watch": "~0.6.1",
"grunt-newer": "~1.1.1",
"grunt-usemin": "^3.1.1",
"grunt-karma": "~0.12.1",
"grunt-nodemon": "~0.4.0",
"grunt-wiredep": "^3.0.1",
"istanbul": "^0.4.0",
"jasmine-core": "^2.4.1",
@ -130,11 +107,9 @@
"karma-mocha-reporter": "^1.1.1",
"karma-ng-html2js-preprocessor": "^0.2.0",
"karma-phantomjs-launcher": "^1.0.4",
"mailosaur": "^1.0.1",
"mocha": "^3.1.2",
"mocha-lcov-reporter": "^1.0.0",
"nightwatch": "^0.9.8",
"node-mandrill": "^1.0.1",
"phantomjs": "^1.9.18",
"selenium-server": "^3.0.1",
"should": "~7.1.1",

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

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

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,9 @@ angular.module('view-form').controller('SubmitFormController', [
'$scope', '$rootScope', '$state', '$translate', 'myForm',
function($scope, $rootScope, $state, $translate, myForm) {
$scope.myform = myForm;
$(".loader").fadeOut("slow");
document.body.style.background = myForm.design.colors.backgroundColor;
$translate.use(myForm.language);
}
]);

View file

@ -25,7 +25,7 @@ angular.module('view-form').directive('onEnterKey', ['$rootScope', function($roo
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
$element.bind('keydown keypress', function(event) {
$element.bind('keyup keypress', function(event) {
var keyCode = event.which || event.keyCode;
@ -65,6 +65,8 @@ angular.module('view-form').directive('onEnterKey', ['$rootScope', function($roo
var keyCode = event.which || event.keyCode;
if(keyCode === 9 && event.shiftKey) {
console.log('onTabAndShiftKey');
event.preventDefault();
$rootScope.$apply(function() {
$rootScope.$eval($attrs.onTabAndShiftKey);

View file

@ -9,8 +9,8 @@ jsep.addBinaryOp('!begins', 10);
jsep.addBinaryOp('ends', 10);
jsep.addBinaryOp('!ends', 10);
angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCounter', '$filter', '$rootScope', 'SendVisitorData', '$translate',
function ($http, TimeCounter, $filter, $rootScope, SendVisitorData, $translate) {
angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCounter', '$filter', '$rootScope', 'SendVisitorData', '$translate', '$timeout',
function ($http, TimeCounter, $filter, $rootScope, SendVisitorData, $translate, $timeout) {
return {
templateUrl: 'form_modules/forms/base/views/directiveViews/form/submit-form.client.view.html',
restrict: 'E',
@ -19,7 +19,8 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
ispreview: '='
},
controller: function($document, $window, $scope){
$scope.noscroll = false;
var NOSCROLL = false;
var FORM_ACTION_ID = 'submit_field';
$scope.forms = {};
//Don't start timer if we are looking at a design preview
@ -59,99 +60,64 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
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();
$scope.fieldTop = elemBox.top;
$scope.fieldBottom = elemBox.bottom;
var field_id;
var field_index;
if(!$scope.noscroll){
//Focus on submit button
if( $scope.selected.index === $scope.myform.visible_form_fields.length-1 && $scope.fieldBottom < 200){
field_index = $scope.selected.index+1;
field_id = 'submit_field';
$scope.setActiveField(field_id, field_index, false);
}
//Focus on field above submit button
else if($scope.selected.index === $scope.myform.visible_form_fields.length){
if($scope.fieldTop > 200){
field_index = $scope.selected.index-1;
field_id = $scope.myform.visible_form_fields[field_index]._id;
$scope.setActiveField(field_id, field_index, false);
}
} else if( $scope.fieldBottom < 0){
field_index = $scope.selected.index+1;
field_id = $scope.myform.visible_form_fields[field_index]._id;
$scope.setActiveField(field_id, field_index, false);
} else if ( $scope.selected.index !== 0 && $scope.fieldTop > 0) {
field_index = $scope.selected.index-1;
field_id = $scope.myform.visible_form_fields[field_index]._id;
$scope.setActiveField(field_id, field_index, false);
}
$scope.$apply();
}
};
/*
** Field Controls
*/
var evaluateLogicJump = function(field){
var logicJump = field.logicJump;
if (logicJump.expressionString && logicJump.valueB && field.fieldValue) {
var parse_tree = jsep(logicJump.expressionString);
var left, right;
if(logicJump.enabled){
if (logicJump.expressionString && logicJump.valueB && field.fieldValue) {
var parse_tree = jsep(logicJump.expressionString);
var left, right;
if(parse_tree.left.name === 'field'){
left = field.fieldValue;
right = logicJump.valueB;
} else {
left = logicJump.valueB;
right = field.fieldValue;
}
if(field.fieldType === 'number' || field.fieldType === 'scale' || field.fieldType === 'rating'){
switch(parse_tree.operator) {
case '==':
return (parseInt(left) === parseInt(right));
case '!==':
return (parseInt(left) !== parseInt(right));
case '>':
return (parseInt(left) > parseInt(right));
case '>=':
return (parseInt(left) > parseInt(right));
case '<':
return (parseInt(left) < parseInt(right));
case '<=':
return (parseInt(left) <= parseInt(right));
default:
return false;
if(parse_tree.left.name === 'field'){
left = field.fieldValue;
right = logicJump.valueB;
} else {
left = logicJump.valueB;
right = field.fieldValue;
}
} else {
switch(parse_tree.operator) {
case '==':
return (left === right);
case '!==':
return (left !== right);
case 'contains':
return (left.indexOf(right) > -1);
case '!contains':
/* jshint -W018 */
return !(left.indexOf(right) > -1);
case 'begins':
return left.startsWith(right);
case '!begins':
return !left.startsWith(right);
case 'ends':
return left.endsWith(right);
case '!ends':
return left.endsWith(right);
default:
return false;
if(field.fieldType === 'number' || field.fieldType === 'scale' || field.fieldType === 'rating'){
switch(parse_tree.operator) {
case '==':
return (parseInt(left) === parseInt(right));
case '!==':
return (parseInt(left) !== parseInt(right));
case '>':
return (parseInt(left) > parseInt(right));
case '>=':
return (parseInt(left) > parseInt(right));
case '<':
return (parseInt(left) < parseInt(right));
case '<=':
return (parseInt(left) <= parseInt(right));
default:
return false;
}
} else {
switch(parse_tree.operator) {
case '==':
return (left === right);
case '!==':
return (left !== right);
case 'contains':
return (left.indexOf(right) > -1);
case '!contains':
/* jshint -W018 */
return !(left.indexOf(right) > -1);
case 'begins':
return left.startsWith(right);
case '!begins':
return !left.startsWith(right);
case 'ends':
return left.endsWith(right);
case '!ends':
return left.endsWith(right);
default:
return false;
}
}
}
}
@ -163,35 +129,46 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
throw new Error('current active field is null');
}
if($scope.selected._id === 'submit_field') {
if($scope.selected._id === FORM_ACTION_ID) {
return $scope.myform.form_fields.length - 1;
}
return $scope.selected.index;
};
$scope.isActiveField = function(field){
if($scope.selected._id === field._id) {
return true
}
return false;
};
$scope.setActiveField = $rootScope.setActiveField = function(field_id, field_index, animateScroll) {
if($scope.selected === null || $scope.selected._id === field_id){
//console.log('not scrolling');
//console.log($scope.selected);
return;
}
//console.log('field_id: '+field_id);
//console.log('field_index: '+field_index);
//console.log($scope.selected);
if($scope.selected === null || (!field_id && field_index === null) ) {
return;
}
if(!field_id){
field_id = $scope.myform.visible_form_fields[field_index]._id;
} else if(field_index === null){
field_index = $scope.myform.visible_form_fields.length
$scope.selected._id = field_id;
$scope.selected.index = field_index;
if(!field_index){
for(var i=0; i<$scope.myform.visible_form_fields.length; i++){
for(var i=0; i < $scope.myform.visible_form_fields.length; i++){
var currField = $scope.myform.visible_form_fields[i];
if(field_id === currField._id){
$scope.selected.index = i;
if(currField['_id'] == field_id){
field_index = i;
break;
}
}
}
if($scope.selected._id === field_id){
return;
}
$scope.selected._id = field_id;
$scope.selected.index = field_index;
var nb_valid = $filter('formValidity')($scope.myform);
$scope.translateAdvancementData = {
done: nb_valid,
@ -200,10 +177,10 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
};
if(animateScroll){
$scope.noscroll=true;
NOSCROLL=true;
setTimeout(function() {
$document.scrollToElement(angular.element('.activeField'), -10, 200).then(function() {
$scope.noscroll = false;
NOSCROLL = false;
setTimeout(function() {
if (document.querySelectorAll('.activeField .focusOn').length) {
//Handle default case
@ -218,54 +195,109 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
});
});
});
} else {
setTimeout(function() {
if (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 if (document.querySelectorAll('.activeField input')[0]){
document.querySelectorAll('.activeField input')[0].focus();
}
});
}
//Only send analytics data if form has not been submitted
if(!$scope.myform.submitted){
SendVisitorData.send($scope.myform, getActiveField(), TimeCounter.getTimeElapsed());
}
}
};
$rootScope.nextField = $scope.nextField = function(){
var currField = $scope.myform.visible_form_fields[$scope.selected.index];
if($scope.selected && $scope.selected.index > -1){
//Jump to logicJump's destination if it is true
if(currField.logicJump && evaluateLogicJump(currField)){
$rootScope.setActiveField(currField.logicJump.jumpTo, null, true);
} else {
var selected_index, selected_id;
if($scope.selected.index < $scope.myform.visible_form_fields.length-1){
selected_index = $scope.selected.index+1;
selected_id = $scope.myform.visible_form_fields[selected_index]._id;
$rootScope.setActiveField(selected_id, selected_index, true);
} else if($scope.selected.index === $scope.myform.visible_form_fields.length-1) {
selected_index = $scope.selected.index+1;
selected_id = 'submit_field';
$rootScope.setActiveField(selected_id, selected_index, true);
}
$scope.$watch('selected.index', function(oldValue, newValue){
if(oldValue !== newValue && newValue < $scope.myform.form_fields.length){
//Only send analytics data if form has not been submitted
if(!$scope.myform.submitted){
console.log('SendVisitorData.send()');
SendVisitorData.send($scope.myform, newValue, TimeCounter.getTimeElapsed());
}
}
}
});
//Fire event when window is scrolled
$window.onscroll = function(){
if(!NOSCROLL){
var scrollTop = $(window).scrollTop();
var elemBox = document.getElementsByClassName('activeField')[0].getBoundingClientRect();
var fieldTop = elemBox.top;
var fieldBottom = elemBox.bottom;
var field_id, field_index;
var elemHeight = $('.activeField').height();
var submitSectionHeight = $('.form-actions').height();
var maxScrollTop = $(document).height() - $(window).height();
var fieldWrapperHeight = $('form_fields').height();
var selector = 'form > .field-directive:nth-of-type(' + String($scope.myform.visible_form_fields.length - 1)+ ')'
var fieldDirectiveHeight = $(selector).height()
var scrollPosition = maxScrollTop - submitSectionHeight - fieldDirectiveHeight*1.2;
var fractionToJump = 0.9;
//Focus on field above submit form button
if($scope.selected.index === $scope.myform.visible_form_fields.length){
if(scrollTop < scrollPosition){
field_index = $scope.selected.index-1;
$scope.setActiveField(null, field_index, false);
}
}
//Focus on submit form button
else if($scope.selected.index === $scope.myform.visible_form_fields.length-1 && scrollTop > scrollPosition){
field_index = $scope.selected.index+1;
$scope.setActiveField(FORM_ACTION_ID, field_index, false);
}
//If we scrolled bellow the current field, move to next field
else if(fieldBottom < elemHeight * fractionToJump && $scope.selected.index < $scope.myform.visible_form_fields.length-1 ){
field_index = $scope.selected.index+1;
$scope.setActiveField(null, field_index, false);
}
//If we scrolled above the current field, move to prev field
else if ( $scope.selected.index !== 0 && fieldTop > elemHeight * fractionToJump) {
field_index = $scope.selected.index-1;
$scope.setActiveField(null, field_index, false);
}
}
$scope.$apply();
};
$rootScope.nextField = $scope.nextField = function(){
if($scope.selected && $scope.selected.index > -1){
if($scope.selected._id !== FORM_ACTION_ID){
var currField = $scope.myform.visible_form_fields[$scope.selected.index];
//Jump to logicJump's destination if it is true
if(currField.logicJump && currField.logicJump.jumpTo && evaluateLogicJump(currField)){
$scope.setActiveField(currField.logicJump.jumpTo, null, true);
} else if($scope.selected.index < $scope.myform.visible_form_fields.length-1){
$scope.setActiveField(null, $scope.selected.index+1, true);
} else {
$scope.setActiveField(FORM_ACTION_ID, null, true);
}
} else {
//If we are at the submit actions page, go to the first field
$rootScope.setActiveField(null, 0, true);
}
} else {
//If selected is not defined go to the first field
$rootScope.setActiveField(null, 0, true);
}
};
$rootScope.prevField = $scope.prevField = function(){
console.log('prevField');
console.log($scope.selected);
var selected_index = $scope.selected.index - 1;
if($scope.selected.index > 0){
var selected_index = $scope.selected.index - 1;
var selected_id = $scope.myform.visible_form_fields[selected_index]._id;
$scope.setActiveField(selected_id, selected_index, true);
$scope.setActiveField(null, selected_index, true);
}
};
$rootScope.goToInvalid = $scope.goToInvalid = function() {
var field_id = $('.row.field-directive .ng-invalid.focusOn, .row.field-directive .ng-untouched.focusOn:not(.ng-valid)').first().parents('.row.field-directive').first().attr('data-id');
$scope.setActiveField(field_id, null, true);
};
/*
** Form Display Functions
*/
@ -276,10 +308,6 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
}
};
$rootScope.goToInvalid = $scope.goToInvalid = function() {
document.querySelectorAll('.ng-invalid.focusOn')[0].focus();
};
var getDeviceData = function(){
var md = new MobileDetect(window.navigator.userAgent);
var deviceType = 'other';
@ -320,6 +348,10 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
};
$rootScope.submitForm = $scope.submitForm = function() {
if($scope.forms.myForm.$invalid){
$scope.goToInvalid();
return;
}
var _timeElapsed = TimeCounter.stopClock();
$scope.loading = true;

View file

@ -16,6 +16,7 @@
<div class="col-xs-12 field-input">
<div class="control-group input-append">
<input class="focusOn"
ng-focus="setActiveField(field._id, null, false)"
ng-style="{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}"
ng-class="{ 'no-border': !!field.fieldValue }"
ui-date="dateOptions"

View file

@ -15,6 +15,7 @@
</div>
<div class="col-xs-12 field-input">
<ui-select ng-model="field.fieldValue"
ng-focus="setActiveField(field._id, null, false)"
theme="selectize"
search-enabled="true"
search-by="option_value"

View file

@ -26,6 +26,7 @@
{{$index+1}}
</div>
<input ng-style="{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}"
ng-focus="setActiveField(field._id, null, false)"
type="radio" class="focusOn"
value="{{option.option_value}}"
ng-model="field.fieldValue"

View file

@ -12,7 +12,7 @@
<p class="col-xs-12" ng-if="field.description.length">{{field.description}} </p>
<br>
<div class="col-xs-offset-1 col-xs-11">
<button class="btn focusOn">
<button class="btn focusOn"
ng-style="{'font-size': '1.3em', 'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}"
ng-click="nextField()">
{{ 'CONTINUE' | translate }}

View file

@ -16,6 +16,7 @@
<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-focus="setActiveField(field._id, null, false)"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
ng-class="{ 'no-border': !!field.fieldValue }"

View file

@ -26,6 +26,7 @@
placeholder="{{placeholder}}"
ng-class="{ 'no-border': !!field.fieldValue }"
class="focusOn text-field-input"
ng-focus="setActiveField(field._id, null, false)"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
value="field.fieldValue"
@ -45,7 +46,7 @@
</div>
</div>
<div>
<div class="btn btn-lg btn-default"
<div class="btn btn-lg btn-default" ng-disabled="!field.fieldValue || field.$invalid"
style="padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)">
<button ng-disabled="!field.fieldValue || field.$invalid"
ng-style="{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}"

View file

@ -28,6 +28,7 @@
<input type="radio" value="true"
class="focusOn"
style="opacity: 0; margin-left: 0px;"
ng-focus="setActiveField(field._id, null, false)"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
ng-required="field.required"
@ -46,6 +47,7 @@
<input type="radio" value="false"
style="opacity:0; margin-left:0px;"
ng-focus="setActiveField(field._id, null, false)"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
ng-required="field.required"

View file

@ -1,16 +1,13 @@
<section class="overlay submitform" ng-if="!ispreview && (loading || (!myform.submitted && !myform.startPage.showStart))"></section>
<section class="overlay previewform submitform" ng-if="ispreview && (loading || (!myform.submitted && !myform.startPage.showStart))"></section>
<!-- Start Page View -->
<div ng-show="!myform.submitted && myform.startPage.showStart" class="form-submitted" style="padding-top: 35vh;">
<div class="row">
<div class="col-xs-12 text-center" style="overflow-wrap: break-word;">
<h1 style="font-weight: 400; nont-size: 25px;" ng-style="{'color': form.design.colors.questionColor}">
<h1 style="font-weight: 400; nont-size: 25px;" ng-style="{'color': myform.design.colors.questionColor}">
{{myform.startPage.introTitle}}
</h1>
</div>
<div class="col-xs-10 col-xs-offset-1 text-center" style="overflow-wrap: break-word;">
<p style="font-weight: 100; font-size: 16px;" ng-style="{'color': form.design.colors.questionColor}">
<p style="font-weight: 100; font-size: 16px;" ng-style="{'color': myform.design.colors.questionColor}">
{{myform.startPage.introParagraph}}
</p>
</div>
@ -37,42 +34,46 @@
</div>
<!-- Form Fields View -->
<div class="form-fields" ng-show="!myform.submitted && !myform.startPage.showStart" ng-style="{ 'border-color': myform.design.colors.buttonTextColor }">
<div class="form-fields"
ng-show="!myform.submitted && !myform.startPage.showStart"
ng-style="{ 'border-color': myform.design.colors.buttonTextColor }">
<div class="row">
<form name="forms.myForm" novalidate class="submission-form col-sm-12 col-md-offset-1 col-md-10">
<div class="row form-field-wrapper">
<form name="forms.myForm" novalidate class="submission-form">
<div ng-repeat="field in myform.form_fields" ng-if="!field.deletePreserved" data-index="{{$index}}" 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>
<div class="row form-actions" id="submit_field" ng-class="{activeField: selected._id == 'submit_field' }"
ng-style="{ 'background-color':myform.design.colors.buttonColor}" style="border-top: 1px solid #ddd; margin-top: 30vh; height: 100vh; margin-left: 1%; margin-right: 1%;"
on-tab-and-shift-key="prevField()"
on-tab-key="nextField()"
on-enter-key="submitForm()">
<div class="col-xs-12 text-left" style="background-color:#990000; color:white;" ng-if="forms.myForm.$invalid">
{{ 'COMPLETING_NEEDED' | translate:translateAdvancementData }}
</div>
<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 || forms.myForm.$invalid" ng-click="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;">
{{ 'SUBMIT' | translate }}
</button>
<button ng-if="forms.myForm.$invalid" 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>
<div class="col-sm-2 hidden-xs" style="font-size: 75%; margin-top:3.25em">
<small>
{{ 'ENTER' | translate }}
</small>
</div>
</div>
</form>
</div>
<div class="row form-actions" id="submit_field" ng-class="{activeField: selected._id == 'submit_field' }" ng-style="{ 'background-color':myform.design.colors.buttonColor}" style="border-top: 1px solid #ddd; margin-right: -13%; margin-left: -13%; margin-top: 30vh; height: 100vh">
<div class="col-xs-12 text-left" style="background-color:#990000; color:white;" ng-if="forms.myForm.$invalid">
{{ 'COMPLETING_NEEDED' | translate:translateAdvancementData }}
</div>
<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 || 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;">
{{ 'SUBMIT' | translate }}
</button>
<button ng-if="forms.myForm.$invalid" 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>
<div class="col-sm-2 hidden-xs" style="font-size: 75%; margin-top:3.25em">
<small>
{{ 'ENTER' | translate }}
</small>
</div>
</div>
<section ng-if="!myform.hideFooter" class="navbar navbar-fixed-bottom" ng-style="{ 'background-color':myform.design.colors.buttonColor, 'padding-top': '15px', 'border-top': '2px '+ myform.design.colors.buttonTextColor +' solid', 'color':myform.design.colors.buttonTextColor}">
<div class="container-fluid">
<div class="row">

View file

@ -0,0 +1,12 @@
<script>
$(".loader").fadeOut("slow");
</script>
<section class="public-form auth sigin-view valign-wrapper">
<div class="row valign">
<h3 class="col-md-12 text-center">404 - Form Does not Exist </h3>
<div class="col-md-4 col-md-offset-4">
<div class="col-md-12 text-center" style="padding-bottom: 50px;">
The form you are trying to access does not exist. Sorry about that!
</div>
</div>
</section>

View file

@ -1,4 +1,7 @@
<section class="auth sigin-view valign-wrapper">
<script>
$(".loader").fadeOut("slow");
</script>
<section class="public-form auth sigin-view valign-wrapper">
<div class="row valign">
<h3 class="col-md-12 text-center">Not Authorized to Access Form</h3>
<div class="col-md-4 col-md-offset-4">

View file

@ -1,4 +1,4 @@
<section class="public-form" ng-style="{ 'background-color': myform.design.colors.backgroundColor }">
<section class="public-form">
<submit-form-directive myform="myform"></submit-form-directive>
</section>

View file

@ -27,7 +27,11 @@ angular.module('view-form').config(['$stateProvider',
}).
state('unauthorizedFormAccess', {
url: '/forms/unauthorized',
templateUrl: '/static/form_modules/forms/base/views/form-unauthorized.client.view.html',
templateUrl: '/static/form_modules/forms/base/views/form-unauthorized.client.view.html'
})
.state('formNotFound', {
url: '*path',
templateUrl: '/static/form_modules/forms/base/views/form-not-found.client.view.html'
});
}
]);

View file

@ -28,18 +28,6 @@
deviceType = 'desktop';
}
$.ajaxSetup( { 'async': false } );
var geoData = $.getJSON('https://freegeoip.net/json/').responseJSON;
$.ajaxSetup( { 'async': true } );
if(!geoData){
geoData = {
ip: '',
city: '',
country_name: ''
};
}
// Create a new message object
var visitorData = {
referrer: document.referrer,
@ -49,11 +37,8 @@
timeElapsed: timeElapsed,
language: lang,
deviceType: deviceType,
ipAddr: geoData.ip,
geoLocation: {
city: geoData.city,
country: geoData.country_name
}
ipAddr: null,
geoLocation: null
};
Socket.emit('form-visitor-data', visitorData);

View file

@ -1,3 +1,18 @@
/*Modal overlay (for lightbox effect)*/
.overlay {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: rgb(0,0,0);
background-color: rgba(0,0,0,0.5);
z-index: 10;
}
.overlay.submitform {
background-color: rgb(256,256,256);
background-color: rgba(256,256,256,0.8);
}
.form-item .title-row > .list-group-item-heading {
color: #34628a;

View file

@ -16,6 +16,7 @@ angular.module('forms').controller('AdminFormController', ['$rootScope', '$windo
$scope.animationsEnabled = true;
$scope.myform = myForm;
$rootScope.saveInProgress = false;
$scope.oldForm = _.cloneDeep($scope.myform);
CurrentForm.setForm($scope.myform);
@ -42,13 +43,23 @@ angular.module('forms').controller('AdminFormController', ['$rootScope', '$windo
}
};
$scope.tabData = [
$scope.tabData = [
{
heading: $filter('translate')('CONFIGURE_TAB'),
templateName: 'configure'
}
];
$scope.designTabActive = false
$scope.deactivateDesignTab = function(){
$scope.designTabActive = false
}
$scope.activateDesignTab = function(){
$scope.designTabActive = true
}
$scope.setForm = function(form){
$scope.myform = form;
};
@ -102,16 +113,20 @@ angular.module('forms').controller('AdminFormController', ['$rootScope', '$windo
}
};
// Update existing Form
$scope.update = $rootScope.update = function(updateImmediately, data, isDiffed, refreshAfterUpdate, cb){
refreshFrame();
$scope.updateDesign = function(updateImmediately, data, shouldDiff, refreshAfterUpdate){
$scope.update(updateImmediately, data, shouldDiff, refreshAfterUpdate, function(){
refreshFrame();
});
}
// Update existing Form
$scope.update = $rootScope.update = function(updateImmediately, data, shouldDiff, refreshAfterUpdate, cb){
var continueUpdate = true;
if(!updateImmediately){
continueUpdate = !$rootScope.saveInProgress;
}
//Update form **if we are not currently updating** or if **shouldUpdateNow flag is set**
//Update form **if we are not in the middle of an update** or if **shouldUpdateNow flag is set**
if(continueUpdate) {
var err = null;
@ -119,11 +134,24 @@ angular.module('forms').controller('AdminFormController', ['$rootScope', '$windo
$rootScope.saveInProgress = true;
}
if (isDiffed) {
if (shouldDiff) {
//Do this so we can create duplicate fields
var checkForValidId = new RegExp('^[0-9a-fA-F]{24}$');
for(var i=0; i < $scope.myform.form_fields.length; i++){
var field = $scope.myform.form_fields[i];
if(!checkForValidId.exec(field._id+'')){
delete $scope.myform.form_fields[i]._id;
delete $scope.myform.form_fields[i].id;
}
}
var data = DeepDiff.diff($scope.oldForm, $scope.myform);
$scope.updatePromise = $http.put('/forms/' + $scope.myform._id, {changes: data})
.then(function (response) {
if (refreshAfterUpdate) {
$rootScope.myform = $scope.myform = response.data;
$scope.oldForm = _.cloneDeep($scope.myform);
}
}).catch(function (response) {
err = response.data;
@ -146,6 +174,22 @@ angular.module('forms').controller('AdminFormController', ['$rootScope', '$windo
delete dataToSend.submissions;
}
if(dataToSend.visible_form_fields){
delete dataToSend.visible_form_fields;
}
if(dataToSend.analytics){
delete dataToSend.analytics.visitors;
delete dataToSend.analytics.fields;
delete dataToSend.analytics.submissions;
delete dataToSend.analytics.views;
delete dataToSend.analytics.conversionRate;
}
delete dataToSend.created;
delete dataToSend.lastModified;
delete dataToSend.__v;
$scope.updatePromise = $http.put('/forms/' + $scope.myform._id, {form: dataToSend})
.then(function (response) {
if (refreshAfterUpdate) {

View file

@ -1,12 +1,13 @@
'use strict';
// Forms controller
angular.module('forms').controller('ListFormsController', ['$rootScope', '$scope', '$stateParams', '$state', 'GetForms', 'CurrentForm', '$http', '$uibModal',
function($rootScope, $scope, $stateParams, $state, GetForms, CurrentForm, $http, $uibModal) {
angular.module('forms').controller('ListFormsController', ['$rootScope', '$scope', '$stateParams', '$state', 'GetForms', 'CurrentForm', '$http', '$uibModal', 'myForms',
function($rootScope, $scope, $stateParams, $state, GetForms, CurrentForm, $http, $uibModal, myForms) {
$scope = $rootScope;
$scope.forms = {};
$scope.showCreateModal = false;
$scope.myforms = myForms
$rootScope.languageRegExp = {
regExp: /[@!#$%^&*()\-+={}\[\]|\\/'";:`.,~№?<>]+/i,
@ -49,13 +50,6 @@ angular.module('forms').controller('ListFormsController', ['$rootScope', '$scope
}
};
// Return all user's Forms
$scope.findAll = function() {
GetForms.query(function(_forms){
$scope.myforms = _forms;
});
};
//Modal functions
$scope.openCreateModal = function(){
if(!$scope.showCreateModal){

View file

@ -1,3 +1,4 @@
'use strict';
angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormFields', '$uibModal',
@ -23,7 +24,7 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
forceHelperSize: true,
forcePlaceholderSize: true,
update: function(e, ui) {
$scope.update(false, $scope.myform, false, false, function(err){
$scope.update(false, $scope.myform, true, false, function(err){
});
},
};
@ -31,7 +32,7 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
/*
** EditModal Functions
*/
$scope.openEditModal = function(curr_field){
$scope.openEditModal = function(curr_field, isEdit, field_index){
$scope.editFieldModal = $uibModal.open({
animation: true,
templateUrl: 'editFieldModal.html',
@ -40,9 +41,11 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
$scope.field = curr_field;
$scope.showLogicJump = false;
$scope.isEdit = isEdit;
// decides whether field options block will be shown (true for dropdown and radio fields)
$scope.showAddOptions = function (field){
if(field.fieldType === 'dropdown' || field.fieldType === 'checkbox' || field.fieldType === 'radio'){
if($scope.field.fieldType === 'dropdown' || $scope.field.fieldType === 'checkbox' || $scope.field.fieldType === 'radio'){
return true;
} else {
return false;
@ -66,13 +69,13 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
];
// add new option to the field
$scope.addOption = function(currField){
if(currField.fieldType === 'checkbox' || currField.fieldType === 'dropdown' || currField.fieldType === 'radio'){
if(!currField.fieldOptions){
currField.fieldOptions = [];
$scope.addOption = function(){
if($scope.field.fieldType === 'checkbox' || $scope.field.fieldType === 'dropdown' || $scope.field.fieldType === 'radio'){
if(!$scope.field.fieldOptions){
$scope.field.fieldOptions = [];
}
var lastOptionID = currField.fieldOptions.length+1;
var lastOptionID = $scope.field.fieldOptions.length+1;
// new option's id
@ -83,17 +86,17 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
};
// put new option into fieldOptions array
currField.fieldOptions.push(newOption);
$scope.field.fieldOptions.push(newOption);
}
};
// delete particular option
$scope.deleteOption = function (currField, option){
if(currField.fieldType === 'checkbox' || currField.fieldType === 'dropdown' || currField.fieldType === 'radio'){
for(var i = 0; i < currField.fieldOptions.length; i++){
if(currField.fieldOptions[i].option_id === option.option_id){
$scope.deleteOption = function (option){
if($scope.field.fieldType === 'checkbox' || $scope.field.fieldType === 'dropdown' || $scope.field.fieldType === 'radio'){
for(var i = 0; i < $scope.field.fieldOptions.length; i++){
if($scope.field.fieldOptions[i].option_id === option.option_id){
currField.fieldOptions.splice(i, 1);
$scope.field.fieldOptions.splice(i, 1);
break;
}
}
@ -118,8 +121,8 @@ 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'){
$scope.showRatingOptions = function (){
if($scope.field.fieldType === 'rating'){
return true;
} else {
return false;
@ -127,11 +130,16 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
};
$scope.saveField = function(){
if($scope.isEdit){
$scope.myform.form_fields[field_index] = $scope.field;
} else {
$scope.myform.form_fields.push(curr_field);
}
$scope.myform.form_fields.push(curr_field);
$scope.$parent.update(false, $scope.$parent.myform, false, true, function(){
$scope.$parent.update(false, $scope.$parent.myform, true, true, function(){
$uibModalInstance.close();
});
};
$scope.cancel = function(){
$uibModalInstance.close();
@ -184,7 +192,7 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
};
$scope.saveStartPage = function(){
$scope.$parent.update(false, $scope.$parent.myform, false, true, function(){
$scope.$parent.update(false, $scope.$parent.myform, true, true, function(){
$uibModalInstance.close();
});
};
@ -196,7 +204,7 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
};
/*
** EditStartPageModal Functions
** EditEndPageModal Functions
*/
$scope.openEditEndPageModal = function(){
$scope.editEndPageModal = $uibModal.open({
@ -239,7 +247,7 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
};
$scope.saveEndPage = function(){
$scope.$parent.update(false, $scope.$parent.myform, false, true, function(){
$scope.$parent.update(false, $scope.$parent.myform, true, true, function(){
$uibModalInstance.close();
});
};
@ -267,7 +275,7 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
** Field CRUD Methods
*/
// Add a new field
$scope.addNewField = function(modifyForm, fieldType){
$scope.addNewField = function(fieldType){
// increment lastAddedID counter
$scope.addField.lastAddedID++;
var fieldTitle = fieldType;
@ -306,12 +314,7 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
});
}
if(modifyForm){
//Add newField to form_fields array
$scope.myform.form_fields.push(newField);
}
$scope.openEditModal(newField);
$scope.openEditModal(newField, false, $scope.myform.form_fields.length);
};
// decides whether field options block will be shown (true for dropdown and radio fields)

View file

@ -16,27 +16,10 @@ angular.module('forms').directive('editSubmissionsFormDirective', ['$rootScope',
rows: []
};
var submissions = $scope.myform.submissions || [];
//Iterate through form's submissions
for(var i = 0; i < submissions.length; i++){
for(var x = 0; x < submissions[i].form_fields.length; x++){
if(submissions[i].form_fields[x].fieldType === 'dropdown'){
submissions[i].form_fields[x].fieldValue = submissions[i].form_fields[x].fieldValue.option_value;
}
//var oldValue = submissions[i].form_fields[x].fieldValue || '';
//submissions[i].form_fields[x] = _.merge(defaultFormFields, submissions[i].form_fields);
//submissions[i].form_fields[x].fieldValue = oldValue;
}
submissions[i].selected = false;
}
$scope.table.rows = submissions;
var initController = function(){
$http({
method: 'GET',
url: '/someUrl'
url: '/forms'+$scope.myform._id+'/submissions'
}).then(function successCallback(response) {
var defaultFormFields = _.cloneDeep($scope.myform.form_fields);
@ -59,6 +42,8 @@ angular.module('forms').directive('editSubmissionsFormDirective', ['$rootScope',
});
};
initController();
/*
** Analytics Functions

View file

@ -59,16 +59,16 @@
<div class="row">
<div class="col-xs-12">
<uib-tabset active="activePill" vertical="true" type="pills">
<uib-tab index="0" heading="{{ 'CREATE_TAB' | translate }}">
<uib-tab index="0" heading="{{ 'CREATE_TAB' | translate }}" select="deactivateDesignTab()">
<edit-form-directive myform="myform"></edit-form-directive>
</uib-tab>
<uib-tab ng-repeat="tab in tabData" index="{{$index+1}}" heading="{{tab.heading}}">
<uib-tab ng-repeat="tab in tabData" index="{{$index+1}}" heading="{{tab.heading}}" select="deactivateDesignTab()">
<div class='row' data-ng-include="'/static/modules/forms/admin/views/adminTabs/'+tab.templateName+'.html'"></div>
</uib-tab>
<uib-tab index="2" heading="{{ 'ANALYZE_TAB' | translate }}">
<uib-tab index="2" heading="{{ 'ANALYZE_TAB' | translate }}" select="deactivateDesignTab()">
<edit-submissions-form-directive myform="myform" user="myform.admin"></edit-submissions-form-directive>
</uib-tab>
<uib-tab ng-if="tabData" heading="{{ 'SHARE_TAB' | translate }}" index="{{tabData.length}}">
<uib-tab ng-if="tabData" heading="{{ 'SHARE_TAB' | translate }}" index="{{tabData.length}}" select="deactivateDesignTab()">
<div class="config-form">
<div class="row">
<div class="col-sm-12">
@ -114,7 +114,8 @@
</div>
</div>
</uib-tab>
<uib-tab ng-if="tabData && myform.form_fields.length" heading="{{ 'DESIGN_TAB' | translate }}" index="{{tabData.length}}+1">
<uib-tab class="design-tab" ng-if="tabData && myform.form_fields.length" heading="{{ 'DESIGN_TAB' | translate }}" index="{{tabData.length}}+1"
select="activateDesignTab()">
<div class="config-form design container">
<div class="row">
<div class="col-sm-4 col-xs-12">
@ -175,7 +176,7 @@
</div>
</div>
</div>
<div class="col-sm-8 hidden-xs">
<div class="col-sm-8 hidden-xs" ng-if="designTabActive">
<div class="public-form" ng-style="{ 'background-color': myform.design.colors.backgroundColor }">
<iframe id="iframe" ng-if="!!formURL" ng-src="{{formURL | trustSrc}}" style="border: none; box-shadow: 0px 0px 10px 0px grey; overflow: hidden; height: 400px; width: 90%; position: absolute;"></iframe>
</div>
@ -184,7 +185,7 @@
<div class="row">
<div class="col-sm-offset-4 col-sm-2">
<button class="btn btn-signup btn-rounded" type="button" ng-click="update(false, myform, false, false, null)"><i class="icon-arrow-left icon-white"></i>{{ 'SAVE_CHANGES' | translate }}</button>
<button class="btn btn-signup btn-rounded" type="button" ng-click="updateDesign(false, myform, false, false)"><i class="icon-arrow-left icon-white"></i>{{ 'SAVE_CHANGES' | translate }}</button>
</div>
<div class="col-sm-1">
<button class="btn btn-secondary btn-rounded" type="button" ng-click="resetForm()"><i class="icon-eye-open icon-white"></i>{{ 'CANCEL' | translate }}</button>

View file

@ -146,8 +146,6 @@
</div>
</div>
</script>
<!-- Edit StartPage Modal Dialog Template -->
@ -291,8 +289,6 @@
</div>
</div>
</script>
<!-- Edit Field Modal Dialog Template -->
@ -312,34 +308,34 @@
<div class="row"><br></div>
<div class="row description" ng-hide="showRatingOptions(field)">
<div class="row description" ng-hide="showRatingOptions()">
<div class="col-md-12 bold">{{ 'QUESTION_DESCRIPTION' | translate }}</div>
<div class="col-md-12">
<textarea type="text" class="form-control" 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-if="showAddOptions(field)">
<div class="row" ng-show="showAddOptions()"><br></div>
<div class="row options" ng-if="showAddOptions()">
<div class="col-md-4 col-xs-12">{{ 'OPTIONS' | translate }}</div>
<div class="col-md-8 col-xs-12">
<div ng-repeat="option in field.fieldOptions track by option.option_id" class="row">
<input type="text" name="{{option.option_value}}{{field._id}}" ng-model="option.option_value" class="col-xs-5">
<a class="btn btn-danger btn-mini right" type="button" ng-click="deleteOption(field, option)" class="col-xs-3">
<a class="btn btn-danger btn-mini right" type="button" ng-click="deleteOption(option)" class="col-xs-3">
<i class="fa fa-trash-o"></i>
</a>
</div>
<div class="row">
<button class="btn btn-primary btn-small col-md-offset-0 col-md-6 col-sm-4 col-sm-offset-4 col-xs-6 col-xs-offset-6" type="button" ng-click="addOption(field)">
<button class="btn btn-primary btn-small col-md-offset-0 col-md-6 col-sm-4 col-sm-offset-4 col-xs-6 col-xs-offset-6" type="button" ng-click="addOption()">
<i class="icon-plus icon-white"></i> {{ 'ADD_OPTION' | translate }}
</button>
</div>
</div>
</div>
<div class="row" ng-show="showRatingOptions(field)"><br></div>
<div class="row" ng-if="showRatingOptions(field)">
<div class="row" ng-show="showRatingOptions()"><br></div>
<div class="row" ng-if="showRatingOptions()">
<div class="col-md-9 col-sm-9">{{ 'NUM_OF_STEPS' | translate }}</div>
<div class="col-md-3 col-sm-3">
<input style="width:100%" type="number"
@ -383,7 +379,7 @@
<div class="col-md-4 col-xs-12 field-input">{{ 'LOGIC_JUMP' | translate }}</div>
<div class="col-md-8 col-xs-12 field-input">
<label class="switch-light switch-holo" onclick="">
<input type="checkbox" ng-model="showLogicJump">
<input type="checkbox" ng-model="field.logicJump.enabled">
<span>
<span> {{ 'OFF' | translate }}</span>
<span> {{ 'ON' | translate }}</span>
@ -392,7 +388,7 @@
</label>
</div>
</div>
<div class="row question" ng-if="!!showLogicJump">
<div class="row question" ng-if="field.logicJump.enabled"">
<div class="col-md-4 col-sm-12">
<b> {{ 'IF_THIS_FIELD' | translate }} </b>
@ -483,7 +479,7 @@
</div>
<div class="preview-field-panel col-md-6 hidden-sm hidden-xs">
<form class="public-form"ss>
<form class="public-form">
<field-directive field="field" validate="false" class="preview-field">
</field-directive>
</form>
@ -507,7 +503,7 @@
<div class="col-xs-12 col-sm-12 col-md-6" ng-repeat="type in addField.types" style="padding-top:7.5px;">
<div class="panel panel-default" style="background-color:#f5f5f5;">
<div class="panel-heading" ng-click="addNewField(false, type.name)" style="cursor: pointer; font-size:12px; padding-left: 10px; padding-right: 10px;">
<div class="panel-heading" ng-click="addNewField(type.name)" style="cursor: pointer; font-size:12px; padding-left: 10px; padding-right: 10px;">
<span>
<field-icon-directive type-name="{{type.name}}">
</field-icon-directive>
@ -545,7 +541,7 @@
<div class="col-xs-12 field-row" ng-repeat="field in myform.form_fields track by $id($index)" ng-if="!field.deletePreserved">
<div class="col-xs-10">
<div class="panel panel-default" ng-click="openEditModal(field)">
<div class="panel panel-default" ng-click="openEditModal(field, true, $index)">
<div class="panel-heading">
<div class="row">
<span class="col-xs-1" ng-switch="field.fieldType">

View file

@ -25,7 +25,7 @@
</div>
</script>
<section data-ng-controller="ListFormsController as ctrl" data-ng-init="findAll()" class="container">
<section class="container">
<br>
<div class="row">
<div ng-click="openCreateModal()" class="col-xs-6 col-xs-offset-3 col-sm-4 col-sm-offset-1 col-md-3 col-md-offset-1 form-item create-new">
@ -71,7 +71,8 @@
<div data-ng-repeat="form in myforms"
class="col-xs-6 col-xs-offset-3 col-sm-4 col-sm-offset-1 col-md-3 col-md-offset-1 form-item container"
ng-class="{'paused': !form.isLive}">
ng-class="{'paused': !form.isLive}"
ng-clkc="goToWithId('viewForm.create', form._id)">
<div class="row">
<span class="pull-right">

View file

@ -1,3 +1,23 @@
/*Modal overlay (for lightbox effect)*/
.overlay {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: rgb(0,0,0);
background-color: rgba(0,0,0,0.5);
z-index: 10;
}
.overlay.submitform {
background-color: rgb(256,256,256);
background-color: rgba(256,256,256,0.8);
}
form .btn {
border-color: grey;
}
.public-form.preview {
border: none;
box-shadow: 0px 0px 10px 0px grey;
@ -11,37 +31,38 @@
.public-form input, .public-form textarea {
background-color: #000000;
background-color: rgba(0,0,0,0);
border: 2px dashed #ddd!important;
border-width: 0px;
}
.public-form input:focus, .public-form textarea:focus {
border: 2px dashed #ddd!important;
form .btn {
border-color: grey;
}
.public-form input.ng-untouched, .public-form textarea.ng-untouched {
border-width: 0 0 2px 0;
border-color: rgba(246, 255, 181, 0.4);
outline: 0;
}
/*.public-form input.no-border.ng-invalid, .public-form textarea.no-border {
border-color: none;
}*/
.public-form input.ng-valid, .public-form textarea.ng-valid {
/*border-color: #20FF20!important;
border-style: solid!important;
border-width: 3px!important;*/
.public-form input:focus, .public-form textarea:focus {
border-width: 0 0 2px 0;
border-color: rgba(246, 255, 181, 0.4);
outline: 0;
}
.public-form input.ng-invalid.ng-dirty, .public-form textarea.ng-invalid.ng-dirty {
/*border-color: #FA787E!important;
border-style: solid!important;
border-width: 3px!important;*/
.public-form input.ng-dirty, .public-form textarea.ng-dirty {
border-width: 0;
}
.public-form input.empty, .public-form textarea.empty {
border-width: 0 0 2px 0;
border-color: rgba(246, 255, 181, 0.4);
}
section.content p.breakwords {
word-break: break-all;
}
.public-form .btn {
border: 1px solid #c6c6c6;
}
.public-form .btn[type='submit'] {
font-size: 1.5em;
padding: 0.35em 1.2em 0.35em 1.2em;
@ -142,7 +163,7 @@ form .row.field {
form .row.field > .field-title {
margin-top:0.5em;
font-size:1.2em;
padding-bottom: 1.8em;
padding-bottom: 0.5em;
width: inherit;
}
form .row.field > .field-input {
@ -243,15 +264,12 @@ form .row.field {
}
/* Styles for form list view (/forms) */
section.public-form {
padding: 0 10% 0 10%;
}
section.public-form .form-submitted {
height: 100vh;
section.public-form field-directive .btn.btn-lg.btn-default {
background: none;
}
section.public-form .btn {
border: 1px solid;
section.public-form field-directive .btn[disabled]{
display: none;
}
.form-item {
@ -330,42 +348,16 @@ section.public-form .btn {
top:0;
}
/*Modal overlay (for lightbox effect)*/
.overlay {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: #000;
background-color: rgba(0,0,0,0.5);
z-index: 10;
}
.overlay.submitform {
background-color: #fff;
background-color: rgba(256,256,256,0.8);
.field-directive {
opacity: 0.2;
padding: 2.5% 10% 2.5% 10%;
}
.field-directive {
z-index: 9;
padding: 10% 10% 10% 0;
border: 25px transparent solid;
position: relative;
.form-field-wrapper .form-actions.activeField, .field-directive.activeField {
opacity: 1;
}
.activeField {
z-index: 11;
position: relative;
background-color: transparent;
}
.activeField.field-directive {
display: inline-block;
border-radius: 7px;
width: 100%;
border: 25px transparent solid;
}
.activeField input {
background-color: transparent;
}
h3.forms-list-title {
color: #3FA2F7;
font-weight: 600;

View file

@ -8,7 +8,21 @@ angular.module('forms').config(['$stateProvider',
$stateProvider.
state('listForms', {
url: '/forms',
templateUrl: 'modules/forms/admin/views/list-forms.client.view.html'
templateUrl: 'modules/forms/admin/views/list-forms.client.view.html',
resolve: {
Forms: 'GetForms',
myForms: function (GetForms, $q) {
var deferred = $q.defer();
GetForms.query(function(forms){
deferred.resolve(forms);
});
return deferred.promise;
}
},
controller: 'ListFormsController',
controllerAs: 'ctrl'
}).state('submitForm', {
url: '/forms/:formId',
templateUrl: '/static/form_modules/forms/base/views/submit-form.client.view.html',

View file

@ -60,22 +60,13 @@
var newFakeModal = function(){
var result = {
opened: true,
result: {
then: function(confirmCallback, cancelCallback) {
//Store the callbacks for later when the user clicks on the OK or Cancel button of the dialog
this.confirmCallBack = confirmCallback;
this.cancelCallback = cancelCallback;
}
},
close: function( item ) {
//The user clicked OK on the modal dialog, call the stored confirm callback with the selected item
this.opened = false;
this.result.confirmCallBack( item );
},
dismiss: function( type ) {
//The user clicked cancel on the modal dialog, call the stored cancel callback
this.opened = false;
this.result.cancelCallback( type );
}
};
return result;
@ -173,7 +164,6 @@
beforeEach(inject(function($uibModal) {
var modal = newFakeModal();
spyOn($uibModal, 'open').and.returnValue(modal);
//spyOn($uibModal, 'close').and.callFake(modal.close());
}));
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).

View file

@ -115,7 +115,3 @@ input.form-control {
font-size: 18px;
padding: 20px 10px;
}
.btn {
border: none;
}

View file

@ -27,7 +27,7 @@
<br><br>
<p>
<strong>{{ 'BEFORE_YOU_CONTINUE' | translate }}</strong> <a href="mail:polydaic@gmail.com">polydaic@gmail.com</a></p>
<strong>{{ 'BEFORE_YOU_CONTINUE' | translate }}</strong> <a href="mail:team@tellform.com">team@tellform.com</a></p>
<div class="text-center form-group">
<button type="submit" class="btn btn-primary btn-rounded">
<a href="/#!/" style="color: white; text-decoration: none;">{{ 'CONTINUE' | translate }}</a>

View file

@ -13,10 +13,10 @@
<div class="text-center form-group">
<button type="submit" class="btn btn-signup btn-rounded btn-block">{{ 'PASSWORD_RESTORE_HEADER' | translate }}</button>
</div>
<div data-ng-show="error" class="text-center text-danger">
<strong>{{error}}</strong>
<div data-ng-show="error" class="text-center">
<strong>Error: {{error}}</strong>
</div>
<div data-ng-show="success" class="text-center text-success">
<div data-ng-show="success" class="text-center">
<strong>{{success}}</strong>
</div>
</fieldset>

View file

@ -5,17 +5,14 @@ var config = require('../config/config'),
exports.run = function(app, db, cb) {
var User = mongoose.model('User');
var email = config.ADMIN_EMAIL || 'admin@admin.com';
var username = config.ADMIN_USERNAME || 'root';
var password = config.ADMIN_PASSWORD || 'root';
var email = 'admin@admin.com' || config.admin.email;
var newUser = new User({
firstName: 'Admin',
lastName: 'Account',
email: email,
username: username,
password: password,
username: 'root' || config.admin.username,
password: 'root' || config.admin.password,
provider: 'local',
roles: ['admin', 'user']
});

View file

@ -3,11 +3,13 @@
* Module dependencies.
*/
//Load ENV vars from .env
if ((process.env.NODE_ENV || 'development') === 'development') {
require('dotenv').config();
require('dotenv').config({path: './.env'});
if(!process.env.NODE_ENV){
process.env.NODE_ENV = 'development';
}
require('events').EventEmitter.prototype._maxListeners = 0;
var config = require('./config/config'),

4
start.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/bash
node server.js
#pm2 start process.yml

View file

@ -0,0 +1,57 @@
Introduction
===========
TellForm is a powerful, open-source form/survey tool that allows you to get data from your users quickly and easily while collecting powerful analytics to improve your surveys. TellForm offers a field by field analytics, custom subdomains per-user, an extendable API, logic-based field jumping, per-device visitor analytics and 11 types of form fields.
By default, TellForm is setup to serve custom subdomains, but you can also configure it to work on a single domain, for those using it in more restricted environments.
Exporters—both the official ones that the Prometheus team maintains as well as the community-contributed ones—provide information about everything from infrastructure, databases, and web servers to messaging systems, APIs, and more.
In this tutorial, you'll install, configure, and secure TellForm to selfhost forms that will make it easy for you and your users to use.
Prerequisites
Before following this tutorial make sure you have:
One Ubuntu 16.x or 14.x Droplet, set up by following the [Initial Server Setup with Ubuntu 16.04 tutorial](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-16-04), including a sudo non-root user and a firewall.
Nginx installed by following the first two steps of the [How To Install Nginx on Ubuntu 16.04 tutorial](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-16-04).
Docker installed by following the steps of the [How To Install and Use Docker on Ubuntu 16.04 tutorial](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-16-04)
## Step 1 - Fetch Docker-Compose Files
```
$ curl -L github.com/tellform/tellform/stable/blob/docker-compose.yml
$ curl -L github.com/tellform/tellform/stable/blob/.env
```
### Prepare .env file:
Create `.env` file at project root folder. Fill in `MAILER_SERVICE_PROVIDER`, `MAILER_EMAIL_ID`, `MAILER_PASSWORD` and `MAILER_FROM`.
```
APP_NAME=TellForm
BASE_URL=localhost:3000
PORT=3000
DB_PORT_27017_TCP_ADDR=tellform-mongo
REDIS_DB_PORT_6379_TCP_ADDR=tellform-redis
MAILER_SERVICE_PROVIDER=<TO-FILL-IN>
MAILER_EMAIL_ID=<TO-FILL-IN>
MAILER_PASSWORD=<TO-FILL-IN>
MAILER_FROM=<TO-FILL-IN>
SIGNUP_DISABLED=false
SUBDOMAINS_DISABLED=true
DISABLE_CLUSTER_MODE=true
```
### Build docker image
```
$ docker-compose build
```
### Run docker containers with docker-compose
Create and start mongo & redis docker container:
```
$ docker-compose up -d
```
Your application should run at the BASE_URL you specified on port 443.