Merge pull request #230 from tellform/emailNotifications

Add Email Notifications
This commit is contained in:
David Baldwynn 2017-11-06 14:45:03 -08:00 committed by GitHub
commit 4fe1e8e848
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1874 additions and 1090 deletions

View File

@ -10,7 +10,13 @@ var mongoose = require('mongoose'),
config = require('../../config/config'),
diff = require('deep-diff'),
_ = require('lodash'),
helpers = require('./helpers.server.controller');
nodemailer = require('nodemailer'),
emailNotifications = require('../libs/send-email-notifications'),
constants = require('../libs/constants'),
helpers = require('./helpers.server.controller'),
async = require('async');
var smtpTransport = nodemailer.createTransport(config.mailer.options);
/**
* Delete a forms submissions
@ -70,7 +76,51 @@ exports.createSubmission = function(req, res) {
message: errorHandler.getErrorMessage(err)
});
}
res.status(200).send('Form submission successfully saved');
var form = req.body
var formFieldDict = emailNotifications.createFieldDict(form.form_fields);
async.waterfall([
function(callback) {
if (form.selfNotifications && form.selfNotifications.enabled && form.selfNotifications.fromField) {
form.selfNotifications.fromEmails = formFieldDict[form.selfNotifications.fromField];
emailNotifications.send(form.selfNotifications, formFieldDict, smtpTransport, constants.varFormat, function(err){
if(err){
return callback({
message: 'Failure sending submission self-notification email'
});
}
callback();
});
} else {
callback();
}
},
function(callback) {
if (form.respondentNotifications && form.respondentNotifications.enabled && form.respondentNotifications.toField) {
form.respondentNotifications.toEmails = formFieldDict[form.respondentNotifications.toField];
debugger;
emailNotifications.send(form.respondentNotifications, formFieldDict, smtpTransport, constants.varFormat, function(err){
if(err){
return callback({
message: 'Failure sending submission respondent-notification email'
});
}
callback();
});
} else {
callback();
}
}
], function (err) {
if(err){
return res.status(400).send(err);
}
res.status(200).send('Form submission successfully saved');
});
});
};
@ -95,14 +145,13 @@ exports.listSubmissions = function(req, res) {
* Create a new form
*/
exports.create = function(req, res) {
if(!req.body.form){
return res.status(400).send({
message: 'Invalid Input'
});
}
var form = new Form(req.body.form);
form.admin = req.user._id;
form.save(function(err, createdForm) {
@ -173,7 +222,7 @@ exports.update = function(req, res) {
form.analytics = {
visitors: [],
gaCode: ''
}
};
}
if (req.body.changes) {
@ -302,7 +351,7 @@ exports.formByIDFast = function(req, res, next, id) {
}
Form.findById(id)
.lean()
.select('title language form_fields startPage endPage hideFooter isLive design analytics.gaCode')
.select('title language form_fields startPage endPage hideFooter isLive design analytics.gaCode selfNotifications respondentNotifications')
.exec(function(err, form) {
if (err) {
return next(err);

View File

@ -1,3 +1,5 @@
'use strict';
module.exports = {
removeSensitiveModelData: function(type, object){
var privateFields = {
@ -5,7 +7,7 @@ module.exports = {
'private_form': ['__v'],
'public_user': ['passwordHash', 'password', 'provider', 'salt', 'lastModified', 'created', 'resetPasswordToken', 'resetPasswordExpires', 'token', 'apiKey', '__v'],
'private_user': ['passwordHash', 'password', 'provider', 'salt', 'resetPasswordToken', 'resetPasswordExpires', 'token', '__v']
}
};
function removeKeysFromDict(dict, keys){
for(var i=0; i<keys.length; i++){
@ -16,19 +18,18 @@ module.exports = {
}
}
switch(type){
case 'private_form':
removeKeysFromDict(object, privateFields['private_form']);
removeKeysFromDict(object, privateFields[type]);
if(object.admin){
removeKeysFromDict(object.admin, privateFields['private_user']);
removeKeysFromDict(object.admin, privateFields.private_user);
}
break;
case 'public_form':
removeKeysFromDict(object, privateFields['public_form']);
removeKeysFromDict(object, privateFields[type]);
if(object.admin){
removeKeysFromDict(object.admin, privateFields['public_user']);
removeKeysFromDict(object.admin, privateFields.public_user);
}
break;
@ -41,4 +42,4 @@ module.exports = {
return object;
}
}
};

View File

@ -61,7 +61,7 @@ config_nev();
exports.validateVerificationToken = function(req, res){
const fn = pug.compileFile(__dirname + "/../../views/welcome.email.view.pug");
const fn = pug.compileFile(__dirname + '/../../views/welcome.email.view.pug');
var renderedHtml = fn(res.locals);
var emailTemplate = {
@ -84,7 +84,7 @@ exports.validateVerificationToken = function(req, res){
};
exports.resendVerificationEmail = function(req, res, next){
const fn = pug.compileFile(__dirname + "/../../views/verification.email.view.pug");
const fn = pug.compileFile(__dirname + '/../../views/verification.email.view.pug');
var renderedHtml = fn(res.locals);
var emailTemplate = {
@ -118,7 +118,7 @@ exports.signup = function(req, res) {
var user = new User(req.body);
// Set language to visitor's language
user.language = req.cookies['userLang'];
user.language = req.cookies.userLang;
// Add missing user fields
user.provider = 'local';
@ -134,7 +134,7 @@ exports.signup = function(req, res) {
// new user created
if (newTempUser) {
const fn = pug.compileFile(__dirname + "/../../views/verification.email.view.pug");
const fn = pug.compileFile(__dirname + '/../../views/verification.email.view.pug');
var renderedHtml = fn(res.locals);
var URL = newTempUser[nev.options.URLFieldName];

View File

@ -81,8 +81,8 @@ exports.forgot = function(req, res) {
}
},
function(token, user, done) {
const fn = pug.compileFile(__dirname + "/../../views/templates/reset-password-email.server.view.pug");
res.locals['url'] = 'http://' + req.headers.host + '/auth/reset/' + token;
const fn = pug.compileFile(__dirname + '/../../views/templates/reset-password-email.server.view.pug');
res.locals.url = 'http://' + req.headers.host + '/auth/reset/' + token;
var renderedHtml = fn(res.locals);
done(null, renderedHtml, user);
@ -97,10 +97,10 @@ exports.forgot = function(req, res) {
};
var userEmail = user.email;
var user = userEmail.split('@')[0];
var emailUsername = userEmail.split('@')[0];
var domain = userEmail.split('@')[1];
var obfuscatedUser = user.substring(0, 1) + user.substring(1).replace(/./g, '*');
var obfuscatedUser = emailUsername.substring(0, 1) + emailUsername.substring(1).replace(/./g, '*');
var domainName = domain.split('.')[0];
var tld = domain.split('.')[1];
@ -191,8 +191,8 @@ exports.reset = function(req, res, next) {
});
},
function(user, done) {
const fn = pug.compileFile(__dirname + "/../../views/templates/reset-password-confirm-email.server.view.pug");
var renderedHtml = fn(res.locals);
const fn = pug.compileFile(__dirname + '/../../views/templates/reset-password-confirm-email.server.view.pug');
const renderedHtml = fn(res.locals);
done(null, renderedHtml, user);
},
// If valid email, send reset email using service

View File

@ -4,7 +4,6 @@ module.exports = {
fieldTypes: ['textfield',
'date',
'email',
'link',
'legal',
'url',
'textarea',
@ -74,6 +73,9 @@ module.exports = {
regex: {
url: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/,
hexCode: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
email: /^(([^<>()\[\]\\.,;:\s@']+(\.[^<>()\[\]\\.,;:\s@']+)*)|('.+'))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
}
email: /^(([^<>()\[\]\\.,;:\s@']+(\.[^<>()\[\]\\.,;:\s@']+)*)|('.+'))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
templateVariable: /<var(.*)id(.*)>(.|\n)*?<\/var>/g
},
varFormat: ['<var([^<>]+)id=["\']{1}field:', '["\']{1}>([^<>]+)*?<\/var>'],
};

View File

@ -0,0 +1,44 @@
'use strict';
module.exports = {
send: function(emailSettings, emailTemplateVars, smtpTransport, varFormat, cb){
var parsedTemplate = this.parseTemplate(emailSettings.htmlTemplate, emailTemplateVars, varFormat);
var parsedSubject = this.parseTemplate(emailSettings.subject, emailTemplateVars, varFormat);
var mailOptions = {
replyTo: emailSettings.fromEmails,
from: 'noreply@tellform.com',
cc: emailSettings.toEmails,
subject: parsedSubject,
html: parsedTemplate
};
console.log('HERE');
smtpTransport.sendMail(mailOptions, function(){
console.log('THERE');
cb();
});
},
parseTemplate: function(emailTemplate, emailAttrs, varFormat){
var resolvedTemplate = emailTemplate;
var that = this;
Object.keys(emailAttrs).forEach(function (key) {
resolvedTemplate = that.replaceTemplateVal(key, emailAttrs[key], resolvedTemplate, varFormat);
});
return resolvedTemplate;
},
replaceTemplateVal: function(key, val, template, varFormat){
return template.replace( new RegExp(varFormat[0] + key + varFormat[1], 'g'), val);
},
createFieldDict: function(form_fields){
var formFieldDict = {};
form_fields.forEach(function(field){
if(field.hasOwnProperty('globalId') && field.hasOwnProperty('fieldValue')){
formFieldDict[field.globalId] = field.fieldValue;
}
});
return formFieldDict;
}
};

View File

@ -2,38 +2,36 @@
// Plugin
module.exports = function timestamp (schema, options) {
options || (options = {})
options = options || (options === {});
// Options
var fields = {}
, createdPath = options.createdPath || 'created'
, modifiedPath = options.modifiedPath || 'modified'
, useVirtual = (options.useVirtual !== undefined)
? options.useVirtual
: true
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 }
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)
})
return new Date(this._id.generationTime * 1000);
});
} else {
if (!schema.paths[createdPath]) {
fields[createdPath] = {
type: Date
, default: Date.now
}
type: Date,
default: Date.now
};
}
}
schema.add(fields)
schema.add(fields);
// Update the modified timestamp on save
schema.pre('save', function (next) {
this[modifiedPath] = new Date
next()
})
}
this[modifiedPath] = new Date();
next();
});
};

View File

@ -9,7 +9,8 @@ var mongoose = require('mongoose'),
timeStampPlugin = require('../libs/timestamp.server.plugin'),
async = require('async'),
Random = require('random-js'),
mt = Random.engines.mt19937();
mt = Random.engines.mt19937(),
constants = require('../libs/constants');
mt.autoSeed();
@ -57,11 +58,12 @@ var VisitorDataSchema = new Schema({
type: Boolean
},
language: {
type: String
type: String,
enum: constants.languageTypes,
default: 'en',
},
ipAddr: {
type: String,
default: ''
type: String
},
deviceType: {
type: String,
@ -154,6 +156,47 @@ var FormSchema = new Schema({
buttons:[ButtonSchema]
},
selfNotifications: {
fromField: {
type: String
},
toEmails: {
type: String
},
subject: {
type: String
},
htmlTemplate: {
type: String
},
enabled: {
type: Boolean,
default: false
}
},
respondentNotifications: {
toField: {
type: String
},
fromEmails: {
type: String,
match: [/.+\@.+\..+/, 'Please fill a valid email address']
},
subject: {
type: String,
default: 'Tellform: Thank you for filling out this TellForm'
},
htmlTemplate: {
type: String,
default: 'Hello, <br><br> Weve received your submission. <br><br> Thank you & have a nice day!',
},
enabled: {
type: Boolean,
default: false
}
},
hideFooter: {
type: Boolean,
default: false
@ -207,10 +250,6 @@ FormSchema.virtual('analytics.views').get(function () {
}
});
FormSchema.virtual('analytics.submissions').get(function () {
return this.submissions.length;
});
FormSchema.virtual('analytics.conversionRate').get(function () {
if(this.analytics && this.analytics.visitors && this.analytics.visitors.length > 0){
return this.submissions.length/this.analytics.visitors.length*100;

View File

@ -9,7 +9,8 @@ var mongoose = require('mongoose'),
_ = require('lodash'),
Schema = mongoose.Schema,
LogicJumpSchema = require('./logic_jump.server.model'),
tokgen = require('../libs/tokenGenerator');
tokgen = require('../libs/tokenGenerator'),
constants = require('../libs/constants');
var FieldOptionSchema = new Schema({
option_id: {
@ -34,21 +35,7 @@ var RatingFieldSchema = new Schema({
},
shape: {
type: String,
enum: [
'Heart',
'Star',
'thumbs-up',
'thumbs-down',
'Circle',
'Square',
'Check Circle',
'Smile Outlined',
'Hourglass',
'bell',
'Paper Plane',
'Comment',
'Trash'
]
enum: constants.ratingShapeTypes
},
validShapes: {
type: [String]
@ -103,29 +90,7 @@ function BaseFieldSchema(){
},
fieldType: {
type: String,
enum: [
'textfield',
'date',
'email',
'link',
'legal',
'url',
'textarea',
'statement',
'welcome',
'thankyou',
'file',
'dropdown',
'scale',
'rating',
'radio',
'checkbox',
'hidden',
'yes_no',
'natural',
'stripe',
'number'
]
enum: constants.fieldTypes
},
fieldValue: Schema.Types.Mixed
});
@ -140,7 +105,7 @@ function BaseFieldSchema(){
this.validFieldTypes = mongoose.model('Field').schema.path('fieldType').enumValues;
if(this.fieldType === 'rating' && this.ratingOptions.validShapes.length === 0){
this.ratingOptions.validShapes = mongoose.model('RatingOptions').schema.path('shape').enumValues;
this.ratingOptions.validShapes = constants.ratingShapeTypes;
}
next();

View File

@ -55,18 +55,18 @@ FormSubmissionSchema.pre('save', function (next) {
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;
delete this.form_fields[i].validFieldTypes;
delete this.form_fields[i].disabled;
delete this.form_fields[i].required;
delete this.form_fields[i].isSubmission;
delete this.form_fields[i].title;
delete this.form_fields[i].fieldOptions;
delete this.form_fields[i].ratingOptions;
delete this.form_fields[i].logicJump;
delete this.form_fields[i].description;
delete this.form_fields[i].created;
delete this.form_fields[i].lastModified;
delete this.form_fields[i].deletePreserved;
}
next();
});

View File

@ -10,7 +10,8 @@ var mongoose = require('mongoose'),
timeStampPlugin = require('../libs/timestamp.server.plugin'),
path = require('path'),
querystring = require('querystring'),
nodemailer = require('nodemailer');
nodemailer = require('nodemailer'),
constants = require('../libs/constants');
var smtpTransport = nodemailer.createTransport(config.mailer.options);
@ -42,7 +43,7 @@ var UserSchema = new Schema({
trim: true,
lowercase: true,
unique: 'Account already exists with this email',
match: [/.+\@.+\..+/, 'Please fill a valid email address'],
match: [constants.regex.email, 'Please fill a valid email address'],
required: [true, 'Email is required']
},
username: {
@ -66,13 +67,13 @@ var UserSchema = new Schema({
roles: {
type: [{
type: String,
enum: ['user', 'admin', 'superuser']
enum: constants.userRoleTypes
}],
default: ['user']
},
language: {
type: String,
enum: ['en', 'fr', 'es', 'it', 'de'],
enum: constants.languageTypes,
default: 'en',
},
lastModified: {

View File

@ -31,7 +31,7 @@ module.exports = function(app) {
}
app.route('/forms/:formIdFast([a-zA-Z0-9]+)')
.post(forms.createSubmission)
.post(forms.createSubmission);
app.route('/forms')
.get(auth.isAuthenticatedOrApiKey, forms.list)

View File

@ -166,7 +166,7 @@ describe('Form Routes Unit tests', function() {
it(' > should not be able to create a Form if body is empty', function(done) {
loginSession.post('/forms')
.send({form: null})
.expect(400, {"message":"Invalid Input"})
.expect(400, {'message':'Invalid Input'})
.end(function(FormSaveErr, FormSaveRes) {
// Call the assertion callback
done(FormSaveErr);

View File

@ -81,8 +81,8 @@ describe('FormSubmission Model Unit Tests:', function() {
user = new User({
firstName: 'Full',
lastName: 'Name',
email: 'test1@test.com'+Date.now(),
username: 'test1'+Date.now(),
email: 'test1@test.com',
username: 'test1',
password: 'password',
provider: 'local'
});

View File

@ -21,15 +21,14 @@ var credentials, user;
* Form routes tests
*/
describe('Form Submission Routes Unit tests', function() {
var FormObj, _Submission, submissionSession, _SubmissionBody
var FormObj, _Submission, submissionSession, _SubmissionBody;
beforeEach(function(done) {
// Create user credentials
credentials = {
email: 'test@test.com',
username: 'test',
email: 'test423@test.com',
username: 'test534',
password: 'password'
};
@ -45,7 +44,10 @@ describe('Form Submission Routes Unit tests', function() {
// Save a user to the test db and create new Form
user.save(function(err) {
if(err) return done(err);
if(err) {
return done(err);
}
FormObj = new Form({
title: 'Form Title',
language: 'en',
@ -54,7 +56,22 @@ describe('Form Submission Routes Unit tests', function() {
new Field({'fieldType':'textfield', 'title':'First Name', 'fieldValue': ''}),
new Field({'fieldType':'checkbox', 'title':'nascar', 'fieldValue': ''}),
new Field({'fieldType':'checkbox', 'title':'hockey', 'fieldValue': ''})
]
],
selfNotifications: {
fromField: mongoose.Types.ObjectId(),
toEmails: 'john@smith.com',
subject: 'Hello there',
htmlTemplate: '<p> A form was submitted </p>',
enabled: true
},
respondentNotifications: {
toField: mongoose.Types.ObjectId(),
fromEmails: 'john@smith.com',
subject: 'Tellform: Thank you for filling out this TellForm',
htmlTemplate:'Hello, <br><br> Weve received your submission. <br><br> Thank you & have a nice day!',
enabled: true
}
});
FormObj.save(function(formSaveErr, form) {
@ -237,6 +254,4 @@ describe('Form Submission Routes Unit tests', function() {
});
});
});
});

View File

@ -0,0 +1,94 @@
'use strict';
/**
* Module dependencies.
*/
const emailNotifications = require('../../libs/send-email-notifications'),
constants = require('../../libs/constants'),
mockTransport = require("nodemailer").createTransport("Stub"),
config = require('../../../config/config');
/**
* Globals
*/
const validFormFields = [
{fieldType:'textfield', title:'First Name', fieldValue: 'John Smith', deletePreserved: false, globalId:'56340745f59a6fc9e22028e9'},
{fieldType:'link', title:'Your Website', fieldValue: 'https://johnsmith.me', deletePreserved: false, globalId:'5c9e22028e907634f45f59a6'},
{fieldType:'number', title:'Your Age', fieldValue: 45, deletePreserved: false, globalId:'56e90745f5934fc9e22028a6'}
];
const validFieldDict = {
'56340745f59a6fc9e22028e9': 'John Smith',
'5c9e22028e907634f45f59a6': 'https://johnsmith.me',
'56e90745f5934fc9e22028a6': 45
};
const invalidFormFields = [
{fieldType:'textfield', title:'First Name', fieldValue: 'John Smith', deletePreserved: false},
{fieldType:'link', title:'Your Website', deletePreserved: false, _id:'5c9e22028e907634f45f59a6'},
{fieldType:'number', title:'Your Age'}
];
const htmlTemplate = '<p><var class="tag" id="field:56340745f59a6fc9e22028e9">First Name</var> \
<br><var class="tag" id="field:5c9e22028e907634f45f59a6">Your Website</var> \
<br><var class="tag" id="field:56e90745f5934fc9e22028a6">Your Age</var></p>';
const renderedTemplate = '<p>John Smith \
<br>https://johnsmith.me \
<br>45</p>';
/**
* Unit tests
*/
describe('Send Email Notification Unit Tests', function() {
describe('Method createFieldDict', function() {
it('should be return a fieldDict from valid form fields', function() {
var actualFieldDict = emailNotifications.createFieldDict(validFormFields);
actualFieldDict.should.deepEqual(validFieldDict);
});
it('should return empty object if form fields are invalid or empty ', function() {
var actualFieldDict = emailNotifications.createFieldDict(invalidFormFields);
actualFieldDict.should.be.empty();
});
});
describe('Method parseTemplate', function(){
it('should properly render a template given a valid field dict', function() {
var actualRenderedTemplate = emailNotifications.parseTemplate(htmlTemplate, validFieldDict, constants.varFormat).replace((/ |\r\n|\n|\r|\t/gm),'');
actualRenderedTemplate.should.equal(renderedTemplate.replace((/ |\r\n|\n|\r|\t/gm),''));
});
});
describe('Method replaceTemplateVal', function() {
it('should properly replace a template var in a valid template', function() {
var expectedHtml = '<p>John Smith \
<br><var class="tag" id="field:5c9e22028e907634f45f59a6">Your Website</var> \
<br><var class="tag" id="field:56e90745f5934fc9e22028a6">Your Age</var></p>';
var actualRenderedTemplate = emailNotifications.replaceTemplateVal('56340745f59a6fc9e22028e9', validFieldDict['56340745f59a6fc9e22028e9'], htmlTemplate, constants.varFormat).replace((/ |\r\n|\n|\r|\t/gm),'');
actualRenderedTemplate.should.equal(expectedHtml.replace((/ |\r\n|\n|\r|\t/gm),''));
});
});
describe('Method send', function() {
this.timeout(10000);
const emailSettings = {
fromEmails: 'somewhere@somewhere.com',
toEmails: 'there@there.com',
subject: 'Hello <var class="tag" id="field:56340745f59a6fc9e22028e9">First Name</var>!',
htmlTemplate: htmlTemplate
};
const emailTemplateVars = validFieldDict;
const varFormat = constants.varFormat;
it('should properly replace a template var in a valid template', function(done) {
emailNotifications.send(emailSettings, emailTemplateVars, mockTransport, varFormat, function(err){
should.not.exist(err);
done();
});
});
});
});

View File

@ -1,70 +1,72 @@
'use strict';
// Dependencies
var util = require('util')
, assert = require('assert')
, mongoose = require('mongoose')
, timestamp = require('../../libs/timestamp.server.plugin')
, Schema = mongoose.Schema
, ObjectId = Schema.ObjectId
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()
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)
})
})
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()
})
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()
})
})
})
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()
var FooSchema = new Schema();
FooSchema.plugin(timestamp, {
createdPath: 'oh'
, modifiedPath: 'hai'
, useVirtual: false
})
var BarModel = mongoose.model('timeBar', FooSchema)
, bar = new BarModel()
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)
})
})
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()
})
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()
})
})
})
})
assert.strictEqual(err, null);
assert.strictEqual(util.isDate(doc.oh), true);
assert.strictEqual(util.isDate(doc.hai), true);
done();
});
});
});
});

