added logicjump to forms

This commit is contained in:
David Baldwynn 2016-08-23 14:45:59 -07:00
parent de5ac881c9
commit 02e25ef739
26 changed files with 369 additions and 220 deletions

View file

@ -13,6 +13,7 @@ exports.index = function(req, res) {
};
exports.form = function(req, res) {
console.log('\n\n\nRENDERING FORM\n\n\n');
//Allow form to be embeded
res.removeHeader('X-Frame-Options');

View file

@ -69,74 +69,6 @@ exports.uploadPDF = function(req, res, next) {
}
};
/**
* Upload PDF
*/
/*
exports.uploadSubmissionFile = function(req, res, next) {
console.log('inside uploadPDF');
// console.log('\n\nProperty Descriptor\n-----------');
// console.log(Object.getOwnPropertyDescriptor(req.files.file, 'path'));
console.log(req.files);
if(req.files){
var file, _user, _path;
for(var i=0; i<req.files.length; i++){
file = req.files[i];
_user = req.user;
_path = file.path;
if (file.size === 0) {
return next(new Error('File uploaded is EMPTY'));
}else if(file.size > 100000000){
return next(new Error('File uploaded exceeds MAX SIZE of 100MB'));
}else {
fs.exists(_path, function(exists) {
//If file exists move to user's form directory
if(exists) {
var newDestination = config.tmpUploadPath+_user.username;
var stat = null;
try {
stat = fs.statSync(newDestination);
} catch (err) {
fs.mkdirSync(newDestination);
}
if (stat && !stat.isDirectory()) {
console.log('Directory cannot be created');
return next(new Error('Directory cannot be created because an inode of a different type exists at "' + newDestination + '"'));
}
console.log(path.join(newDestination, pdfFile.filename));
fs.move(pdfFile.path, path.join(newDestination, pdfFile.filename), function (err) {
if (err) {
return next(new Error(err.message));
}
pdfFile.path = path.join(newDestination, pdfFile.filename);
console.log(pdfFile.filename + ' uploaded to ' + pdfFile.path);
res.json(pdfFile);
});
} else {
return next(new Error('Did NOT get your file!'));
}
});
}
}
}else {
return next(new Error('Uploaded files were NOT detected'));
}
};
*/
/**
* Delete a forms submissions
*/
@ -311,6 +243,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++){
delete req.body.form.form_fields[i].input_type;
var field = req.body.form.form_fields[i];
if(!checkForValidId.exec(field._id+'')){
delete field._id;

View file

@ -7,7 +7,8 @@ var mongoose = require('mongoose'),
util = require('util'),
mUtilities = require('mongoose-utilities'),
_ = require('lodash'),
Schema = mongoose.Schema;
Schema = mongoose.Schema,
LogicJumpSchema = require('./logic_jump.server.model');
var FieldOptionSchema = new Schema({
option_id: {
@ -77,10 +78,7 @@ function BaseFieldSchema(){
default: ''
},
logicJump: {
type: Schema.Types.ObjectId,
ref: 'LogicJump'
},
logicJump: LogicJumpSchema,
ratingOptions: {
type: RatingFieldSchema,
@ -162,6 +160,7 @@ FormFieldSchema.pre('validate', function(next) {
if(this.ratingOptions && this.ratingOptions.steps && this.ratingOptions.shape){
error.errors.ratingOptions = new mongoose.Error.ValidatorError({path: 'ratingOptions', message: 'ratingOptions is only allowed for type \'rating\' fields.', type: 'notvalid', value: this.ratingOptions});
console.error(error);
return(next(error));
}
@ -183,8 +182,9 @@ FormFieldSchema.pre('validate', function(next) {
//If field is multiple choice check that it has field
if(this.fieldType !== 'dropdown' && this.fieldType !== 'radio' && this.fieldType !== 'checkbox'){
if(!this.fieldOptions || this.fieldOptions.length !== 0){
if(this.fieldOptions && this.fieldOptions.length > 0){
error.errors.ratingOptions = new mongoose.Error.ValidatorError({path:'fieldOptions', message: 'fieldOptions are only allowed for type dropdown, checkbox or radio fields.', type: 'notvalid', value: this.ratingOptions});
console.error(error);
return(next(error));
}
}
@ -192,13 +192,18 @@ FormFieldSchema.pre('validate', function(next) {
return next();
});
//LogicJump Save
FormFieldSchema.pre('save', function(next) {
if(this.logicJump && this.logicJump.fieldA){
if(this.logicJump.jumpTo = '') delete this.logicJump.jumpTo;
}
next();
});
//Submission fieldValue correction
FormFieldSchema.pre('save', function(next) {
if(this.fieldType === 'dropdown' && this.isSubmission){
//console.log(this);
this.fieldValue = this.fieldValue.option_value;
//console.log(this.fieldValue);
}
return next();

View file

@ -6,76 +6,108 @@
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
_ = require('lodash'),
math = require('math');
math = require('mathjs');
/**
* Calculate a 32 bit FNV-1a hash
* Found here: https://gist.github.com/vaiorabbit/5657561
* Ref.: http://isthe.com/chongo/tech/comp/fnv/
*
* @param {string} str the input value
* @param {boolean} [asString=false] set to true to return the hash value as
* 8-digit hex string instead of an integer
* @param {integer} [seed] optionally pass the hash of the previous chunk
* @returns {integer | string}
*/
function hashFnv32a(str, asString, seed) {
/*jshint bitwise:false */
var i, l,
hval = (seed === undefined) ? 0x811c9dc5 : seed;
var BooleanExpressionSchema = new Schema({
expressionString: {
type: String,
for (i = 0, l = str.length; i < l; i++) {
hval ^= str.charCodeAt(i);
hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
}
if( asString ){
// Convert to 8 digit hex string
return ("0000000" + (hval >>> 0).toString(16)).substr(-8);
}
return hval >>> 0;
}
var schemaOptions = {
toObject: {
virtuals: true
},
result: {
type: Boolean,
}
});
BooleanExpressionSchema.methods.evaluate = function(){
if(this.expressionString){
//Get headNode
var headNode = math.parse(this.expressionString);
var expressionScope = {};
var that = this;
//Create scope
headNode.traverse(function (node, path, parent) {
if(node.type === 'SymbolNode'){
mongoose.model('Field')
.findOne({_id: node.name}).exec(function(err, field){
if(err) {
console.log(err);
throw new Error(err);
}
if(!!_.parseInt(field.fieldValue)){
that.expressionScope[node.name] = _.parseInt(field.fieldValue);
}else {
that.expressionScope[node.name] = field.fieldValue;
}
console.log('_id: '+node.name);
console.log('value: '+that.expressionScope[node.name]);
});
}
});
var code = headNode.compile();
var result = code.eval(expressionScope);
this.result = result;
return result;
}else{
return null;
toJSON: {
virtuals: true
}
};
mongoose.model('BooleanExpression', BooleanExpressionSchema);
/**
* Form Schema
*/
var LogicJumpSchema = new Schema({
created: {
type: Date,
default: Date.now
expressionString: {
type: String,
enum: [
'a == b',
'a !== b'
]
},
lastModified: {
type: Date,
},
BooleanExpression: {
fieldA: {
type: Schema.Types.ObjectId,
ref: 'BooleanExpression'
ref: 'FormField'
},
valueB: {
type: Schema.Types.String
},
jumpTo: {
type: Schema.Types.ObjectId,
ref: 'FormField'
}
}, schemaOptions);
/*
IS EQUAL TO statement
var scope = {
a: val1,
b: val2
};
math.eval('a == b', scope);
IS NOT EQUAL TO statement
var scope = {
a: val1,
b: val2
};
math.eval('a !== b', scope);
BEGINS WITH statement
ENDS WITH statement
CONTAINS statement
DOES NOT CONTAIN statement
*/
LogicJumpSchema.virtual('result').get(function () {
if (this.expressionString && this.fieldA.fieldValue && this.valueB) {
var valA = hashFnv32a(String(this.fieldA.fieldValue));
var valB = hashFnv32a(String(this.valueB));
var scope = {
'a': valA,
'b': valB
};
return math.eval(this.expressionString, scope);
} else {
return null;
}
});
mongoose.model('LogicJump', LogicJumpSchema);
module.exports = LogicJumpSchema;

View file

@ -2,7 +2,7 @@
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>{{title}}</title>
<title>{{title}} Form</title>
<!-- General META -->
<meta charset="utf-8">

View file

@ -37,7 +37,8 @@
"angular-ui-select": "https://github.com/whitef0x0/ui-select.git#compiled",
"angular-translate": "~2.11.0",
"ng-device-detector": "~3.0.1",
"ng-translate": "*"
"ng-translate": "*",
"mathjs": "^3.4.1"
},
"resolutions": {
"angular-bootstrap": "^0.14.0",

View file

@ -72,6 +72,7 @@ module.exports = function(db) {
var subdomains = req.subdomains;
var host = req.hostname;
console.log(subdomains);
if(subdomains.slice(0, 4).join('.')+'' === '1.0.0.127'){
subdomains = subdomains.slice(4);
@ -81,18 +82,18 @@ module.exports = function(db) {
if (!subdomains.length) return next();
var urlPath = url.parse(req.url).path.split('/');
if(urlPath.indexOf('static')){
if(urlPath.indexOf('static') > -1){
urlPath.splice(1,1);
req.root = 'https://' + config.baseUrl + urlPath.join('/');
return next();
}
if(subdomains.indexOf('stage') || subdomains.indexOf('admin')){
if(subdomains.indexOf('stage') > -1 || subdomains.indexOf('admin') > -1){
return next();
}
User.findOne({username: req.subdomains.reverse()[0]}).exec(function (err, user) {
console.log(user);
if (err) {
console.log(err);
req.subdomains = null;
@ -117,6 +118,8 @@ module.exports = function(db) {
req.userId = user._id;
console.log('\n\n\ngot subdomain: '+ req.subdomains.reverse()[0]);
// Q.E.D.
next();
});

View file

@ -24,6 +24,7 @@
},
"dependencies": {
"async": "^1.4.2",
"async-boolean-expression-evaluator": "^1.1.1",
"aws-sdk": "^2.3.9",
"bcrypt": "^0.8.7",
"body-parser": "~1.14.1",
@ -64,6 +65,7 @@
"lodash": "^2.4.1",
"main-bower-files": "~2.9.0",
"math": "0.0.3",
"mathjs": "^3.4.1",
"method-override": "~2.3.0",
"mkdirp": "^0.5.1",
"mongoose": "~4.4.19",

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

@ -1,5 +1,32 @@
'use strict';
/**
* Calculate a 32 bit FNV-1a hash
* Found here: https://gist.github.com/vaiorabbit/5657561
* Ref.: http://isthe.com/chongo/tech/comp/fnv/
*
* @param {string} str the input value
* @param {boolean} [asString=false] set to true to return the hash value as
* 8-digit hex string instead of an integer
* @param {integer} [seed] optionally pass the hash of the previous chunk
* @returns {integer | string}
*/
function hashFnv32a(str, asString, seed) {
/*jshint bitwise:false */
var i, l,
hval = (seed === undefined) ? 0x811c9dc5 : seed;
for (i = 0, l = str.length; i < l; i++) {
hval ^= str.charCodeAt(i);
hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
}
if( asString ){
// Convert to 8 digit hex string
return ("0000000" + (hval >>> 0).toString(16)).substr(-8);
}
return hval >>> 0;
}
;
angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCounter', '$filter', '$rootScope', 'SendVisitorData',
function ($http, TimeCounter, $filter, $rootScope, SendVisitorData) {
@ -92,6 +119,20 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
/*
** Field Controls
*/
var evaluateLogicJump = function(field){
console.log('evaluateLogicJump');
var logicJump = field.logicJump;
if (logicJump.expressionString && logicJump.valueB) {
var valA = hashFnv32a(String(field.fieldValue));
var valB = hashFnv32a(String(logicJump.valueB));
var scope = {
'a': valA,
'b': valB
};
return math.eval(logicJump.expressionString, scope);
}
};
var getActiveField = function(){
if($scope.selected === null){
console.error('current active field is null');
@ -117,6 +158,15 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
$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++){
var currField = $scope.myform.visible_form_fields[i];
if(field_id == currField._id){
$scope.selected.index = i;
break;
}
}
}
var nb_valid = $filter('formValidity')($scope.myform);
$scope.translateAdvancementData = {
@ -159,20 +209,27 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
};
$rootScope.nextField = $scope.nextField = function(){
//console.log('nextfield');
//console.log($scope.selected.index);
//console.log($scope.myform.visible_form_fields.length-1);
var currField = $scope.myform.visible_form_fields[$scope.selected.index];
console.log(currField.logicJump);
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) {
//console.log('Second last element');
selected_index = $scope.selected.index+1;
selected_id = 'submit_field';
$rootScope.setActiveField(selected_id, selected_index, true);
}
}
}
};
$rootScope.prevField = $scope.prevField = function(){

View file

@ -51,7 +51,7 @@
style="padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)">
<button ng-disabled="!field.fieldValue || forms.myForm.{{field.fieldType}}{{$index}}.$invalid"
ng-style="{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}"
ng-click="$root.nextField()"
ng-click="nextField()"
class="btn col-sm-5 col-xs-5">
{{ 'OK' | translate }} <i class="fa fa-check"></i>

View file

@ -4,4 +4,4 @@
ApplicationConfiguration.registerModule('view-form', [
'ngFileUpload', 'ui.router.tabs', 'ui.date', 'ui.sortable',
'angular-input-stars', 'pascalprecht.translate'
]);//, 'colorpicker.module' @TODO reactivate this module
]);

View file

@ -39,6 +39,10 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
PREVIEW: 'Preview',
//Edit Form View
DISABLED: 'Disabled:',
YES: 'YES',
NO: 'NO',
ADD_LOGIC_JUMP: 'Add Logic Jump',
ADD_FIELD_LG: 'Click to Add New Field',
ADD_FIELD_MD: 'Add New Field',
ADD_FIELD_SM: 'Add Field',

View file

@ -10,7 +10,6 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
},
controller: function($scope){
console.log($scope.myform);
var field_ids = _($scope.myform.form_fields).pluck('_id');
for(var i=0; i<field_ids.length; i++){
$scope.myform.plugins.oscarhost.settings.fieldMap[field_ids[i]] = null;
@ -63,23 +62,23 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
//Populate local scope with rootScope methods/variables
$scope.update = $rootScope.update;
//Many-to-many Select for Mapping OscarhostFields -> FormFields
$scope.oscarFieldsLeft = function(field_id){
// LOGIC JUMP METHODS
$scope.removeLogicJump = function (field_index) {
var currField = $scope.myform.form_fields[field_index];
currField.logicJump = {};
};
if($scope.myform && $scope.myform.plugins.oscarhost.settings.validFields.length > 0){
if(!$scope.myform.plugins.oscarhost.settings.fieldMap) $scope.myform.plugins.oscarhost.settings.fieldMap = {};
$scope.addNewLogicJump = function (field_index) {
var form_fields = $scope.myform.form_fields;
var currField = form_fields[field_index];
console.log(currField);
if (form_fields.length > 1 && currField._id) {
var oscarhostFields = $scope.myform.plugins.oscarhost.settings.validFields;
var currentFields = _($scope.myform.plugins.oscarhost.settings.fieldMap).invert().keys().value();
if( $scope.myform.plugins.oscarhost.settings.fieldMap.hasOwnProperty(field_id) ){
currentFields = _(currentFields).difference($scope.myform.plugins.oscarhost.settings.fieldMap[field_id]);
var newLogicJump = {
fieldA: currField._id
};
currField.logicJump = newLogicJump;
}
//Get all oscarhostFields that haven't been mapped to a formfield
return _(oscarhostFields).difference(currentFields).value();
}
return [];
};
/*
@ -114,7 +113,8 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
fieldValue: '',
required: true,
disabled: false,
deletePreserved: false
deletePreserved: false,
logicJump: {}
};
if($scope.showAddOptions(newField)){
@ -126,7 +126,6 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
});
}
if(modifyForm){
//Add newField to form_fields array
$scope.myform.form_fields.push(newField);

View file

@ -249,7 +249,6 @@
</div>
</div>
<div class="row" ng-show="showRatingOptions(field)"><br></div>
<div class="row" ng-if="showRatingOptions(field)">
<div class="col-md-9 col-sm-9">{{ 'NUM_OF_STEPS' | translate }}</div>
@ -293,7 +292,7 @@
</div>
<div class="row">
<div class="col-md-4 col-xs-12 field-input">Disabled:</div>
<div class="col-md-4 col-xs-12 field-input">{{ 'DISABLED' | translate }}</div>
<div class="col-md-8 col-xs-12 field-input">
<label class="btn col-xs-5">
<input type="radio" ng-value="true"
@ -309,6 +308,60 @@
</div>
</div>
<div class="row" ng-if="myform.form_fields.length > 1 && myform.form_fields[$index].logicJump.fieldA">
<h4> Logic Jump </h4>
</div>
<div class="row" ng-if="myform.form_fields.length > 1 && myform.form_fields[$index].logicJump.fieldA">
<div class="col-md-4 col-xs-12 field-input">{{ 'ADD_LOGIC_JUMP' | translate }}</div>
<div class="col-md-8 col-xs-12 field-input">
<label class="btn col-xs-5">
<input type="radio" ng-checked="!!myform.form_fields[$index].logicJump.fieldA"
name="logicJumpYes{{field._id}}" ng-click="addNewLogicJump($index)"/>
<span> &nbsp; {{ 'YES' | translate }}</span>
</label>
<label class="btn col-xs-5 col-xs-offset-1">
<input type="radio" ng-checked="!myform.form_fields[$index].logicJump.fieldA"
name="logicJumpNo{{field._id}}" ng-click="removeLogicJump($index)"/>
<span> &nbsp; {{ 'NO' | translate }}</span>
</label>
</div>
</div>
<div class="row question" ng-if="myform.form_fields.length > 1 && myform.form_fields[$index].logicJump.fieldA">
<div class="col-md-4 col-sm-12">
<b> If this field </b>
</div>
<div class="col-md-4 col-sm-12">
<select style="width:100%" ng-model="field.logicJump.expressionString"
value="{{field.logicJump.expressionString}}"
name="logicjump_expressionString{{field._id}}">
<option value="a == b">
is equal to
</option>
<option value="a !== b">
is not equal to
</option>
</select>
</div>
<div class="col-md-4 col-sm-12">
<input type="text" ng-model="field.logicJump.valueB"/>
</div>
<div class="col-md-4 col-md-offset-2">
<b>Jumps to </b>
</div>
<div class="col-md-6 col-sm-12">
<select style="width:100%" ng-model="field.logicJump.jumpTo"
value="{{field.logicJump.jumpTo}}"
name="logicjump_jumpTo{{field._id}}">
<option ng-repeat="jump_field in myform.form_fields"
value="{{jump_field._id}}">
{{jump_field.title}}
</option>
</select>
</div>
</div>
</div>
</accordion-group>

0
public/modules/forms/base/bower.json Executable file → Normal file
View file

View file

View file

View file

View file

View file

View file