View File

@ -48,7 +48,7 @@ describe('User CRUD tests', function() {
.send(_User)
.expect(200)
.end(function(err) {
callback(err)
callback(err);
});
},
function(callback) {
@ -136,7 +136,7 @@ describe('User CRUD tests', function() {
if(err){
callback(err);
}
callback(null, user.resetPasswordToken)
callback(null, user.resetPasswordToken);
});
},
function(resetPasswordToken, callback) {

View File

@ -78,13 +78,15 @@ html(lang='en', xmlns='http://www.w3.org/1999/xhtml')
//Socket.io Client Dependency
script(src='/static/lib/socket.io-client/dist/socket.io.min.js')
script(src='/static/lib/jquery-ui/jquery-ui.js', type='text/javascript')
//Minified Bower Dependencies
script(src='/static/lib/angular/angular.min.js')
script(src='/static/dist/vendor.min.js')
script(src='/static/lib/angular-ui-date/src/date.js', type='text/javascript')
script(src='/static/lib/jquery-ui/jquery-ui.js', type='text/javascript')
script(src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js', integrity='sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa', crossorigin='anonymous')
//Application JavaScript Files
each jsFile in formJSFiles
script(type='text/javascript', src=jsFile)

View File

@ -3,7 +3,9 @@ extends layout.server.view.pug
block content
section.content(ui-view='', ng-cloak='')
script(src='/static/lib/file-saver.js/FileSaver.js', type='text/javascript')
link(rel='stylesheet', href='/static/lib/jquery-ui/themes/flick/jquery-ui.min.css')
script(src='/static/lib/jquery/jquery.min.js')
//Embedding The User Object
script(type='text/javascript').

View File

@ -31,8 +31,8 @@ html(lang='en', xmlns='http://www.w3.org/1999/xhtml')
// Fav Icon
link(href='/static/modules/core/img/brand/favicon.ico', rel='shortcut icon', type='image/x-icon')
link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css')
link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css', integrity='sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u', crossorigin='anonymous')
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')
link(rel='stylesheet', href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,900')
//Bower CSS dependencies
@ -40,7 +40,6 @@ html(lang='en', xmlns='http://www.w3.org/1999/xhtml')
link(rel='stylesheet', href=bowerCssFile)
link(rel='stylesheet', href='/static/lib/angular-input-stars/angular-input-stars.css')
link(rel='stylesheet', href='/static/lib/jquery-ui/themes/flick/jquery-ui.css')
link(rel='stylesheet', href='/static/modules/core/css/github-fork-ribbon.css')
// end Bower CSS dependencies
//Application CSS Files

View File

@ -25,7 +25,6 @@
"angular-permission": "~1.1.1",
"file-saver.js": "~1.20150507.2",
"angular-bootstrap-colorpicker": "~3.0.19",
"angular-ui-router-tabs": "~1.7.0",
"angular-scroll": "^1.0.0",
"angular-sanitize": "1.4.14",
"v-button": "^1.1.1",
@ -33,7 +32,6 @@
"raven-js": "^3.0.4",
"tableExport.jquery.plugin": "^1.5.1",
"js-yaml": "^3.6.1",
"angular-ui-select": "https://github.com/tellform/ui-select.git#compiled",
"angular-translate": "~2.11.0",
"ng-translate": "*",
"deep-diff": "^0.3.4",
@ -42,17 +40,22 @@
"mobile-detect": "^1.3.3",
"socket.io-client": "^1.7.2",
"css-toggle-switch": "^4.0.2",
"angular-strap": "^2.3.12"
"angular-strap": "^2.3.12",
"textAngular": "^1.5.16",
"angular-ui-select": "^0.19.8",
"angular-bootstrap-switch": "^0.5.2",
"jquery": "^3.2.1"
},
"resolutions": {
"angular-bootstrap": "^0.14.0",
"angular": "1.4.14",
"angular-ui-select": "compiled",
"jspdf": "~1.0.178",
"angular-sanitize": "1.4.14",
"angular-ui-sortable": "^0.17.1",
"angular-ui-date": "~0.0.11",
"angular-input-stars-directive": "master"
"angular-input-stars-directive": "master",
"angular-ui-select": "^0.19.8",
"jquery": "^3.2.1"
},
"overrides": {
"BOWER-PACKAGE": {

View File

@ -39,8 +39,9 @@ var configureSocketIO = function (app, db) {
var supportedLanguages = ['en', 'de', 'fr', 'it', 'es'];
function containsAnySupportedLanguages(preferredLanguages){
for (var i = 0; i < preferredLanguages.length; i++) {
var currIndex = supportedLanguages.indexOf(preferredLanguages[i]);
var i, currIndex;
for (i = 0; i < preferredLanguages.length; i++) {
currIndex = supportedLanguages.indexOf(preferredLanguages[i]);
if (currIndex > -1) {
return supportedLanguages[currIndex];
}
@ -226,7 +227,6 @@ module.exports = function(db) {
// Setting the app router and static folder
app.use('/static', express.static(path.resolve('./public')));
app.use('/uploads', express.static(path.resolve('./uploads')));
// CookieParser should be above session
app.use(cookieParser());
@ -261,6 +261,7 @@ module.exports = function(db) {
//Visitor Language Detection
app.use(function(req, res, next) {
var acceptLanguage = req.headers['accept-language'];
var languages, supportedLanguage;
if(acceptLanguage){
@ -270,13 +271,12 @@ module.exports = function(db) {
if(!req.user && supportedLanguage !== null){
var currLanguage = res.cookie('userLang');
if(currLanguage && currLanguage !== supportedLanguage || !currLanguage){
res.clearCookie('userLang');
res.cookie('userLang', supportedLanguage, { maxAge: 90000, httpOnly: true });
} else if(req.user && (!req.cookies.hasOwnProperty('userLang') || req.cookies.userLang !== req.user.language) ){
res.cookie('userLang', req.user.language, { maxAge: 90000, httpOnly: true });
}
} else if(req.user && (!req.cookies.hasOwnProperty('userLang') || req.cookies['userLang'] !== req.user.language) ){
res.cookie('userLang', req.user.language, { maxAge: 90000, httpOnly: true });
}
next();
});

BIN
dump.rdb Normal file

Binary file not shown.

View File

@ -1,4 +1,4 @@
'use strict';
var bowerArray = ['public/lib/angular/angular.min.js',
'public/lib/angular-scroll/angular-scroll.min.js',

1072
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -41,7 +41,7 @@
"express": "~4.13.3",
"express-session": "~1.12.1",
"glob": "^7.0.3",
"grunt": "~0.4.1",
"grunt": "^0.4.5",
"grunt-concurrent": "~2.3.0",
"grunt-contrib-csslint": "~1.0.0",
"grunt-contrib-cssmin": "~1.0.1",

View File

@ -4,7 +4,7 @@
var ApplicationConfiguration = (function() {
// Init module configuration options
var applicationModuleName = 'TellForm';
var applicationModuleVendorDependencies = ['duScroll', 'ui.select', 'ngSanitize', 'vButton', 'ngResource', 'TellForm.templates', 'ui.router', 'ui.bootstrap', 'ui.utils', 'pascalprecht.translate', 'view-form'];
var applicationModuleVendorDependencies = ['duScroll', 'ui.select', 'ngSanitize', 'vButton', 'ngResource', 'TellForm.templates', 'ui.router', 'ui.bootstrap', 'ui.utils', 'pascalprecht.translate'];
// Add a new vertical module
var registerModule = function(moduleName, dependencies) {

View File

@ -42,7 +42,7 @@ angular.element(document).ready(function() {
var ApplicationConfiguration = (function() {
// Init module configuration options
var applicationModuleName = 'TellForm';
var applicationModuleVendorDependencies = ['duScroll', 'ui.select', 'ngSanitize', 'vButton', 'ngResource', 'TellForm.templates', 'ui.router', 'ui.bootstrap', 'ui.utils', 'pascalprecht.translate', 'view-form'];
var applicationModuleVendorDependencies = ['duScroll', 'ui.select', 'ngSanitize', 'vButton', 'ngResource', 'TellForm.templates', 'ui.router', 'ui.bootstrap', 'ui.utils', 'pascalprecht.translate'];
// Add a new vertical module
var registerModule = function(moduleName, dependencies) {
@ -890,8 +890,6 @@ angular.module('view-form').directive('submitFormDirective', ["$http", "TimeCoun
};
$rootScope.prevField = $scope.prevField = function(){
console.log('prevField');
console.log($scope.selected);
var selected_index = $scope.selected.index - 1;
if($scope.selected.index > 0){
$scope.setActiveField(null, selected_index, true);
@ -985,17 +983,6 @@ angular.module('view-form').directive('submitFormDirective', ["$http", "TimeCoun
if($scope.myform.form_fields[i].fieldType === 'dropdown' && !$scope.myform.form_fields[i].deletePreserved){
$scope.myform.form_fields[i].fieldValue = $scope.myform.form_fields[i].fieldValue.option_value;
}
//Get rid of unnessecary attributes for each form field
delete form.form_fields[i].submissionId;
delete form.form_fields[i].disabled;
delete form.form_fields[i].ratingOptions;
delete form.form_fields[i].fieldOptions;
delete form.form_fields[i].logicJump;
delete form.form_fields[i].description;
delete form.form_fields[i].validFieldTypes;
delete form.form_fields[i].fieldType;
}
setTimeout(function () {

4
public/dist/application.min.css vendored Normal file

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

@ -372,7 +372,6 @@ div.config-form .row.field {
background-color:#efefef;
}
/*Styles for add fields tab*/
.admin-form .add-field {
background-color: #ddd;
@ -389,10 +388,6 @@ div.config-form .row.field {
border-radius: 4px;
}
.admin-form .oscar-field-select {
margin: 10px 0 10px;
}
.view-form-btn.span {
padding-right:0.6em;
}

View File

@ -285,8 +285,6 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
};
$rootScope.prevField = $scope.prevField = function(){
console.log('prevField');
console.log($scope.selected);
var selected_index = $scope.selected.index - 1;
if($scope.selected.index > 0){
$scope.setActiveField(null, selected_index, true);
@ -380,17 +378,6 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
if($scope.myform.form_fields[i].fieldType === 'dropdown' && !$scope.myform.form_fields[i].deletePreserved){
$scope.myform.form_fields[i].fieldValue = $scope.myform.form_fields[i].fieldValue.option_value;
}
//Get rid of unnessecary attributes for each form field
delete form.form_fields[i].submissionId;
delete form.form_fields[i].disabled;
delete form.form_fields[i].ratingOptions;
delete form.form_fields[i].fieldOptions;
delete form.form_fields[i].logicJump;
delete form.form_fields[i].description;
delete form.form_fields[i].validFieldTypes;
delete form.form_fields[i].fieldType;
}
setTimeout(function () {

View File

@ -15,11 +15,10 @@
</div>
<div class="col-xs-12 field-input">
<ui-select ng-model="field.fieldValue"
class="dropdown"
ng-focus="setActiveField(field._id, null, false)"
theme="selectize"
search-enabled="true"
search-by="option_value"
set-search-to-answer="true"
ng-required="field.required"
on-tab-and-shift-key="prevField()"
on-tab-key="nextField()"

View File

@ -21,8 +21,13 @@ angular.module(ApplicationConfiguration.applicationModuleName).run(['$rootScope'
params: fromParams
}
<<<<<<< HEAD
var statesToIgnore = ['home', 'signin', 'resendVerifyEmail', 'verify', 'signup', 'signup-success', 'forgot', 'reset-invalid', 'reset', 'reset-success'];
=======
var statesToIgnore = ['', 'home', 'signin', 'resendVerifyEmail', 'verify', 'signup', 'signup-success', 'forgot', 'reset-invalid', 'reset', 'reset-success'];
>>>>>>> 2.20
//Redirect to listForms if user is authenticated
if(statesToIgnore.indexOf(toState.name) > 0){
if(Auth.isAuthenticated()){

View File

@ -6,16 +6,30 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
//Configure Form Tab View
ADVANCED_SETTINGS: 'Advanced Settings',
FORM_NAME: 'Form Name',
FORM_STATUS: 'Form Status',
FORM_NAME: 'Your tellform is called',
FORM_STATUS: 'Status',
PUBLIC: 'Public',
PRIVATE: 'Private',
GA_TRACKING_CODE: 'Google Analytics Tracking Code',
DISPLAY_FOOTER: 'Display Form Footer?',
DISPLAY_FOOTER: 'Form Footer',
SAVE_CHANGES: 'Save Changes',
CANCEL: 'Cancel',
DISPLAY_START_PAGE: 'Display Start Page?',
DISPLAY_END_PAGE: 'Display Custom End Page?',
DISPLAY_START_PAGE: 'Start Page',
DISPLAY_END_PAGE: 'Custom End Page',
GENERAL_TAB: 'General',
SELF_NOTIFICATIONS_TAB: 'Self notifications',
RESPONDENT_NOTIFICATIONS_TAB: 'Respondent notifications',
SEND_NOTIFICATION_TO: 'Send to',
NO_EMAIL_FIELD_WARNING: 'Error: You need an email field in your form to send the email to your form respondent',
REPLY_TO: 'Reply to',
EMAIL_SUBJECT: 'Subject',
EMAIL_MESSAGE: 'Message',
ENABLE_RESPONDENT_NOTIFICATIONS: 'Respondent Notifications are currently',
ENABLE_SELF_NOTIFICATIONS: 'Self Notifications are currently',
TOGGLE_ENABLED: 'Enabled',
TOGGLE_DISABLED: 'Disabled',
ADD_VARIABLE_BUTTON: 'Add variable',
//List Forms View
CREATE_A_NEW_FORM: 'Create a new form',
@ -47,7 +61,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
DELETE_FORM_MD: 'Delete Form',
DELETE: 'Delete',
FORM: 'Form',
VIEW: 'View',
VIEW_MY_TELLFORM: 'View my tellform',
LIVE: 'Live',
PREVIEW: 'Preview',
COPY: 'Copy',

View File

@ -5,16 +5,31 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
$translateProvider.translations('fr', {
// Configurer la vue de l'onglet Formulaire
ADVANCED_SETTINGS: 'Paramètres avancés',
FORM_NAME: "Nom du formulaire",
FORM_STATUS: 'Statut du formulaire',
FORM_NAME: "Votre tellform est appelé",
FORM_STATUS: 'Statut',
PUBLIC: 'Public',
PRIVATE: "Privé",
GA_TRACKING_CODE: "Code de suivi Google Analytics",
DISPLAY_FOOTER: "Afficher le pied de formulaire?",
DISPLAY_FOOTER: "Pied de formulaire",
SAVE_CHANGES: 'Enregistrer les modifications',
CANCEL: 'Annuler',
DISPLAY_START_PAGE: "Afficher la page de démarrage?",
DISPLAY_END_PAGE: "Afficher la page de fin personnalisée?",
DISPLAY_START_PAGE: "Page de démarrage",
DISPLAY_END_PAGE: "Page de fin personnalisée",
GENERAL_TAB: 'General',
SELF_NOTIFICATIONS_TAB: 'Self notifications',
RESPONDANT_NOTIFICATIONS_TAB: 'Respondent notifications',
SEND_NOTIFICATION_TO: 'Envoyer à',
NO_EMAIL_FIELD_WARNING: 'Erreur: Vous avez besoin d\'un champ e-mail dans votre formulaire pour envoyer l\'e-mail au répondant de votre formulaire',
REPLY_TO: "Répondre à",
EMAIL_SUBJECT: 'Sujet',
EMAIL_MESSAGE: "Message",
ENABLE_RESPONDENT_NOTIFICATIONS: 'Les notifications des répondants sont actuellement',
ENABLE_SELF_NOTIFICATIONS: 'Les notifications automatiques sont actuellement',
TOGGLE_ENABLED: 'Activé',
TOGGLE_DISABLED: 'Désactivé',
ADD_VARIABLE_BUTTON: "Ajouter une variable",
// Afficher les formulaires
CREATE_A_NEW_FORM: "Créer un nouveau formulaire",
@ -46,7 +61,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
DELETE_FORM_MD: "Supprimer le formulaire",
DELETE: "Supprimer",
FORM: 'Formulaire',
VIEW: "Afficher",
VIEW_MY_TELLFORM: "Afficher ma forme",
LIVE: "Live",
PREVIEW: 'Aperçu',
COPY: "Copier",

View File

@ -5,16 +5,31 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
$translateProvider.translations('de', {
// Konfigurieren der Formularregisterkarte
ADVANCED_SETTINGS: 'Erweiterte Einstellungen',
FORM_NAME: 'Formularname',
FORM_STATUS: 'Formularstatus',
FORM_NAME: 'Ihr tellform heißt',
FORM_STATUS: 'Status',
PUBLIC: 'Öffentlich',
PRIVATE: 'Privat',
GA_TRACKING_CODE: 'Google Analytics Tracking-Code',
DISPLAY_FOOTER: 'Formularfußzeile anzeigen?',
DISPLAY_FOOTER: 'Fußzeile',
SAVE_CHANGES: 'Änderungen speichern',
CANCEL: 'Abbrechen',
DISPLAY_START_PAGE: 'Startseite anzeigen?',
DISPLAY_END_PAGE: 'Benutzerdefinierte Endseite anzeigen?',
DISPLAY_START_PAGE: 'Startseite',
DISPLAY_END_PAGE: 'Benutzerdefinierte Endseite',
GENERAL_TAB: 'Allgemein',
SELF_NOTIFICATIONS_TAB: 'Selbstbenachrichtigungen',
RESPONDANT_NOTIFICATIONS_TAB: 'Beantwortungsbenachrichtigungen',
SEND_NOTIFICATION_TO: 'Senden an',
NO_EMAIL_FIELD_WARNING: 'Fehler: Sie benötigen ein E-Mail-Feld in Ihrem Formular, um die E-Mail an Ihr Formular zu senden.',
REPLY_TO: 'Antworten auf',
EMAIL_SUBJECT: "Betreff",
EMAIL_MESSAGE: 'Nachricht',
ENABLE_RESPONDENT_NOTIFICATIONS: 'Antwortbenachrichtigungen sind derzeit',
ENABLE_SELF_NOTIFICATIONS: 'Selbstbenachrichtigungen sind derzeit',
TOGGLE_ENABLED: 'Aktiviert',
TOGGLE_DISABLED: 'Deaktiviert',
ADD_VARIABLE_BUTTON: 'Variable hinzufügen',
// Listenformularansicht
CREATE_A_NEW_FORM: 'Erstelle ein neues Formular',
@ -46,7 +61,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
DELETE_FORM_MD: 'Formular löschen',
DELETE: 'Löschen',
FORM: 'Formular',
VIEW: 'Ansicht',
VIEW_MY_TELLFORM: 'Mein tellform anzeigen',
LIVE: 'Leben',
PREVIEW: 'Vorschau',
COPY: 'Kopieren',

View File

@ -5,16 +5,31 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
$translateProvider.translations('it', {
// Configura la visualizzazione scheda modulo
ADVANCED_SETTINGS: 'Impostazioni avanzate',
FORM_NAME: 'Nome modulo',
FORM_STATUS: 'Stato modulo',
FORM_NAME: 'Il tuo tellform è chiamato',
FORM_STATUS: 'Stato',
PUBLIC: 'pubblico',
PRIVATE: 'Privato',
GA_TRACKING_CODE: 'Codice di monitoraggio di Google Analytics',
DISPLAY_FOOTER: 'Visualizza piè di pagina?',
DISPLAY_FOOTER: 'Piè di pagina',
SAVE_CHANGES: 'Salva modifiche',
CANCEL: 'Annulla',
DISPLAY_START_PAGE: 'Visualizza pagina iniziale?',
DISPLAY_END_PAGE: 'Mostra pagina finale personalizzata?',
DISPLAY_START_PAGE: 'Pagina iniziale',
DISPLAY_END_PAGE: 'Pagina finale personalizzata',
GENERAL_TAB: 'Generale',
SELF_NOTIFICATIONS_TAB: 'Autodiagnosi',
RESPONDANT_NOTIFICATIONS_TAB: 'Notifiche rispondenti',
SEND_NOTIFICATION_TO: 'Invia a',
NO_EMAIL_FIELD_WARNING: 'Errore: Hai bisogno di un campo e-mail nel tuo modulo per inviare l\'email al tuo interlocutore',
REPLY_TO: 'Rispondi a',
EMAIL_SUBJECT: 'Oggetto',
EMAIL_MESSAGE: 'Messaggio',
ENABLE_RESPONDENT_NOTIFICATIONS: 'Notifiche rispondenti sono attualmente',
ENABLE_SELF_NOTIFICATIONS: 'Le notifiche auto sono attualmente',
TOGGLE_ENABLED: 'Abilitato',
TOGGLE_DISABLED: 'disabilitato',
ADD_VARIABLE_BUTTON: 'Aggiungi variabile',
// Visualizzazione dei moduli di elenco
CREATE_A_NEW_FORM: 'Crea un nuovo modulo',
@ -46,7 +61,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
DELETE_FORM_MD: 'Elimina modulo',
DELETE: 'Elimina',
FORM: 'Forma',
VIEW: 'Visualizza',
VIEW_MY_TELLFORM: 'Visualizza la mia informazione',
LIVE: 'Live',
PREVIEW: 'Anteprima',
COPY: 'Copia',

View File

@ -3,19 +3,33 @@
angular.module('forms').config(['$translateProvider', function ($translateProvider) {
$translateProvider.translations('es', {
//Configure Form Tab View
ADVANCED_SETTINGS: 'Configuraciones avanzadas',
FORM_NAME: 'Nombre del formulario',
FORM_STATUS: 'Estado del formulario',
FORM_NAME: 'Tu tellform se llama',
FORM_STATUS: 'Estado',
PUBLIC: 'Público',
PRIVATE: 'Privado',
GA_TRACKING_CODE: 'Código de Google Analytics',
DISPLAY_FOOTER: '¿Mostrar pie de página?',
DISPLAY_FOOTER: 'Pie de página',
SAVE_CHANGES: 'Grabar',
CANCEL: 'Cancelar',
DISPLAY_START_PAGE: '¿Mostrar página de inicio?',
DISPLAY_END_PAGE: '¿Mostrar paǵina de fin?',
DISPLAY_START_PAGE: 'Página de inicio',
DISPLAY_END_PAGE: 'Página final personalizada',
SELF_NOTIFICATIONS_TAB: 'Auto notificaciones',
RESPONDANT_NOTIFICATIONS_TAB: 'Notificaciones de los demandados',
GENERAL_TAB: 'Général',
SEND_NOTIFICATION_TO: 'Enviar a',
NO_EMAIL_FIELD_WARNING: 'Error: necesita un campo de correo electrónico en su formulario para enviar el correo electrónico a su encuestado',
REPLY_TO: 'Responder a',
EMAIL_SUBJECT: 'Asunto',
EMAIL_MESSAGE: 'Mensaje',
ENABLE_RESPONDENT_NOTIFICATIONS: 'Las notificaciones de los demandados son actualmente',
ENABLE_SELF_NOTIFICATIONS: 'Las notificaciones automáticas están actualmente',
TOGGLE_ENABLED: 'Habilitado',
TOGGLE_DISABLED: 'Desactivado',
ADD_VARIABLE_BUTTON: 'Agregar variable',
//List Forms View
CREATE_A_NEW_FORM: 'Crear formulario',
@ -47,7 +61,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
DELETE_FORM_MD: 'Borrar formulario',
DELETE: 'Borrar',
FORM: 'Formulario',
VIEW: 'Vista',
VIEW_MY_TELLFORM: 'Ver mi tellform',
LIVE: 'Online',
PREVIEW: 'Vista previa',
COPY: 'Copiar',

View File

@ -4,19 +4,12 @@
angular.module('forms').controller('AdminFormController', ['$rootScope', '$window', '$scope', '$stateParams', '$state', 'Forms', 'CurrentForm', '$http', '$uibModal', 'myForm', '$filter', '$translate',
function($rootScope, $window, $scope, $stateParams, $state, Forms, CurrentForm, $http, $uibModal, myForm, $filter, $translate) {
//Set active tab to Create
$scope.activePill = 0;
$scope.copied = false;
$scope.onCopySuccess = function (e) {
$scope.copied = true;
};
$scope = $rootScope;
$scope.animationsEnabled = true;
$scope.myform = myForm;
$rootScope.saveInProgress = false;
$scope.oldForm = _.cloneDeep($scope.myform);
$scope.designTabActive = false
CurrentForm.setForm($scope.myform);
@ -36,7 +29,6 @@ angular.module('forms').controller('AdminFormController', ['$rootScope', '$windo
$scope.actualFormURL = window.location.protocol + '//' + window.location.host + $scope.formURL;
}
var refreshFrame = $scope.refreshFrame = function(){
if(document.getElementById('iframe')) {
document.getElementById('iframe').contentWindow.location.reload();
@ -44,13 +36,59 @@ angular.module('forms').controller('AdminFormController', ['$rootScope', '$windo
};
$scope.tabData = [
{
heading: $filter('translate')('CREATE_TAB'),
route: 'viewForm.create',
active: false
},
{
heading: $filter('translate')('CONFIGURE_TAB'),
templateName: 'configure'
route: 'viewForm.configure.general',
active: false
},
{
heading: $filter('translate')('ANALYZE_TAB'),
route: 'viewForm.analyze',
active: false
},
{
heading: $filter('translate')('SHARE_TAB'),
route: 'viewForm.share',
active: false
},
{
heading: $filter('translate')('DESIGN_TAB'),
route: 'viewForm.design',
active: false
}
];
$scope.designTabActive = false
$scope.go = function(tab){
var currParentState = $state.current.name.split('.').slice(0,2).join('.');
var tabParentState = tab.route.split('.').slice(0,2).join('.');
if(currParentState !== tabParentState && tabParentState !== 'viewForm.configure.general'){
$state.go(tab.route);
}
};
function setActiveTab() {
$scope.tabData.forEach(function(tab) {
var currentTabState = $state.current.name.split('.').slice(0,2).join('.');
var tabRouteState = tab.route.split('.').slice(0,2).join('.');
tab.active = (currentTabState === tabRouteState);
if(tab.active && tab.route === 'viewForm.design'){
$scope.designTabActive = true;
} else {
$scope.designTabActive = false;
}
});
}
setActiveTab();
$scope.$on("$stateChangeSuccess", setActiveTab());
$scope.deactivateDesignTab = function(){
$scope.designTabActive = false

View File

@ -0,0 +1,43 @@
.tag {
background-color: #999;
border-radius: 3px 3px 3px 3px;
border: 0;
color: #FFFFFF;
font-style: inherit;
font-size: 11px;
padding: 4px 5px;
margin: 0 2px 2px 2px;
font-family: inherit;
white-space: nowrap;
vertical-align: middle;
cursor: pointer !important;
pointer-events: none;
}
.email-subject.ta-root .ta-editor.ta-html, .email-subject .ta-scroll-window.form-control {
min-height: 0;
overflow: hidden;
height: auto;
border-radius: 4px;
box-shadow: none;
font-size: 18px;
padding-top: 10px;
}
.email-subject.ta-root .ta-scroll-window > .ta-bind {
min-height: 0;
outline: 0;
}
.ui-select input.form-control {
height: 34px;
padding: 6px;
}
.config-form .btn-secondary {
border-color: #DDDDDD;
}
.notification-toggle.toggle-switch {
margin: 5px 0;
}

View File

@ -247,9 +247,25 @@ div.config-form .row.field {
margin: 10px 0 10px;
}
.view-form-btn {
border: none;
}
.view-form-btn.span {
padding-right:0.6em;
}
.notification-row {
display: inline-block;
padding: 0 5px;
}
.status-light {
font-size: 10px;
}
.notification-row .status-light {
padding-top: 15px;
}
.status-light.status-light-off {
color: #BE0000;
}

View File

@ -1,23 +1,79 @@
'use strict';
angular.module('forms').directive('configureFormDirective', ['$rootScope', '$http', 'Upload', 'CurrentForm',
function ($rootScope, $http, Upload, CurrentForm) {
angular.module('forms').directive('configureFormDirective', ['$rootScope', '$filter', '$state',
function ($rootScope, $filter, $state) {
return {
templateUrl: 'modules/forms/admin/views/directiveViews/form/configure-form.client.view.html',
restrict: 'E',
scope: {
myform:'=',
user:'=',
pdfFields:'@',
formFields:'@'
myform:'='
},
controller: function($scope){
$scope.log = '';
$rootScope.myform = $scope.myform;
$scope.languages = $rootScope.languages;
$scope.resetForm = $rootScope.resetForm;
$scope.update = $rootScope.update;
$scope.$evalAsync(function() {
angular.element('.tag')
});
$scope.languages = ['en', 'fr', 'es', 'it', 'de'];
$scope.langCodeToWord = {
'en': 'English',
'fr': 'Français',
'es': 'Español',
'it': 'Italiàno',
'de': 'Deutsch'
};
$scope.wordToLangCode = {
'English': 'en',
'Français': 'fr',
'Español': 'es',
'Italiàno': 'it',
'Deutsch': 'de'
};
$scope.configureTabs = [
{
heading: $filter('translate')('GENERAL_TAB'),
route: 'viewForm.configure.general',
active: false
},
{
heading: $filter('translate')('SELF_NOTIFICATIONS_TAB'),
route: 'viewForm.configure.self_notifications',
active: false
},
{
heading: $filter('translate')('RESPONDENT_NOTIFICATIONS_TAB'),
route: 'viewForm.configure.respondent_notifications',
active: false
}
];
$scope.emailFields = $scope.myform.form_fields.filter(function(field){
return field.fieldType === 'email';
});
$scope.formHasEmailField = ($scope.emailFields.length > 0);
$scope.go = function(tab){
tab.active = true;
$state.go(tab.route);
};
function setActiveTab() {
$scope.configureTabs.forEach(function(tab) {
tab.active = ($state.current.name === tab.route);
});
}
setActiveTab();
$scope.$on("$stateChangeSuccess", setActiveTab());
}
};
}

View File

@ -0,0 +1,14 @@
'use strict';
angular.module('forms').directive('designFormDirective', [
function () {
return {
templateUrl: 'modules/forms/admin/views/directiveViews/form/design-form.client.view.html',
restrict: 'E',
scope: {
myform:'=',
formurl: '='
}
}
}
]);

View File

@ -1,4 +1,3 @@
'use strict';
angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormFields', '$uibModal',

View File

@ -0,0 +1,16 @@
'use strict';
angular.module('forms').directive('shareFormDirective', ['$rootScope',
function ($rootScope) {
return {
templateUrl: 'modules/forms/admin/views/directiveViews/form/share-form.client.view.html',
restrict: 'E',
scope: {
actualformurl:'='
},
controller: function($scope){
$scope.actualFormURL = $scope.actualformurl;
}
};
}
]);

View File

@ -1,11 +1,18 @@
'use strict';
//TODO: DAVID: URGENT: Make this a $resource that fetches valid field types from server
<<<<<<< HEAD
angular.module('forms').service('FormFields', [ '$rootScope', '$translate', '$window',
function($rootScope, $translate, $window) {
console.log($window.user);
$translate.use($window.user.language);
=======
angular.module('forms').service('FormFields', [ '$rootScope', '$translate', 'Auth',
function($rootScope, $translate, Auth) {
var language = Auth.ensureHasCurrentUser().language;
$translate.use(language);
>>>>>>> 2.20
this.types = [
{

View File

@ -42,158 +42,26 @@
<div class="col-xs-1 col-sm-2">
<small class="pull-right">
<a class="btn btn-secondary view-form-btn" href="{{actualFormURL}}">
<span class="hidden-xs hidden-sm">
{{ 'VIEW' | translate }}
<span ng-show="myform.isLive">
{{ 'LIVE' | translate }}
</span>
<span ng-hide="myform.isLive">{{ 'PREVIEW' | translate }}</span>
<i class="fa fa-external-link"></i>
<span>
{{ 'VIEW_MY_TELLFORM' | translate }}
</span>
<i class="status-light status-light-on fa fa-dot-circle-o" ng-if="myform.isLive"></i>
<i class="status-light status-light-off fa fa-dot-circle-o" ng-if="!myform.isLive"></i>
<i class="status-light status-light-on fa fa-circle" ng-if="myform.isLive"></i>
<i class="status-light status-light-off fa fa-circle" ng-if="!myform.isLive"></i>
</a>
</small>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<uib-tabset active="activePill" vertical="true" type="pills">
<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}}" 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 }}" 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}}" select="deactivateDesignTab()">
<div class="config-form">
<div class="row">
<div class="col-sm-12">
<uib-tabset active="activePill" vertical="true" type="pills">
<uib-tab index="0" heading="{{ 'SHARE_YOUR_FORM' | translate }}">
<div class="row">
<div class="col-sm-12">
{{ 'TELLFORM_URL' | translate }}
</div>
<div class="col-sm-8 form-input">
<span ngclipboard data-clipboard-target="#copyURL"> <input id="copyURL" ng-value="actualFormURL" class="form-control ng-pristine ng-untouched ng-valid"> </span>
</div>
<div class="col-sm-4">
<button class="btn btn btn-secondary view-form-btn" ngclipboard data-clipboard-target="#copyURL">
{{ 'COPY' | translate }} <i class="fa fa-clipboard" aria-hidden="true"></i>
</button>
</div>
</div>
</uib-tab>
<uib-tab index="1" heading="{{ 'EMBED_YOUR_FORM' | translate }}">
<div class="row">
<div class="col-sm-12">
{{ 'COPY_AND_PASTE' | translate }}
</div>
<div class="col-sm-8 form-input">
<span ngclipboard data-clipboard-target="#copyEmbedded">
<textarea id="copyEmbedded" class="form-control ng-pristine ng-untouched ng-valid" style="min-height:200px; width:100%; background-color: #FFFFCC; color: #30313F;">
&lt;!-- {{ 'CHANGE_WIDTH_AND_HEIGHT' | translate }} --&gt;
<iframe id="iframe" src="{{actualFormURL}}" style="width:100%;height:500px;"></iframe>
<div style="font-family: Sans-Serif;font-size: 12px;color: #999;opacity: 0.5; padding-top: 5px;">{{ 'POWERED_BY' | translate }} <a href="https://www.tellform.com" style="color: #999" target="_blank">TellForm</a></div>
</textarea>
</span>
</div>
<div class="col-sm-4">
<button class="btn btn btn-secondary view-form-btn" ngclipboard data-clipboard-target="#copyEmbedded">
{{ 'COPY' | translate }} <i class="fa fa-clipboard" aria-hidden="true"></i>
</button>
</div>
</div>
</uib-tab>
</uib-tabset>
</div>
</div>
</div>
</uib-tab>
<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">
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'BACKGROUND_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text"
ng-model="myform.design.colors.backgroundColor"
ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/"
ng-style="{ 'background-color': myform.design.colors.backgroundColor }"/>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'QUESTION_TEXT_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text" ng-model="myform.design.colors.questionColor" ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/"ng-style="{ 'background-color': myform.design.colors.questionColor }"/>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'ANSWER_TEXT_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text" ng-model="myform.design.colors.answerColor" ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/" ng-style="{ 'background-color': myform.design.colors.answerColor }"/>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'BTN_BACKGROUND_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text"
ng-model="myform.design.colors.buttonColor"
ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/"
ng-style="{ 'background-color': myform.design.colors.buttonColor }"/>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'BTN_TEXT_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text"
ng-model="myform.design.colors.buttonTextColor"
ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/"
ng-style="{ 'background-color': myform.design.colors.buttonTextColor }"/>
</div>
</div>
</div>
<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>
</div>
</div>
<div class="row">
<div class="col-sm-offset-4 col-sm-2">
<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>
</div>
</div>
</div>
<div >
<uib-tabset vertical="true" type="pills">
<uib-tab ng-repeat="tab in tabData" active="tab.active" select="go(tab)" heading="{{tab.heading}}">
</uib-tab>
</uib-tabset>
</div>
<div class="col-xs-10">
<div ui-view></div>
</div>
</div>
</section>

View File

@ -1,2 +1,4 @@
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.8.5/css/selectize.default.css'>
<configure-form-directive myform="myform" user="user">
</configure-form-directive>

View File

@ -0,0 +1,106 @@
<!-- Settings -->
<div class="row">
<div class="col-sm-offset-2 col-sm-10">
<div class="row field">
<div class="field-title col-sm-12">
<h4>{{ 'FORM_NAME' | translate }}</h4>
</div>
<div class="col-sm-12">
<input class=""
type="text"
ng-model="myform.title"
value="{{myform.title}}"
style="width: 100%;"
ng-minlength="4"
ng-pattern="/^[a-zA-Z0-9 \-.]*$/">
</div>
</div>
<div class="row field">
<div class="col-sm-4 field-title"><h4>{{ 'LANGUAGE' | translate }}</h4></div>
<div class="col-sm-8 field-input">
<ui-select ng-model="myform.language" search-enabled="false" theme="selectize">
<ui-select-match>
{{ langCodeToWord[$select.selected] }}
</ui-select-match>
<ui-select-choices repeat="language in languages">
<span ng-bind-html="langCodeToWord[language] | highlight: $select.search">
</span>
</ui-select-choices>
</select>
<span class="required-error" ng-show="field.required && !field.fieldValue">* {{ 'REQUIRED_FIELD' | translate }}</span>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-8">
<h4>{{ 'FORM_STATUS' | translate }}</h4>
</div>
<div class="field-input col-sm-4 text-right">
<input class="toggle-switch" type="checkbox" name="my-checkbox"
bs-switch ng-model="myform.isLive"
switch-on-text="{{ 'PUBLIC' | translate }}"
switch-off-text="{{ 'PRIVATE' | translate }}"
switch-on-color="success"
switch-off-color="danger">
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h4>{{ 'GA_TRACKING_CODE' | translate }}</h4>
</div>
<div class="col-sm-12">
<input class=""
type="text"
ng-model="myform.analytics.gaCode"
value="{{myform.analytics.gaCode}}"
style="width: 100%;"
ng-minlength="4"
placeholder="UA-XXXXX-Y"
ng-pattern="/\bUA-\d{4,10}-\d{1,4}\b/">
</div>
</div>
<div class="row field">
<div class="field-title col-sm-8">
<h4>{{ 'DISPLAY_FOOTER' | translate }}</h4>
</div>
<div class="field-input col-sm-4 text-right">
<input class="toggle-switch" type="checkbox"
bs-switch ng-model="myform.hideFooter"
switch-on-text="{{ 'ON' | translate }}"
switch-off-text="{{ 'OFF' | translate }}">
</div>
</div>
<div class="row field">
<div class="field-title col-sm-8">
<h4>{{ 'DISPLAY_START_PAGE' | translate }}</h4>
</div>
<div class="field-input col-sm-4 text-right">
<input class="toggle-switch" type="checkbox"
bs-switch ng-model="myform.startPage.showStart"
switch-on-text="{{ 'ON' | translate }}"
switch-off-text="{{ 'OFF' | translate }}">
</div>
</div>
<div class="row field">
<div class="field-title col-sm-8">
<h4>{{ 'DISPLAY_END_PAGE' | translate }}</h4>
</div>
<div class="field-input col-sm-4 text-right">
<input class="toggle-switch" type="checkbox"
bs-switch ng-model="myform.endPage.showEnd"
switch-on-text="{{ 'ON' | translate }}"
switch-off-text="{{ 'OFF' | translate }}">
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,81 @@
<div class="row">
<div class="col-sm-offset-2 col-sm-10">
<div class="row field">
<div class="notification-row">
<i class="status-light status-light-on fa fa-circle" ng-if="myform.respondentNotifications.enabled"></i>
<i class="status-light status-light-off fa fa-circle" ng-if="!myform.respondentNotifications.enabled"></i>
</div>
<div class="notification-row">
<h5>{{ 'ENABLE_RESPONDENT_NOTIFICATIONS' | translate }}</h5>
</div>
<div class="notification-row">
<input class="toggle-switch notification-toggle" type="checkbox" switch-size="small"
bs-switch ng-model="myform.respondentNotifications.enabled"
switch-on-text="{{ 'ON' | translate }}"
switch-off-text="{{ 'OFF' | translate }}">
</div>
</div>
<div class="row field" ng-if="formHasEmailField">
<div class="field-title col-sm-12">
<h5>{{ 'SEND_NOTIFICATION_TO' | translate }}:</h5>
</div>
<div class="col-sm-12 field-input">
<ui-select ng-model="myform.respondentNotifications.toField" theme="selectize">
<ui-select-match placeholder="{{ 'OPTION_PLACEHOLDER' | translate }}">
{{$select.selected.title}}
</ui-select-match>
<ui-select-choices repeat="field.globalId as field in emailFields | filter: { title: $select.search }">
<span ng-bind-html="field.title | highlight: $select.search">
</span>
</ui-select-choices>
</ui-select>
</div>
</div>
<div class="row field" ng-if="!formHasEmailField">
<strong>
{{ 'NO_EMAIL_FIELD_WARNING' | translate }}
</strong>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h5>{{ 'REPLY_TO' | translate }}:</h5>
</div>
<div class="col-sm-12 field-input">
<input class="form-control"
type="text"
ng-model="myform.respondentNotifications.replyEmail"
placeholder="noreply@tellform.com">
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h5>{{ 'EMAIL_SUBJECT' | translate }}:</h5>
</div>
<div class="col-sm-12 field-input">
<text-angular class="email-subject" ng-model="myform.respondentNotifications.subject" ta-toolbar="[['insertField']]" ta-default-wrap="n" ta-unsafe-sanitizer="true">
</text-angular>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h5>{{ 'EMAIL_MESSAGE' | translate }}</h5>
</div>
<div class="col-sm-12 field-input">
<text-angular class="email-subject" ng-model="myform.respondentNotifications.htmlTemplate"
ta-toolbar="[['bold','italics', 'insertField']]"
ta-unsafe-sanitizer="true">
</text-angular>
<div ng-bind="myform.respondentNotifications.htmlTemplate"></div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,74 @@
<div class="row">
<div class="col-sm-offset-2 col-sm-10">
<div class="row field">
<div class='notification-row'>
<i class="status-light status-light-on fa fa-circle" ng-if="myform.selfNotifications.enabled"></i>
<i class="status-light status-light-off fa fa-circle" ng-if="!myform.selfNotifications.enabled"></i>
</div>
<div class='notification-row'>
<h5>{{ 'ENABLE_SELF_NOTIFICATIONS' | translate }}</h5>
</div>
<div class='notification-row'>
<input class="toggle-switch notification-toggle" type="checkbox" switch-size="small"
bs-switch ng-model="myform.selfNotifications.enabled"
switch-on-text="{{ 'ON' | translate }}"
switch-off-text="{{ 'OFF' | translate }}">
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h5>{{ 'SEND_NOTIFICATION_TO' | translate }}</h5>
</div>
<div class="col-sm-12 field-input">
<input class="form-control"
type="text"
ng-model="myform.selfNotifications.toEmails"
placeholder="email@domain.com,email2@domain2.com,etc">
</div>
</div>
<div class="row field" ng-if="formHasEmailField">
<div class="field-title col-sm-12">
<h5>{{ 'REPLY_TO' | translate }}</h5>
</div>
<div class="col-sm-12 ui-select field-input">
<ui-select ng-model="myform.selfNotifications.fromField" theme="selectize">
<ui-select-match placeholder="{{ 'OPTION_PLACEHOLDER' | translate }}">
{{$select.selected.title}}
</ui-select-match>
<ui-select-choices repeat="field.globalId as field in emailFields | filter: { title: $select.search }">
<span ng-bind-html="field.title | highlight: $select.search">
</span>
</ui-select-choices>
</ui-select>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h5>{{ 'EMAIL_SUBJECT' | translate }}</h5>
</div>
<div class="col-sm-12 field-input">
<text-angular class="email-subject" ng-model="myform.selfNotifications.subject" ta-toolbar="[['insertField']]"
ta-unsafe-sanitizer="true" ta-default-wrap="n"></text-angular>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h5>{{ 'EMAIL_MESSAGE' | translate }}</h5>
</div>
<div class="col-sm-12 field-input">
<text-angular ng-model="myform.selfNotifications.htmlTemplate" ta-toolbar="[['bold','italics', 'insertField']]" ta-unsafe-sanitizer="true"></text-angular>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1 @@
<design-form-directive myform="myform" formurl="formURL"></design-form-directive>

View File

@ -0,0 +1 @@
<share-form-directive actualformurl="actualFormURL"></share-form-directive>

View File

@ -1,133 +1,13 @@
<div class="config-form container">
<!-- Settings -->
<div class="row">
<div class="col-sm-offset-2 col-sm-4">
<div class="row field">
<div class="field-title col-sm-12">
<h5>{{ 'FORM_NAME' | translate }}</h5>
</div>
<uib-tabset active="activePill" type="pills">
<uib-tab ng-repeat="tab in configureTabs" active="tab.active" select="go(tab)" heading="{{tab.heading}}">
<div ui-view></div>
</uib-tab>
</uib-tabset>
</div>
<div class="col-sm-12">
<input class="form-control"
type="text"
ng-model="myform.title"
value="{{myform.title}}"
style="width: 100%;"
ng-minlength="4"
ng-pattern="/^[a-zA-Z0-9 \-.]*$/">
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h5>{{ 'FORM_STATUS' | translate }}</h5>
</div>
<div class="field-input col-sm-12">
<label style="display: inline-block;">
<input type="radio" data-ng-value="true" ng-model="myform.isLive" ng-required="true" style="background-color:#33CC00;"/>
&nbsp;<span>{{ 'PUBLIC' | translate }}</span>
</label>
<label style="display: inline-block;">
<input type="radio" data-ng-value="false" ng-model="myform.isLive" ng-required="true" />
&nbsp;<span>{{ 'PRIVATE' | translate }}</span>
</label>
</div>
</div>
<div class="row field">
<div class="col-sm-12 field-title">{{ 'LANGUAGE' | translate }}</div>
<div class="col-sm-12 field-input">
<select ng-model="myform.language">
<option ng-repeat="language in languages"
ng-selected="language == myform.language"
value="{{language}}">
{{language}}
</option>
</select>
<span class="required-error" ng-show="field.required && !field.fieldValue">* {{ 'REQUIRED_FIELD' | translate }}</span>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="row field">
<div class="field-title col-sm-12">
<h5>{{ 'GA_TRACKING_CODE' | translate }}</h5>
</div>
<div class="col-sm-12">
<input class="form-control"
type="text"
ng-model="myform.analytics.gaCode"
value="{{myform.analytics.gaCode}}"
style="width: 100%;"
ng-minlength="4"
placeholder="UA-XXXXX-Y"
ng-pattern="/\bUA-\d{4,10}-\d{1,4}\b/">
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h5>{{ 'DISPLAY_FOOTER' | translate }}</h5>
</div>
<div class="field-input col-sm-12">
<label style="display: inline-block;">
<input type="radio" data-ng-value="false" ng-model="myform.hideFooter" ng-required="true" />
&nbsp;<span>{{ 'YES' | translate }}</span>
</label>
<label style="display: inline-block;">
<input type="radio" data-ng-value="true" ng-model="myform.hideFooter" ng-required="true" />
&nbsp;<span>{{ 'NO' | translate }}</span>
</label>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h5>{{ 'DISPLAY_START_PAGE' | translate }}</h5>
</div>
<div class="field-input col-sm-12">
<label style="display: inline-block;">
<input type="radio" data-ng-value="true" ng-model="myform.startPage.showStart" ng-required="true" style="background-color:#33CC00;"/>
&nbsp;<span>{{ 'YES' | translate }}</span>
</label>
<label style="display: inline-block;">
<input type="radio" data-ng-value="false" ng-model="myform.startPage.showStart" ng-required="true" />
&nbsp;<span>{{ 'NO' | translate }}</span>
</label>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h5>{{ 'DISPLAY_END_PAGE' | translate }}</h5>
</div>
<div class="field-input col-sm-12">
<label style="display: inline-block;">
<input type="radio" data-ng-value="true" ng-model="myform.endPage.showEnd" ng-required="true" style="background-color:#33CC00;"/>
&nbsp;<span>{{ 'YES' | translate }}</span>
</label>
<label style="display: inline-block;">
<input type="radio" data-ng-value="false" ng-model="myform.endPage.showEnd" ng-required="true" />
&nbsp;<span>{{ 'NO' | translate }}</span>
</label>
</div>
</div>
</div>
</div>
<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>

View File

@ -0,0 +1,76 @@
<div class="config-form design container">
<div class="row">
<div class="col-sm-4 col-xs-12">
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'BACKGROUND_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text"
ng-model="myform.design.colors.backgroundColor"
ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/"
ng-style="{ 'background-color': myform.design.colors.backgroundColor }"/>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'QUESTION_TEXT_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text" ng-model="myform.design.colors.questionColor" ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/"ng-style="{ 'background-color': myform.design.colors.questionColor }"/>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'ANSWER_TEXT_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text" ng-model="myform.design.colors.answerColor" ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/" ng-style="{ 'background-color': myform.design.colors.answerColor }"/>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'BTN_BACKGROUND_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text"
ng-model="myform.design.colors.buttonColor"
ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/"
ng-style="{ 'background-color': myform.design.colors.buttonColor }"/>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'BTN_TEXT_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text"
ng-model="myform.design.colors.buttonTextColor"
ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/"
ng-style="{ 'background-color': myform.design.colors.buttonTextColor }"/>
</div>
</div>
</div>
<div class="col-sm-8 hidden-xs">
<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>
</div>
</div>
<div class="row">
<div class="col-sm-offset-4 col-sm-2">
<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>
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
<div class="submissions-table container">
<div class="submissions-table row">
<div class="row text-center analytics">
<div class="col-xs-12 header-title">
<div class="col-xs-3">

View File

@ -0,0 +1,44 @@
<div class="config-form row">
<div class="row">
<div class="col-sm-12">
<uib-tabset active="activePill" vertical="true" type="pills">
<uib-tab index="0" heading="{{ 'SHARE_YOUR_FORM' | translate }}">
<div class="row">
<div class="col-sm-12">
{{ 'TELLFORM_URL' | translate }}
</div>
<div class="col-sm-8 form-input">
<span ngclipboard data-clipboard-target="#copyURL"> <input id="copyURL" ng-value="actualFormURL" class="form-control ng-pristine ng-untouched ng-valid"> </span>
</div>
<div class="col-sm-4">
<button class="btn btn btn-secondary view-form-btn" ngclipboard data-clipboard-target="#copyURL">
{{ 'COPY' | translate }} <i class="fa fa-clipboard" aria-hidden="true"></i>
</button>
</div>
</div>
</uib-tab>
<uib-tab index="1" heading="{{ 'EMBED_YOUR_FORM' | translate }}">
<div class="row">
<div class="col-sm-12">
{{ 'COPY_AND_PASTE' | translate }}
</div>
<div class="col-sm-8 form-input">
<span ngclipboard data-clipboard-target="#copyEmbedded">
<textarea id="copyEmbedded" class="form-control ng-pristine ng-untouched ng-valid" style="min-height:200px; width:100%; background-color: #FFFFCC; color: #30313F;">
&lt;!-- {{ 'CHANGE_WIDTH_AND_HEIGHT' | translate }} --&gt;
<iframe id="iframe" src="{{actualFormURL}}" style="width:100%;height:500px;"></iframe>
<div style="font-family: Sans-Serif;font-size: 12px;color: #999;opacity: 0.5; padding-top: 5px;">{{ 'POWERED_BY' | translate }} <a href="https://www.tellform.com" style="color: #999" target="_blank">TellForm</a></div>
</textarea>
</span>
</div>
<div class="col-sm-4">
<button class="btn btn btn-secondary view-form-btn" ngclipboard data-clipboard-target="#copyEmbedded">
{{ 'COPY' | translate }} <i class="fa fa-clipboard" aria-hidden="true"></i>
</button>
</div>
</div>
</uib-tab>
</uib-tabset>
</div>
</div>
</div>

View File

@ -224,13 +224,13 @@ form .row.field {
font-size:0.8em;
}
form .row.field.dropdown > .field-input input {
form .dropdown > .field-input input {
min-height: 34px;
border-width: 0 0 2px 0;
border-radius: 5px;
}
form .row.field.dropdown > .field-input input:focus {
form .dropdown > .field-input input:focus {
border: none;
}

View File

@ -6,6 +6,15 @@ angular.module('forms').run(['Menus',
// Set top bar menu items
Menus.addMenuItem('topbar', 'My Forms', 'forms', '', '/forms', false);
}
]).run(['$rootScope', '$state',
function($rootScope, $state) {
$rootScope.$on('$stateChangeStart', function(evt, to, params) {
if (to.redirectTo) {
evt.preventDefault();
$state.go(to.redirectTo, params)
}
});
}
]).filter('secondsToDateTime', [function() {
return function(seconds) {
return new Date(1970, 0, 1).setSeconds(seconds);
@ -46,4 +55,31 @@ angular.module('forms').run(['Menus',
directive.replace = true;
return $delegate;
});
}]);
}]).config(['$provide', function ($provide){
$provide.decorator('taOptions', ['$delegate', 'taRegisterTool', '$translate', '$window', function(taOptions, taRegisterTool, $translate, $window) {
taRegisterTool('insertField', {
display: '<div class="dropdown" uib-dropdown is-open="isopen">\
<div class="dropdown-toggle" ng-disabled="isDisabled()" uib-dropdown-toggle>\
<span>{{ "ADD_VARIABLE_BUTTON" | translate }}</span>\
<b class="caret"></b>\
</div>\
<ul class="dropdown-menu">\
<li ng-repeat="field in $root.myform.form_fields" ng-click="onClickField(field.globalId, field.title)">\
{{field.title}}\
</li>\
</ul>\
</div>',
onClickField: function(field_id, field_name){
this.$editor().wrapSelection('insertHTML', '<var class="tag" contenteditable="false" id="field:' + field_id + '">' + field_name + '</var>', false);
},
action: function(){
}
});
taOptions.defaultTagAttributes['var'] = {
'contenteditable': 'false'
};
return taOptions;
}]);
}]);

View File

@ -43,6 +43,7 @@ angular.module('forms').config(['$stateProvider',
controller: 'SubmitFormController',
controllerAs: 'ctrl'
}).state('viewForm', {
abstract: true,
url: '/forms/:formId/admin',
templateUrl: 'modules/forms/admin/views/admin-form.client.view.html',
data: {
@ -63,18 +64,35 @@ angular.module('forms').config(['$stateProvider',
}]
},
controller: 'AdminFormController'
}).state('viewForm.configure', {
}).state('viewForm.create', {
url: '/create',
templateUrl: 'modules/forms/admin/views/adminTabs/create.html'
})
.state('viewForm.configure', {
abstract: true,
url: '/configure',
templateUrl: 'modules/forms/admin/views/adminTabs/configure.html'
}).state('viewForm.design', {
}).state('viewForm.configure.general', {
url: '/general',
templateUrl: 'modules/forms/admin/views/adminTabs/configureTabs/general.html'
}).state('viewForm.configure.self_notifications', {
url: '/self_notifications',
templateUrl: 'modules/forms/admin/views/adminTabs/configureTabs/self-notifications.html'
}).state('viewForm.configure.respondent_notifications', {
url: '/respondent_notifications',
templateUrl: 'modules/forms/admin/views/adminTabs/configureTabs/respondent-notifications.html'
})
.state('viewForm.design', {
url: '/design',
templateUrl: 'modules/forms/admin/views/adminTabs/design.html'
}).state('viewForm.share', {
url: '/share',
templateUrl: 'modules/forms/admin/views/adminTabs/share.html'
}).state('viewForm.analyze', {
url: '/analyze',
templateUrl: 'modules/forms/admin/views/adminTabs/analyze.html'
}).state('viewForm.create', {
url: '/create',
templateUrl: 'modules/forms/admin/views/adminTabs/create.html'
});
}
]);

View File

@ -3,5 +3,10 @@
// Use Application configuration module to register a new module
ApplicationConfiguration.registerModule('forms', [
'ngFileUpload', 'ui.date', 'ui.sortable',
<<<<<<< HEAD
'angular-input-stars', 'users', 'ngclipboard', 'textAngular',
'frapontillo.bootstrap-switch'
=======
'angular-input-stars', 'users', 'ngclipboard'
>>>>>>> 2.20
]);//, 'colorpicker.module' @TODO reactivate this module

View File

@ -10,7 +10,8 @@
$httpBackend,
$stateParams,
$location,
$state;
$state,
$timeout;
var sampleUser = {
firstName: 'Full',
@ -175,7 +176,7 @@
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
// This allows us to inject a service but then attach it to a variable
// with the same name as the service.
beforeEach(inject(function($controller, $rootScope, _$state_, _$location_, _$stateParams_, _$httpBackend_, CurrentForm, Forms) {
beforeEach(inject(function($controller, $rootScope, _$state_, _$location_, _$stateParams_, _$httpBackend_, CurrentForm, Forms, _$timeout_) {
// Set a new global scope
scope = $rootScope.$new();
@ -187,6 +188,7 @@
$httpBackend = _$httpBackend_;
$location = _$location_;
$state = _$state_;
$timeout = _$timeout_;
$httpBackend.whenGET(/\.html$/).respond('');
$httpBackend.whenGET('/users/me/').respond('');
@ -197,60 +199,70 @@
};
}));
it('AdminFormController should fetch current Form when instantiated', function() {
// Run controller functionality
var controller = createAdminFormController();
it('AdminFormController should fetch current Form when instantiated', inject(function($timeout) {
$timeout(function() {
// Run controller functionality
var controller = createAdminFormController();
// Test scope value
expect(scope.myform).toEqualData(sampleForm);
});
it('$scope.removeCurrentForm() with valid form data should send a DELETE request with the id of form', inject(function($uibModal) {
var controller = createAdminFormController();
//Set $state transition
$state.expectTransitionTo('listForms');
// Set DELETE response
$httpBackend.expect('DELETE', /^(\/forms\/)([0-9a-fA-F]{24})$/).respond(200, sampleForm);
//Run controller functionality
scope.openDeleteModal();
scope.removeCurrentForm();
$httpBackend.flush();
$state.ensureAllTransitionsHappened();
// Test scope value
expect(scope.myform).toEqualData(sampleForm);
});
}));
it('$scope.update() should send a PUT request with the id of form', function() {
var controller = createAdminFormController();
it('$scope.removeCurrentForm() with valid form data should send a DELETE request with the id of form', inject(function($timeout, $uibModal) {
$timeout(function() {
var controller = createAdminFormController();
//Set PUT response
$httpBackend.expect('PUT', /^(\/forms\/)([0-9a-fA-F]{24})$/).respond(200, sampleForm);
//Set $state transition
$state.expectTransitionTo('listForms');
//Run controller functionality
scope.update(false, sampleForm, false, false);
// Set DELETE response
$httpBackend.expect('DELETE', /^(\/forms\/)([0-9a-fA-F]{24})$/).respond(200, sampleForm);
$httpBackend.flush();
});
//Run controller functionality
scope.openDeleteModal();
scope.removeCurrentForm();
it('$scope.openDeleteModal() should open scope.deleteModal', function() {
var controller = createAdminFormController();
$httpBackend.flush();
$state.ensureAllTransitionsHappened();
});
}));
//Run controller functionality
scope.openDeleteModal();
expect(scope.deleteModal.opened).toEqual(true);
});
it('$scope.update() should send a PUT request with the id of form', inject(function($timeout) {
$timeout(function() {
var controller = createAdminFormController();
it('$scope.cancelDeleteModal() should close $scope.deleteModal', inject(function($uibModal) {
var controller = createAdminFormController();
//Set PUT response
$httpBackend.expect('PUT', /^(\/forms\/)([0-9a-fA-F]{24})$/).respond(200, sampleForm);
//Run controller functionality
scope.openDeleteModal();
//Run controller functionality
scope.update(false, sampleForm, false, false);
//Run controller functionality
scope.cancelDeleteModal();
expect( scope.deleteModal.opened ).toEqual(false);
$httpBackend.flush();
});
}));
it('$scope.openDeleteModal() should open scope.deleteModal', inject(function($timeout) {
$timeout(function() {
var controller = createAdminFormController();
//Run controller functionality
scope.openDeleteModal();
expect(scope.deleteModal.opened).toEqual(true);
});
}));
it('$scope.cancelDeleteModal() should close $scope.deleteModal', inject(function($uibModal, $timeout) {
$timeout(function() {
var controller = createAdminFormController();
//Run controller functionality
scope.openDeleteModal();
//Run controller functionality
scope.cancelDeleteModal();
expect( scope.deleteModal.opened ).toEqual(false);
});
}));
});
}());

View File

@ -11,7 +11,7 @@ angular.module('users').controller('AuthenticationController', ['$scope', '$loca
var statesToIgnore = ['', 'home', 'signin', 'resendVerifyEmail', 'verify', 'signup', 'signup-success', 'forgot', 'reset-invalid', 'reset', 'reset-success'];
$scope.signin = function() {
if($scope.forms && $scope.forms.hasOwnProperty('siginForm') && !$scope.forms.signinForm.$invalid){
if($scope.forms && $scope.forms.signinForm.$valid){
User.login($scope.credentials).then(
function(response) {
Auth.login(response);
@ -40,7 +40,7 @@ angular.module('users').controller('AuthenticationController', ['$scope', '$loca
return;
}
if(!$scope.forms.signupForm.$invalid){
if($scope.forms && $scope.forms.signupForm.$valid){
User.signup($scope.credentials).then(
function(response) {
$state.go('signup-success');