From 79dfbbc8a75b88adb5f1618ad2e3e8c03b9447f4 Mon Sep 17 00:00:00 2001 From: David Baldwynn Date: Tue, 21 Jul 2015 16:25:45 -0700 Subject: [PATCH] got table submissions to display correctly --- app/controllers/forms.server.controller.js | 64 +- .../users.authentication.server.controller.js | 109 +++- .../users/users.profile.server.controller.js | 8 + app/models/form.server.model.js | 183 ++++-- app/models/form_field.server.model.js | 16 +- app/models/form_submission.server.model.js | 7 +- app/models/user.server.model.js | 11 +- app/routes/forms.server.routes.js | 2 +- app/routes/users.server.routes.js | 4 + app/tests/form.server.model.test.js | 83 +++ app/tests/test.pdf | Bin 0 -> 56223 bytes .../verify-account-email.server.view.html | 18 + bower.json | 5 +- config/express.js | 19 + docs/example_formfields.json | 96 +++ docs/form_update_process.md | 22 + gruntfile.js | 16 +- karma.conf.js | 2 +- package.json | 7 +- public/application.js | 47 +- public/dist/application.js | 557 ++++++++++-------- public/dist/application.min.css | 2 +- public/dist/application.min.js | 3 +- .../forms/config/forms.client.config.js | 10 +- .../forms/config/forms.client.routes.js | 4 +- .../create-form.client.controller.js | 217 ++++--- .../view-form.client.controller.js | 232 ++++---- public/modules/forms/css/form.css | 22 +- .../directives/auto-save.client.directive.js | 39 +- .../configure-form.client.directive.js | 3 +- .../directives/edit-form.client.directive.js | 84 ++- .../directives/field-icon.client.directive.js | 28 +- .../directives/field.client.directive.js | 13 +- .../form-locator.client.directive.js | 2 +- .../forms/directives/form.client.directive.js | 4 + .../on-finish-render.client.directive.js | 2 +- public/modules/forms/forms.client.module.js | 2 +- .../forms/views/create-form.client.view.html | 231 -------- .../views/directiveViews/field/date.html | 2 +- .../views/directiveViews/field/dropdown.html | 8 +- .../views/directiveViews/field/radio.html | 2 +- .../directiveViews/form/configure-form.html | 2 +- .../views/directiveViews/form/edit-form.html | 38 +- .../forms/views/directiveViews/form/form.html | 2 +- .../forms/views/view-form.client.view.html | 6 +- .../users/config/users.client.routes.js | 2 +- .../authentication.client.controller.js | 9 +- public/modules/users/css/users.css | 4 +- .../services/authorizer.client.service.js | 34 ++ .../authentication.client.controller.test.js | 56 +- .../signup-success.client.view.html | 18 +- .../authentication/signup.client.view.html | 3 +- 52 files changed, 1417 insertions(+), 943 deletions(-) create mode 100644 app/tests/form.server.model.test.js create mode 100644 app/tests/test.pdf create mode 100644 app/views/templates/verify-account-email.server.view.html create mode 100644 docs/example_formfields.json create mode 100644 docs/form_update_process.md delete mode 100644 public/modules/forms/views/create-form.client.view.html create mode 100644 public/modules/users/services/authorizer.client.service.js diff --git a/app/controllers/forms.server.controller.js b/app/controllers/forms.server.controller.js index 87692c5b..6348c1c8 100644 --- a/app/controllers/forms.server.controller.js +++ b/app/controllers/forms.server.controller.js @@ -79,7 +79,6 @@ exports.createSubmission = function(req, res) { submission.form_fields = req.body.form_fields; submission.title = req.body.title; submission.timeElapsed = req.body.timeElapsed; - // console.log(req.body);s // submission.ipAddr = req.headers['x-forwarded-for'] || req.connection.remoteAddress; if(form.autofillPDFs){ @@ -126,8 +125,8 @@ exports.createSubmission = function(req, res) { message: errorHandler.getErrorMessage(err) }); } - console.log(results); - console.log(that.form_fields); + // console.log(results); + // console.log(that.form_fields); res.status(200).send('Form submission successfully saved'); }); }; @@ -137,21 +136,31 @@ exports.createSubmission = function(req, res) { */ exports.listSubmissions = function(req, res) { var _form = req.form; + var _user = req.user; + // console.log(_form); // if(_form.submissions.length){ // res.json(_form.submissions); // }else{ - FormSubmission.find({ form: req.form }).populate('admin', 'form').exec(function(err, _submissions) { + FormSubmission.find({ form: req.form, admin: _user }).populate('admin', 'form').exec(function(err, _submissions) { if (err) { console.log(err); res.status(400).send({ message: errorHandler.getErrorMessage(err) }); - } else { - // _form.submissions = _submissions; - _form.update({ $set : { submissions: _submissions }}); - res.status(200); } + + _form.update({ $set : { submissions: _submissions }}).exec(function(err, form){ + if (err) { + console.log(err); + res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + } + res.json(_submissions); + }); + // res.status(200).send('Updated forms'); + }); // } }; @@ -192,10 +201,21 @@ exports.read = function(req, res) { exports.update = function(req, res) { console.log('in form.update()'); - var form = req.form; - form = _.extend(form, req.body.form); - form.admin = req.user; + console.log(req.body.form.form_fields); + var form = req.form; + delete req.body.form.__v; + delete req.body.form._id; + delete req.body.form.created; + delete req.body.form.lastModified; + + //Unless we have 'admin' priviledges, updating form admin is disabled + if(req.user.roles.indexOf('admin') === -1) delete req.body.form.admin; + + form = _.extend(form, req.body.form); + // console.log(req.body.form); + // form.form_fields = req.body.form.form_fields; + form.save(function(err) { if (err) { console.log(err); @@ -267,28 +287,6 @@ exports.formByID = function(req, res, next, id) { }); } else { - if(!form.admin){ - form.admin = req.user; - form.save(function(err) { - if (err) { - console.log(err); - res.status(400).send({ - message: errorHandler.getErrorMessage(err) - }); - } else { - //Remove sensitive information from User object - form.admin.password = null; - form.admin.created = null; - form.admin.salt = null; - - req.form = form; - next(); - } - }); - } - - console.log(form.submissions); - //Remove sensitive information from User object form.admin.password = null; form.admin.created = null; diff --git a/app/controllers/users/users.authentication.server.controller.js b/app/controllers/users/users.authentication.server.controller.js index 6daa5fc9..b936ccda 100755 --- a/app/controllers/users/users.authentication.server.controller.js +++ b/app/controllers/users/users.authentication.server.controller.js @@ -7,8 +7,94 @@ var _ = require('lodash'), errorHandler = require('../errors.server.controller'), mongoose = require('mongoose'), passport = require('passport'), + async = require('async'), + config = require('../../../config/config'), + nodemailer = require('nodemailer'), + crypto = require('crypto'), User = mongoose.model('User'); +var smtpTransport = nodemailer.createTransport(config.mailer.options); + + +/** + * Reset password GET from email token + */ +exports.validateResetToken = function(req, res) { + User.findOne({ + resetPasswordToken: req.params.token, + resetPasswordExpires: { + $gt: Date.now() + } + }, function(err, user) { + if (!user) { + return res.redirect('/#!/password/reset/invalid'); + } + + res.redirect('/#!/password/reset/' + req.params.token); + }); +}; + +/** + * Send verification email + */ +var sendVerificationEmail = function(req, res, next) { + // Init Variables + var passwordDetails = req.body; + + async.waterfall([ + // Generate random token + function(done) { + crypto.randomBytes(20, function(err, buffer) { + var token = buffer.toString('hex'); + done(err, token); + }); + }, + function(token, done) { + // For security measurement we remove the roles from the req.body object + delete req.body.roles; + + // Init Variables + var user = new User(req.body); + user.resetPasswordToken = token; + user.resetPasswordExpires = Date.now() + 3600000; // 1 hour + + // Add missing user fields + user.provider = 'local'; + user.displayName = user.firstName + ' ' + user.lastName; + + // Then save the user + user.save(function(err) { + done(err, token, user); + }); + }, + function(token, user, done) { + res.render('templates/verify-account-email', { + name: user.displayName, + url: 'http://' + req.headers.host + '/auth/activate/' + token, + appName: config.app.title + }, function(err, emailHTML) { + done(err, emailHTML, user); + }); + }, + // If valid email, send reset email using service + function(emailHTML, user, done) { + var mailOptions = { + to: user.email, + from: config.mailer.from, + subject: 'Please verify your email', + html: emailHTML + }; + + smtpTransport.sendMail(mailOptions, function(err) { + done(err, 'done'); + }); + } + ], function(err) { + if (err) return next(err); + res.status(200).send('new user successfully registered'); + }); +}; + /** * Signup */ @@ -34,14 +120,15 @@ exports.signup = function(req, res) { // Remove sensitive data before login user.password = undefined; user.salt = undefined; + res.status(200).send('new user successfully registered'); - req.login(user, function(err) { - if (err) { - res.status(400).send(err); - } else { - res.status(200).send('user successfully loggedin'); - } - }); + // req.login(user, function(err) { + // if (err) { + // res.status(400).send(err); + // } else { + // res.status(200).send('new user successfully registered'); + // } + // }); } }); }; @@ -60,7 +147,9 @@ exports.signin = function(req, res, next) { req.login(user, function(err) { if (err) { - res.status(400).send(err); + return res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); } else { res.json(user); } @@ -74,8 +163,8 @@ exports.signin = function(req, res, next) { */ exports.signout = function(req, res) { req.logout(); - // res.status(200).send('user successfully logged out'); - res.redirect('/'); + res.status(200).send('user successfully logged out'); + // res.redirect('/'); }; /** diff --git a/app/controllers/users/users.profile.server.controller.js b/app/controllers/users/users.profile.server.controller.js index 8e438f7c..e9d53509 100755 --- a/app/controllers/users/users.profile.server.controller.js +++ b/app/controllers/users/users.profile.server.controller.js @@ -52,5 +52,13 @@ exports.update = function(req, res) { * Send User */ exports.me = function(req, res) { + var _user = req.user; + delete _user.password; + delete _user.salt; + delete _user.provider; + delete _user.__v; + delete _user.created; + + res.json(req.user || null); }; diff --git a/app/models/form.server.model.js b/app/models/form.server.model.js index 80db6da0..c31b74a4 100644 --- a/app/models/form.server.model.js +++ b/app/models/form.server.model.js @@ -5,14 +5,16 @@ */ var mongoose = require('mongoose'), FieldSchema = require('./form_field.server.model.js'), + FormSubmissionSchema = require('./form_submission.server.model.js'), Schema = mongoose.Schema, pdfFiller = require('pdffiller'), _ = require('lodash'), config = require('../../config/config'), path = require('path'), fs = require('fs-extra'), - async = require('async'), - Field = mongoose.model('Field', FieldSchema); + async = require('async'); + var Field = mongoose.model('Field', FieldSchema); + var FormSubmission = mongoose.model('FormSubmission', FormSubmissionSchema); /** @@ -44,7 +46,7 @@ var FormSchema = new Schema({ default: '', }, form_fields: { - type: [Schema.Types.Mixed], + type: [FieldSchema], set: function(form_fields) { this._previousFormFields = this.form_fields; return form_fields; @@ -58,7 +60,8 @@ var FormSchema = new Schema({ admin: { type: Schema.Types.ObjectId, - ref: 'User' + ref: 'User', + required: 'Form must have an Admin' }, pdf: { @@ -67,6 +70,7 @@ var FormSchema = new Schema({ pdfFieldMap: { type: Schema.Types.Mixed }, + hideFooter: { type: Boolean, default: false, @@ -85,6 +89,10 @@ var FormSchema = new Schema({ }, }); +FormSchema.post('init', function() { + this._original = this.toObject(); +}); + //Delete template PDF of current Form FormSchema.pre('remove', function (next) { if(this.pdf){ @@ -109,6 +117,7 @@ FormSchema.pre('save', function (next) { //Concatenate submission and form's form_fields // FormSchema.pre('save', function (next) { // if(this.isModified('form_fields')){ + // if(this.submissions.length){ // for(var i=0; i 0){ + for(var i = 0; i < needle.length; i++){ + if(haystack.indexOf(needle[i]) <= -1){ + deletedIndexes.push(i); + } + } + } + return deletedIndexes; +} + + +FormSchema.pre('save', function (next) { + if(this.isModified('form_fields')){ + + var old_form_fields = this._original.form_fields, + old_ids = _.pluck(this.form_fields, '_id'), + new_ids = _.pluck(old_form_fields, '_id'), + deletedIds = getDeletedIndexes(old_ids, new_ids), + that = this; + + // console.log(deletedIds); + // console.log('old_ids\n--------'); + // console.log(old_ids); + // console.log('new_ids\n--------'); + // console.log(new_ids); + + //Preserve fields that have at least one submission + if( deletedIds.length > 0 ){ + + var modifiedSubmissions; + + async.forEachOfSeries(deletedIds, function (deletedIdIndex, key, callback) { + + var deleted_id = old_ids[deletedIdIndex]; + + //Search for submissions with deleted form_field + FormSubmission. + find({ form: that, admin: that.admin, form_fields: {$elemMatch: {_id: deleted_id} } }). + exec(function(err, submissions){ + if(err){ + console.error(err); + return callback(err); + } + + //Delete field if there are no submission(s) found + if(submissions.length > 0) { + //Push old form_field to start of array + that.form_fields.unshift(old_form_fields[deletedIdIndex]); + modifiedSubmissions.push.apply(modifiedSubmissions, submissions); + console.log(modifiedSubmissions); + } + + callback(null, modifiedSubmissions); + } + ); + }, function (err, submissions) { + if(err){ + console.error(err.message); + next(err); + } + + console.log('preserved deleted fields'); + console.log(submissions); + + async.forEachOfSeries(modifiedSubmissions, function (submission, key, callback) { + + for(var i = 0; i < deletedIds.length; i++){ + var tmpField = _.find(submission.form_fields, function(field){ + return field._id === deletedIds[i]; + }); + + var index = submission.form_fields.indexOf(tmpField); + + if(tmpField){ + //Delete old form_field + submission.form_fields.splice(index, 1); + + //Move old form_field to start + submission.form_fields.unshift(tmpField); + } + } + + submission.save(function (err) { + if (err) callback(err); + callback(); + }); + + }, function (err) { + if(err){ + console.error(err.message); + next(err); + } + + next(); + }); + + }); + } + } + + next(); + +}); + //Move PDF to permanent location after new template is uploaded FormSchema.pre('save', function (next) { @@ -129,7 +246,6 @@ FormSchema.pre('save', function (next) { async.series([ function(callback){ if(that.isModified('pdf')){ - // console.log('about to move PDF'); var new_filename = that.title.replace(/ /g,'')+'_template.pdf'; @@ -142,26 +258,21 @@ FormSchema.pre('save', function (next) { fs.mkdirSync(newDestination); } if (stat && !stat.isDirectory()) { - // console.log('Directory '+newDestination+' cannot be created'); - callback( new Error('Directory cannot be created because an inode of a different type exists at "' + config.pdfUploadPath + '"') ); + callback( new Error('Directory cannot be created because an inode of a different type exists at "' + config.pdfUploadPath + '"'), null); } - // console.log('about to move PDF'); - fs.move(that.pdf.path, path.join(newDestination, new_filename), function (err) { if (err) { console.error(err); - callback( new Error(err.message) ); + callback( new Error(err.message), null); } that.pdf.path = path.join(newDestination, new_filename); that.pdf.name = new_filename; - // console.log('\n\n PDF file:'+that.pdf.name+' successfully moved to: '+that.pdf.path); - callback(null,'task1'); }); } - callback(null,null); + callback(null,'task1'); }, function(callback){ if(that.isGenerated){ @@ -182,9 +293,9 @@ FormSchema.pre('save', function (next) { pdfFiller.generateFieldJson(that.pdf.path, function(err, _form_fields){ if(err){ - next( new Error(err.message), null); - }else if(!_form_fields.length){ - next( new Error('Generated formfields is empty'), null); + callback( new Error(err.message), null); + }else if(!_form_fields.length || _form_fields === undefined || _form_fields === null){ + callback( new Error('Generated formfields is empty'), null); } //Map PDF field names to FormField field names @@ -196,21 +307,8 @@ FormSchema.pre('save', function (next) { field.fieldType = _typeConvMap[ field.fieldType+'' ]; } - field.fieldValue = ''; - field.created = Date.now(); - field.required = true; - field.disabled = false; - - // field = new Field(field); - // field.save(function(err) { - // if (err) { - // console.error(err.message); - // throw new Error(err.message); - // }); - // } else { - // _form_fields[i] = that; - // } - // }); + field = new Field(field); + field.required = false; _form_fields[i] = field; } @@ -310,28 +408,7 @@ FormSchema.pre('save', function (next) { // next(); // }); -// FormSchema.methods.generateSubmissionsCSV = function (cb) { -// if(this.submissions.length){ -// submissions = this.submissions -// }else{ -// submissions = -// } - - -// _values.forEach(function(val){ -// if(val === true){ -// val = 'Yes'; -// }else if(val === false) { -// val = 'Off'; -// } -// }); - -// var jsonObj = _.zipObject(_keys, _values); - -// return jsonObj; -// }; - -FormSchema.methods.generateFDFTemplate = function (cb) { +FormSchema.methods.generateFDFTemplate = function() { var _keys = _.pluck(this.form_fields, 'title'), _values = _.pluck(this.form_fields, 'fieldValue'); diff --git a/app/models/form_field.server.model.js b/app/models/form_field.server.model.js index 6307d23d..796b3293 100644 --- a/app/models/form_field.server.model.js +++ b/app/models/form_field.server.model.js @@ -8,7 +8,7 @@ var mongoose = require('mongoose'), // questionType Validation function validateFormFieldType(value) { - if (!value || typeof myVar !== 'string' ) { return false; } + if (!value) { return false; } var validTypes = [ 'textfield', @@ -17,20 +17,16 @@ function validateFormFieldType(value) { 'legal', 'url', 'number', - 'textarea', 'statement', 'welcome', 'thankyou', - 'file', - 'dropdown', 'scale', 'rating', 'radio', 'checkbox', - 'hidden', ]; @@ -49,7 +45,11 @@ var FormFieldSchema = new Schema({ type: Date, default: Date.now }, - name: { + lastModified: { + type: Date, + default: Date.now + }, + title: { type: String, default: '', trim: true, @@ -60,7 +60,7 @@ var FormFieldSchema = new Schema({ default: '', }, fieldOptions: [{ - type: String + type: Schema.Types.Mixed }], required: { type: Boolean, @@ -75,7 +75,7 @@ var FormFieldSchema = new Schema({ required: 'Field type cannot be blank', validate: [validateFormFieldType, 'Invalid field type'] }, - value: Schema.Types.Mixed + fieldValue: Schema.Types.Mixed }); diff --git a/app/models/form_submission.server.model.js b/app/models/form_submission.server.model.js index 80058e52..b9336db2 100644 --- a/app/models/form_submission.server.model.js +++ b/app/models/form_submission.server.model.js @@ -10,9 +10,6 @@ var mongoose = require('mongoose'), _ = require('lodash'), config = require('../../config/config'), path = require('path'), - Form = mongoose.model('Form'), - FieldSchema = require('./form_field.server.model.js'), - Field = mongoose.model('Field', FieldSchema), fs = require('fs-extra'); /** @@ -119,4 +116,6 @@ FormSubmissionSchema.pre('save', function (next) { }); -mongoose.model('FormSubmission', FormSubmissionSchema); +module.exports = FormSubmissionSchema; + +// mongoose.model('FormSubmission', FormSubmissionSchema); diff --git a/app/models/user.server.model.js b/app/models/user.server.model.js index 4a612fdd..2ae755af 100755 --- a/app/models/user.server.model.js +++ b/app/models/user.server.model.js @@ -74,7 +74,7 @@ var UserSchema = new Schema({ roles: { type: [{ type: String, - enum: ['user', 'admin'] + enum: ['user', 'admin', 'superuser'] }], default: ['user'] }, @@ -91,6 +91,15 @@ var UserSchema = new Schema({ type: Date, default: Date.now }, + + /* For account activation */ + activationToken: { + type: String + }, + activationTokenExpires: { + type: Date + }, + /* For reset password */ resetPasswordToken: { type: String diff --git a/app/routes/forms.server.routes.js b/app/routes/forms.server.routes.js index ef80ccb6..36a2da37 100644 --- a/app/routes/forms.server.routes.js +++ b/app/routes/forms.server.routes.js @@ -22,7 +22,7 @@ module.exports = function(app) { .delete(users.requiresLogin, forms.hasAuthorization, forms.delete); app.route('/forms/:formId([a-zA-Z0-9]+)/submissions') - .get(forms.listSubmissions); + .get(users.requiresLogin, forms.hasAuthorization, forms.listSubmissions); // Finish by binding the form middleware app.param('formId', forms.formByID); diff --git a/app/routes/users.server.routes.js b/app/routes/users.server.routes.js index 01925126..48fb4d6a 100755 --- a/app/routes/users.server.routes.js +++ b/app/routes/users.server.routes.js @@ -14,6 +14,10 @@ module.exports = function(app) { app.route('/users').put(users.requiresLogin, users.update); app.route('/users/accounts').delete(users.requiresLogin, users.removeOAuthProvider); + // Account activation reset token + app.route('/auth/reset/:token').get(users.validateResetToken); + app.route('/auth/reset/:token').post(users.reset); + // Setting up the users password api app.route('/users/password').post(users.requiresLogin, users.changePassword); app.route('/auth/forgot').post(users.forgot); diff --git a/app/tests/form.server.model.test.js b/app/tests/form.server.model.test.js new file mode 100644 index 00000000..afc1263d --- /dev/null +++ b/app/tests/form.server.model.test.js @@ -0,0 +1,83 @@ +'use strict'; + +/** + * Module dependencies. + */ +var should = require('should'), + mongoose = require('mongoose'), + User = mongoose.model('User'), + Form = mongoose.model('Form'); + +/** + * Globals + */ +var user, Form, +FormFDF; + +/** + * Unit tests + */ +describe('Form Model Unit Tests:', function() { + beforeEach(function(done) { + user = new User({ + firstName: 'Full', + lastName: 'Name', + displayName: 'Full Name', + email: 'test@test.com', + username: 'username', + password: 'password' + }); + + user.save(function() { + Form = new Form({ + title: 'Form Title', + admin: user, + form_fields: [{'title':'Short Text2','fieldType':'textfield','fieldValue':'','disabled':false},{'disabled':false,'created':1435952663586,'fieldValue':'','fieldFlags':'0','fieldType':'checkbox','title':'nascar'},{'disabled':false,'created':1435952663586,'fieldValue':'','fieldFlags':'0','fieldType':'checkbox','title':'hockey'}] + }); + + done(); + }); + }); + + describe('Method Save', function() { + it('should be able to save without problems', function(done) { + return Form.save(function(err) { + should.not.exist(err); + done(); + }); + }); + + it('should be able to show an error when try to save without title', function(done) { + Form.title = ''; + + return Form.save(function(err) { + should.exist(err); + done(); + }); + }); + }); + + describe('Method generateFDFTemplate', function() { + it('should be able to generate one that is correct', function(done) { + return Form.generateFDFTemplate(function(err) { + should.not.exist(err); + done(); + }); + }); + + it('should be able to show an error when try to save without title', function(done) { + Form.title = ''; + + return Form.save(function(err) { + should.exist(err); + done(); + }); + }); + }); + + afterEach(function(done) { + Form.remove().exec(function() { + User.remove().exec(done); + }); + }); +}); diff --git a/app/tests/test.pdf b/app/tests/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e090513d8a95f3bb10df1340dd522f5d2a496e27 GIT binary patch literal 56223 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

lUAaR*VDmssWt%l!&^7Bx|IAu=ip-n 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 + + + + +

Dear {{name}},

+
+

+ You have just created a new account at {{appName}}. To use your account you must activate it.s +

+

Please visit this url to activate your account:

+

{{url}}

+ If you didn't make this request, you can ignore this email. +
+
+

The {{appName}} Support Team

+ + diff --git a/bower.json b/bower.json index 12abd202..618c184b 100755 --- a/bower.json +++ b/bower.json @@ -23,7 +23,10 @@ "restangular": "~1.5.1", "fontawesome": "~4.3.0", "ng-file-upload": "~5.0.9", - "angular-raven": "~0.5.11" + "angular-raven": "~0.5.11", + "angular-ui-date": "~0.0.8", + "lodash": "~3.10.0", + "angular-ui-sortable": "~0.13.4" }, "resolutions": { "angular": "^1.2.21", diff --git a/config/express.js b/config/express.js index aa8681e8..445d75b7 100755 --- a/config/express.js +++ b/config/express.js @@ -150,6 +150,25 @@ module.exports = function(db) { require(path.resolve(routePath))(app); }); + // Add headers for Sentry +app.use(function (req, res, next) { + + // Website you wish to allow to connect + res.setHeader('Access-Control-Allow-Origin', 'http://sentry.polydaic.com'); + + // Request methods you wish to allow + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); + + // Request headers you wish to allow + res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); + + // Set to true if you need the website to include cookies in the requests sent + // to the API (e.g. in case you use sessions) + res.setHeader('Access-Control-Allow-Credentials', true); + + // Pass to next layer of middleware + next(); + }); // Sentry (Raven) middleware app.use(raven.middleware.express.requestHandler(config.DSN)); diff --git a/docs/example_formfields.json b/docs/example_formfields.json new file mode 100644 index 00000000..2cb6b475 --- /dev/null +++ b/docs/example_formfields.json @@ -0,0 +1,96 @@ + [ + [{ + "fieldType": "textfield", + "fieldValue": "snthsnth", + "_id": "55aec5d284bae1a1996210bd", + "disabled": false, + "fieldOptions": [], + "description": "", + "title": "Short Text2", + "lastModified": "2015-07-21T22:21:06.653Z", + "created": "2015-07-21T22:21:06.653Z", + "$$hashKey": "02J" + }, { + "fieldType": "textfield", + "fieldValue": "duieedi", + "_id": "55aec5b084bae1a1996210b4", + "disabled": false, + "fieldOptions": [], + "description": "", + "title": "Last Name", + "lastModified": "2015-07-21T22:20:32.053Z", + "created": "2015-07-21T22:20:32.053Z", + "$$hashKey": "02K" + }], + + [{ + "fieldType": "textfield", + "fieldValue": "snthsnth", + "_id": "55aec5d284bae1a1996210bd", + "disabled": false, + "fieldOptions": [], + "description": "", + "title": "Short Text2", + "lastModified": "2015-07-21T22:21:06.653Z", + "created": "2015-07-21T22:21:06.653Z", + "$$hashKey": "02J" + }, { + "fieldType": "textfield", + "fieldValue": "duieedi", + "_id": "55aec5b084bae1a1996210b4", + "disabled": false, + "fieldOptions": [], + "description": "", + "title": "Last Name", + "lastModified": "2015-07-21T22:20:32.053Z", + "created": "2015-07-21T22:20:32.053Z", + "$$hashKey": "02K" + }], + [{ + "fieldType": "textfield", + "fieldValue": "snthsnth", + "_id": "55aec5d284bae1a1996210bd", + "disabled": false, + "fieldOptions": [], + "description": "", + "title": "Short Text2", + "lastModified": "2015-07-21T22:21:06.653Z", + "created": "2015-07-21T22:21:06.653Z", + "$$hashKey": "02J" + }, { + "fieldType": "textfield", + "fieldValue": "duieedi", + "_id": "55aec5b084bae1a1996210b4", + "disabled": false, + "fieldOptions": [], + "description": "", + "title": "Last Name", + "lastModified": "2015-07-21T22:20:32.053Z", + "created": "2015-07-21T22:20:32.053Z", + "$$hashKey": "02K" + }], + [{ + "fieldType": "textfield", + "fieldValue": "snthsnth", + "_id": "55aec5d284bae1a1996210bd", + "disabled": false, + "fieldOptions": [], + "description": "", + "title": "Short Text2", + "lastModified": "2015-07-21T22:21:06.653Z", + "created": "2015-07-21T22:21:06.653Z", + "$$hashKey": "02J" + }, { + "fieldType": "textfield", + "fieldValue": "duieedi", + "_id": "55aec5b084bae1a1996210b4", + "disabled": false, + "fieldOptions": [], + "description": "", + "title": "Last Name", + "lastModified": "2015-07-21T22:20:32.053Z", + "created": "2015-07-21T22:20:32.053Z", + "$$hashKey": "02K" + }] + + ] \ No newline at end of file diff --git a/docs/form_update_process.md b/docs/form_update_process.md new file mode 100644 index 00000000..37132b36 --- /dev/null +++ b/docs/form_update_process.md @@ -0,0 +1,22 @@ +##First Form Auto-Update + +1. Added field CLIENT +time: 2553 + +2. Finding form SERVER +time: 2841 + +3. Update form CLIENT +time: 2870 + +4.Updated form SERVER +time: 2863 + + +##Second Form Auto-Update + +1. Added field CLIENT +time: 2755 + +2. Finding form SERVER +time: 2898 \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js index dc5b6bcf..e4e7b0c4 100755 --- a/gruntfile.js +++ b/gruntfile.js @@ -114,13 +114,13 @@ module.exports = function(grunt) { } } }, - ngAnnotate: { - production: { - files: { - 'public/dist/application.js': '<%= applicationJavaScriptFiles %>' - } - } - }, + // ngAnnotate: { + // production: { + // files: { + // 'public/dist/application.js': '<%= applicationJavaScriptFiles %>' + // } + // } + // }, concurrent: { default: ['nodemon', 'watch'], debug: ['nodemon', 'watch', 'node-inspector'], @@ -179,7 +179,7 @@ module.exports = function(grunt) { grunt.registerTask('lint', ['newer:jshint', 'newer:csslint']); // Build task(s). - grunt.registerTask('build', ['lint', 'loadConfig', 'ngAnnotate', 'uglify', 'cssmin']); + grunt.registerTask('build', ['lint', 'loadConfig', 'uglify', 'cssmin']); //'ngAnnotate', ]); // Test task. grunt.registerTask('test', ['test:server', 'test:client']); diff --git a/karma.conf.js b/karma.conf.js index 0f5ab311..f20106f0 100755 --- a/karma.conf.js +++ b/karma.conf.js @@ -39,7 +39,7 @@ module.exports = function(config) { // - Safari (only Mac) // - PhantomJS // - IE (only Windows) - browsers: ['PhantomJS'], + browsers: ['Chrome'], // If browser does not capture in given timeout [ms], kill it captureTimeout: 60000, diff --git a/package.json b/package.json index fe0d4e68..32e885eb 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "postinstall": "bower install --config.interactive=false; grunt build" }, "dependencies": { - "async": "~0.9.0", + "async": "^1.4.0", "body-parser": "~1.9.0", "bower": "~1.3.8", "chalk": "~1.0.0", @@ -63,6 +63,7 @@ "method-override": "~2.3.0", "mocha": ">=1.20.0", "mongoose": "~3.8.8", + "mongoose-datatable": "^1.0.2", "morgan": "~1.4.1", "multer": "~0.1.8", "nodemailer": "~1.3.0", @@ -80,5 +81,9 @@ "supertest": "~0.14.0", "swig": "~1.4.1", "then-fs": "~2.0.0" + }, + "devDependencies": { + "karma-chrome-launcher": "^0.1.12", + "karma-jasmine": "^0.2.3" } } diff --git a/public/application.js b/public/application.js index 5f5b4d4d..3e394fce 100755 --- a/public/application.js +++ b/public/application.js @@ -9,20 +9,34 @@ angular.module(ApplicationConfiguration.applicationModuleName).config(['$locatio $locationProvider.hashPrefix('!'); } ]); -angular.module(ApplicationConfiguration.applicationModuleName).run(['$rootScope', '$state', '$stateParams', - function($rootScope, $state, $stateParams) { + +//Permission Constants +angular.module(ApplicationConfiguration.applicationModuleName).constant('APP_PERMISSIONS', { + viewAdminSettings: 'viewAdminSettings', + editAdminSettings: 'editAdminSettings', + editForm: 'editForm', + viewPrivateForm: 'viewPrivateForm', +}); +//User Role constants +angular.module(ApplicationConfiguration.applicationModuleName).constant('USER_ROLES', { + admin: 'admin', + normal: 'user', + superuser: 'superuser', +}); + +angular.module(ApplicationConfiguration.applicationModuleName).run(['$rootScope', 'Auth', '$state', '$stateParams', + function($rootScope, Auth, $state, $stateParams) { $rootScope.$state = $state; $rootScope.$stateParams = $stateParams; // add previous state property $rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState) { - console.log(fromState); $state.previous = fromState; //Redirect home to listForms if user is authenticated if(toState.name === 'home'){ - if($rootScope.authentication.isAuthenticated()){ + if(Auth.isAuthenticated()){ event.preventDefault(); // stop current execution $state.go('listForms'); // go to login } @@ -32,6 +46,31 @@ angular.module(ApplicationConfiguration.applicationModuleName).run(['$rootScope' } ]); +//Page access/authorization logic +angular.module(ApplicationConfiguration.applicationModuleName).run(['$rootScope', 'Auth', 'User', 'Authorizer', '$state', '$stateParams', + function($rootScope, Auth, User, Authorizer, $state, $stateParams) { + $rootScope.$on('$stateChangeStart', function(event, next) { + var authenticator, permissions, user; + permissions = next && next.data && next.data.permissions ? next.data.permissions : null; + + Auth.ensureHasCurrentUser(User); + user = Auth.currentUser; + + if(user){ + authenticator = new Authorizer(user); + + // console.log('Permissions'); + // console.log(permissions); + + if( (permissions !== null) && !authenticator.canAccess(permissions) ){ + event.preventDefault(); + console.log('access denied') + $state.go('access_denied'); + } + } + }); +}]); + //Then define the init function for starting up the application angular.element(document).ready(function() { //Fixing facebook bug with redirect diff --git a/public/dist/application.js b/public/dist/application.js index 9598b6ff..ee37a1cb 100644 --- a/public/dist/application.js +++ b/public/dist/application.js @@ -32,28 +32,66 @@ angular.module(ApplicationConfiguration.applicationModuleName).config(['$locatio $locationProvider.hashPrefix('!'); } ]); -angular.module(ApplicationConfiguration.applicationModuleName).run(['$rootScope', '$state', '$stateParams', - function($rootScope, $state, $stateParams) { - $rootScope.$state = $state; - $rootScope.$stateParams = $stateParams; +//Permission Constants +angular.module(ApplicationConfiguration.applicationModuleName).constant('APP_PERMISSIONS', { + viewAdminSettings: 'viewAdminSettings', + editAdminSettings: 'editAdminSettings', + editForm: 'editForm', + viewPrivateForm: 'viewPrivateForm', +}); +//User Role constants +angular.module(ApplicationConfiguration.applicationModuleName).constant('USER_ROLES', { + admin: 'admin', + normal: 'user', + superuser: 'superuser', +}); - // add previous state property - $rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState) { - console.log(fromState); - $state.previous = fromState; +// angular.module(ApplicationConfiguration.applicationModuleName).run(['$rootScope', 'Auth', '$state', '$stateParams', +// function($rootScope, Auth, $state, $stateParams) { - //Redirect home to listForms if user is authenticated - if(toState.name === 'home'){ - if($rootScope.authentication.isAuthenticated()){ - event.preventDefault(); // stop current execution - $state.go('listForms'); // go to login - } - } - }); +// $rootScope.$state = $state; +// $rootScope.$stateParams = $stateParams; - } -]); +// // add previous state property +// $rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState) { +// // console.log(fromState); +// $state.previous = fromState; + +// //Redirect home to listForms if user is authenticated +// if(toState.name === 'home'){ +// if(Auth.isAuthenticated()){ +// event.preventDefault(); // stop current execution +// $state.go('listForms'); // go to login +// } +// } +// }); + +// } +// ]); + +angular.module(ApplicationConfiguration.applicationModuleName).run(['$rootScope', 'Auth', 'User', 'Authorizer', '$state', '$stateParams', + function($rootScope, Auth, User, Authorizer, $state, $stateParams) { + $rootScope.$on('$stateChangeStart', function(event, next) { + var authenticator, permissions, user; + permissions = next && next.data ? next.data.permissions : null; + + Auth.ensureCurrentUser(User); + user = Auth.currentUser; + + // if(user){} + authenticator = new Authorizer(user); + + if( (permissions !== null) && !authenticator.canAccess(permissions) ){ + event.preventDefault(); + if (!user) { + $state.go('sigin'); + } else { + $state.go('access_denied'); + } + } + }); +}]); //Then define the init function for starting up the application angular.element(document).ready(function() { @@ -71,7 +109,7 @@ ApplicationConfiguration.registerModule('core', ['users']); 'use strict'; // Use Application configuration module to register a new module -ApplicationConfiguration.registerModule('forms', ['ngFileUpload', 'users']); +ApplicationConfiguration.registerModule('forms', ['ngFileUpload', 'ui.date', 'users']); 'use strict'; // Use Application configuration module to register a new module @@ -454,131 +492,128 @@ angular.module('forms').config(['$stateProvider', }); } ]); -'use strict'; +// 'use strict'; -angular.module('forms').controller('EditFormController', ['$scope', '$state', '$rootScope', 'Upload', '$stateParams', 'FormFields', 'Forms', 'CurrentForm', '$modal', '$location', '$http', - function ($scope, $state, $rootScope, Upload, $stateParams, FormFields, Forms, CurrentForm, $modal, $location, $http) { - $scope.form = {}; - $scope.isNewForm = false; - $scope.log = ''; - $scope.pdfLoading = false; - var _current_upload = null; +// angular.module('forms').controller('EditFormController', ['$scope', '$state', '$rootScope', 'Upload', '$stateParams', 'FormFields', 'Forms', 'CurrentForm', '$modal', '$location', '$http', +// function ($scope, $state, $rootScope, Upload, $stateParams, FormFields, Forms, CurrentForm, $modal, $location, $http) { +// $scope.form = {}; +// $scope.isNewForm = false; +// $scope.log = ''; +// $scope.pdfLoading = false; +// var _current_upload = null; - // Get current form if it exists, or create new one - if($stateParams.formId){ - Forms.get({ formId: $stateParams.formId}, function(form){ - $scope.form = angular.fromJson(angular.toJson(form)); - console.log($scope.form); - }); - } else { - $scope.form.form_fields = []; - $scope.isNewForm = true; - } +// // Get current form if it exists, or create new one +// if($stateParams.formId){ +// Forms.get({ formId: $stateParams.formId}, function(form){ +// $scope.form = angular.fromJson(angular.toJson(form)); +// console.log($scope.form); +// }); +// } else { +// $scope.form.form_fields = []; +// $scope.isNewForm = true; +// } - //PDF Functions - $scope.cancelUpload = function(){ - _current_upload.abort(); - $scope.pdfLoading = false; - $scope.removePDF(); - }; +// //PDF Functions +// $scope.cancelUpload = function(){ +// _current_upload.abort(); +// $scope.pdfLoading = false; +// $scope.removePDF(); +// }; - $scope.removePDF = function(){ - $scope.form.pdf = null; - $scope.form.isGenerated = false; - $scope.form.autofillPDFs = false; +// $scope.removePDF = function(){ +// $scope.form.pdf = null; +// $scope.form.isGenerated = false; +// $scope.form.autofillPDFs = false; - console.log('form.pdf: '+$scope.form.pdf+' REMOVED'); - }; +// console.log('form.pdf: '+$scope.form.pdf+' REMOVED'); +// }; - $scope.uploadPDF = function(files) { +// $scope.uploadPDF = function(files) { - if (files && files.length) { - // for (var i = 0; i < files.length; i++) { - var file = files[0]; - _current_upload = Upload.upload({ - url: '/upload/pdf', - fields: { - 'user': $scope.user, - 'form': $scope.form - }, - file: file - }).progress(function (evt) { - var progressPercentage = parseInt(100.0 * evt.loaded / evt.total); - $scope.log = 'progress: ' + progressPercentage + '% ' + - evt.config.file.name + '\n' + $scope.log; - $scope.pdfLoading = true; - }).success(function (data, status, headers, config) { - $scope.log = 'file ' + data.originalname + ' uploaded as '+ data.name +'. JSON: ' + JSON.stringify(data) + '\n' + $scope.log; - console.log($scope.form.pdf); - $scope.form.pdf = angular.fromJson(angular.toJson(data)); - $scope.pdfLoading = false; +// if (files && files.length) { +// // for (var i = 0; i < files.length; i++) { +// var file = files[0]; +// _current_upload = Upload.upload({ +// url: '/upload/pdf', +// fields: { +// 'user': $scope.user, +// 'form': $scope.form +// }, +// file: file +// }).progress(function (evt) { +// var progressPercentage = parseInt(100.0 * evt.loaded / evt.total); +// $scope.log = 'progress: ' + progressPercentage + '% ' + +// evt.config.file.name + '\n' + $scope.log; +// $scope.pdfLoading = true; +// }).success(function (data, status, headers, config) { +// $scope.log = 'file ' + data.originalname + ' uploaded as '+ data.name +'. JSON: ' + JSON.stringify(data) + '\n' + $scope.log; +// console.log($scope.form.pdf); +// $scope.form.pdf = angular.fromJson(angular.toJson(data)); +// $scope.pdfLoading = false; - console.log($scope.log); - console.log('$scope.pdf: '+$scope.form.pdf.name); - if(!$scope.$$phase){ - $scope.$apply(); - } - }).error(function(err){ - $scope.pdfLoading = false; - console.log('Error occured during upload.\n'); - console.log(err); - }); - // } - } - }; +// console.log($scope.log); +// console.log('$scope.pdf: '+$scope.form.pdf.name); +// if(!$scope.$$phase){ +// $scope.$apply(); +// } +// }).error(function(err){ +// $scope.pdfLoading = false; +// console.log('Error occured during upload.\n'); +// console.log(err); +// }); +// // } +// } +// }; - $rootScope.goToWithId = function(route, id) { - $state.go(route, {'formId': id}, {reload: true}); - }; +// $rootScope.goToWithId = function(route, id) { +// $state.go(route, {'formId': id}, {reload: true}); +// }; - // Create new Form - $rootScope.createOrUpdate = function() { +// // Create new Form +// $rootScope.createOrUpdate = function() { - if($scope.isNewForm){ - // Create new Form object - var form = new Forms($scope.form); +// if($scope.isNewForm){ +// // Create new Form object +// var form = new Forms($scope.form); - $http.post('/forms', {form: $scope.form}) - .success(function(data, status, headers){ - console.log('form created'); +// $http.post('/forms', {form: $scope.form}) +// .success(function(data, status, headers){ +// console.log('form created'); - // Clear form fields - $scope.form = {}; - // Redirect after save - $scope.goToWithId('viewForm', $scope.form._id); - }).error(function(errorResponse){ - console.log(errorResponse); - $scope.error = errorResponse; - }); - } else{ - $scope.update(); - } - }; +// // Clear form fields +// $scope.form = {}; +// // Redirect after save +// $scope.goToWithId('viewForm', $scope.form._id); +// }).error(function(errorResponse){ +// console.log(errorResponse); +// $scope.error = errorResponse; +// }); +// } else{ +// $scope.update(function(err){ +// console.log('done updating'); +// }); +// } +// }; - // Update existing Form - $rootScope.update = function() { - var form = new Forms($scope.form); - console.log('update form'); - console.log($scope.form); +// // Update existing Form +// $rootScope.update = function(cb) { +// var form = new Forms($scope.form); +// console.log('update form'); +// console.log($scope.form); - $http.put('/forms/'+$scope.form._id, {form: $scope.form}) - .success(function(data, status, headers){ - console.log('form updated successfully'); - $scope.goToWithId('viewForm', $scope.form._id); - }).error(function(err){ - console.log('Error occured during form UPDATE.\n'); - console.log(err); - }); - // form.$update({formId: $scope.form._id}, function(response) { - // console.log('form successfully updated'); - // $scope.goToWithId('viewForm', response._id); - // }, function(errorResponse) { - // console.log(errorResponse.data.message); - // $scope.error = errorResponse.data.message; - // }); - }; - } -]); +// $http.put('/forms/'+$scope.form._id, {form: $scope.form}) +// .success(function(data, status, headers){ +// console.log('form updated successfully'); +// $scope.goToWithId('viewForm', $scope.form._id); +// cb(null); +// }).error(function(err){ +// console.log('Error occured during form UPDATE.\n'); +// console.log(err); +// cb(err); +// }); +// }; +// } +// ]); 'use strict'; @@ -652,10 +687,30 @@ angular.module('forms').controller('ViewFormController', ['$rootScope', '$scope' rows: [] }; + // Return all user's Forms + $scope.findAll = function() { + $scope.myforms = Forms.query(); + }; + + // Find a specific Form + $scope.findOne = function() { + $scope.myform = Forms.get({ + formId: $stateParams.formId + }); + CurrentForm.setForm($scope.myform); + }; + + $scope.goToWithId = function(route, id) { + $state.go(route, {'formId': id}, {reload: true}); + }; + + + $scope.setForm = function (form) { $scope.myForm = form; }; + //Modal functions $scope.openCreateModal = function(){ if(!$scope.showCreateModal){ $scope.showCreateModal = true; @@ -667,53 +722,9 @@ angular.module('forms').controller('ViewFormController', ['$rootScope', '$scope' } }; - //Create new form - $scope.createNew = function(){ - var form = {}; - form.title = $scope.myForm.name.$modelValue; - form.language = $scope.myForm.language.$modelValue; - console.log(form); - $scope.showCreateModal = true; - - console.log($scope.myForm); - if($scope.myForm.$valid && $scope.myForm.$dirty){ - $http.post('/forms', {form: form}) - .success(function(data, status, headers){ - console.log('form created'); - - // Clear form fields - $scope.myForm = {}; - // Redirect after save - $scope.goToWithId('viewForm', $scope.myform._id); - }).error(function(errorResponse){ - console.log(errorResponse); - // $scope.error = errorResponse.data.message; - }); - } - }; - - $scope.saveInProgress = false; - $scope.update = function() { - if(!$scope.saveInProgress){ - $scope.saveInProgress = true; - - console.log('start update()'); - - $http.put('/forms/'+$scope.myform._id, {form: $scope.myform}) - .then(function(response){ - console.log('form updated successfully'); - console.log('$scope.saveInProgress: '+$scope.saveInProgress); - // $rootScope.goToWithId('viewForm', $scope.myform._id); - }).catch(function(response){ - console.log('Error occured during form UPDATE.\n'); - console.log(response.data); - }).finally(function() { - $scope.saveInProgress = false; - }); - } - }; - - //Table Functions + /* + * Table Functions + */ $scope.toggleAllCheckers = function(){ console.log('toggleAllCheckers'); for(var i=0; i<$scope.table.rows.length; i++){ @@ -723,12 +734,11 @@ angular.module('forms').controller('ViewFormController', ['$rootScope', '$scope' $scope.toggleObjSelection = function($event, description) { $event.stopPropagation(); console.log('checkbox clicked'); - }; - - $scope.rowClicked = function(obj) { + }; + $scope.rowClicked = function(obj) { console.log('row clicked'); obj.selected = !obj.selected; - }; + }; //show submissions of Form $scope.showSubmissions = function(){ @@ -758,24 +768,12 @@ angular.module('forms').controller('ViewFormController', ['$rootScope', '$scope' } console.log($scope.submissions); }; - //hide submissions of Form $scope.hideSubmissions = function(){ $scope.viewSubmissions = false; }; - // Return all user's Forms - $scope.findAll = function() { - $scope.myforms = Forms.query(); - }; - // Find a specific Form - $scope.findOne = function() { - $scope.myform = Forms.get({ - formId: $stateParams.formId - }); - CurrentForm.setForm($scope.myform); - }; // Remove existing Form $scope.remove = function(form_id) { @@ -806,56 +804,55 @@ angular.module('forms').controller('ViewFormController', ['$rootScope', '$scope' }); }; - $scope.goToWithId = function(route, id) { - $state.go(route, {'formId': id}, {reload: true}); - }; - // Create new Form - $rootScope.createOrUpdate = function() { - if($scope.isNewForm){ - // Create new Form object - var form = new Forms($scope.myform); + $scope.createNew = function(){ + var form = {}; + form.title = $scope.myForm.name.$modelValue; + form.language = $scope.myForm.language.$modelValue; + console.log(form); + $scope.showCreateModal = true; - $http.post('/forms', {form: $scope.myform}) + console.log($scope.myForm); + if($scope.myForm.$valid && $scope.myForm.$dirty){ + $http.post('/forms', {form: form}) .success(function(data, status, headers){ console.log('form created'); // Clear form fields - $scope.myform = {}; + $scope.myForm = {}; // Redirect after save $scope.goToWithId('viewForm', $scope.myform._id); }).error(function(errorResponse){ - console.log(errorResponse.data.message); - $scope.error = errorResponse.data.message; + console.log(errorResponse); + // $scope.error = errorResponse.data.message; }); - } else{ - $rootScope.update(); } }; - // $rootScope.saveInProgress = false; - - var saveFinished = function() { - $rootScope.saveInProgress = false; - console.log('update form'); - }; - // Update existing Form - $rootScope.update = function() { + $scope.saveInProgress = false; + $scope.update = $rootScope.update = function(cb) { + if(!$scope.saveInProgress){ + $scope.saveInProgress = true; - $rootScope.saveInProgress = true; - console.log('update form'); + $rootScope.saveInProgress = true; + console.log('begin updating form'); + var err = null; - $http.put('/forms/'+$scope.myform._id, {form: $scope.myform}) - .then(function(response){ - console.log('form updated successfully'); - }).catch(function(response){ - console.log('Error occured during form UPDATE.\n'); - console.log(response.data); - }).finally(function() { - $rootScope.saveInProgress = false; - console.log('update form'); - }); + $http.put('/forms/'+$scope.myform._id, {form: $scope.myform}) + .then(function(response){ + console.log('form updated successfully'); + console.log(response.status); + }).catch(function(response){ + console.log('Error occured during form UPDATE.\n'); + console.log(response.data); + err = response.data; + }).finally(function() { + console.log('finished updating'); + $scope.saveInProgress = false; + cb(err); + }); + } }; $rootScope.resetForm = function(){ @@ -872,11 +869,15 @@ angular.module('forms').directive('autoSaveForm', ['$rootScope', '$timeout', fun return { require: ['^form'], + // scope: { + // callback: '&autoSaveCallback' + // }, link: function($scope, $element, $attrs, $ctrls) { - if(!$rootScope.watchCount === undefined){ + if($rootScope.watchCount === undefined){ $rootScope.watchCount = 0; } + var difference = function(array){ var rest = Array.prototype.concat.apply(Array.prototype, Array.prototype.slice.call(arguments, 1)); @@ -896,7 +897,7 @@ angular.module('forms').directive('autoSaveForm', ['$rootScope', '$timeout', fun var expression = $attrs.autoSaveForm || 'true'; $scope.$on('ngRepeatStarted', function(ngRepeatFinishedEvent) { - $scope.finishedRender = false; + // $scope.finishedRender = false; $rootScope.watchCount = 0; }); $scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) { @@ -904,20 +905,20 @@ angular.module('forms').directive('autoSaveForm', ['$rootScope', '$timeout', fun }); $scope.$watch('myform.form_fields', function(newValue, oldValue) { - + console.log('watchCount: '+$rootScope.watchCount); if(difference(oldValue,newValue).length === 0 || oldValue === undefined){ return; } - // console.log('\n\n-------\n$pristine: '+( $formCtrl.$pristine ) ); - // console.log('$dirty: '+( $formCtrl.$dirty ) ); - // console.log('form_fields changed: '+difference(oldValue.form_fields,newValue.form_fields).length ); + console.log('\n\n----------\n$dirty: '+( $formCtrl.$dirty ) ); + console.log('form_fields changed: '+difference(oldValue,newValue).length ); // console.log('$valid: '+$formCtrl.$valid); - // console.log('finishedRender: '+$scope.finishedRender); + console.log('finishedRender: '+$scope.finishedRender); // console.log('saveInProgress: '+$scope.saveInProgress); if($scope.finishedRender && ($formCtrl.$dirty || difference(oldValue,newValue).length !== 0) ) { $rootScope.watchCount++; + if($rootScope.watchCount === 1) { if(savePromise) { @@ -927,15 +928,22 @@ angular.module('forms').directive('autoSaveForm', ['$rootScope', '$timeout', fun savePromise = $timeout(function() { savePromise = null; - // Still valid? - // if($formCtrl.$valid) { - if($scope.$eval(expression) !== false) { - console.log('Form data persisted -- setting pristine flag'); - $formCtrl.$setPristine(); - } - // } + $rootScope[$attrs.autoSaveCallback]( + function(err){ + if(!err){ + // console.log('Form data persisted -- setting pristine flag'); + console.log('\n\n---------\nUpdate form CLIENT'); + console.log(Date.now()); + $rootScope.watchCount = 0; + $formCtrl.$setPristine(); + }else{ + console.log('Error form data NOT persisted'); + console.log(err); + } + }); }); + } } @@ -1103,7 +1111,7 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', '$q', '$ht // console.log($scope.addField.types[i].name === fieldType); if($scope.addField.types[i].name === fieldType){ $scope.addField.types[i].lastAddedID++; - console.log($scope.addField.types[i].lastAddedID); + // console.log($scope.addField.types[i].lastAddedID); fieldTitle = $scope.addField.types[i].value+$scope.addField.types[i].lastAddedID; break; } @@ -1118,14 +1126,16 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', '$q', '$ht // put newField into fields array $scope.myform.form_fields.unshift(newField); - console.log($scope.myform.form_fields.length); + console.log('\n\n---------\nAdded field CLIENT'); + console.log(Date.now()); + // console.log($scope.myform.form_fields.length); }; // deletes particular field on button click $scope.deleteField = function (hashKey){ - console.log($scope.myform.form_fields); + // console.log($scope.myform.form_fields); for(var i = 0; i < $scope.myform.form_fields.length; i++){ - console.log($scope.myform.form_fields[i].$$hashKey === hashKey); + // console.log($scope.myform.form_fields[i].$$hashKey === hashKey); if($scope.myform.form_fields[i].$$hashKey === hashKey){ $scope.myform.form_fields.splice(i, 1); break; @@ -1230,7 +1240,7 @@ var __indexOf = [].indexOf || function(item) { }; angular.module('forms').directive('fieldDirective', function($http, $compile) { - + var getTemplateUrl = function(field) { @@ -1254,6 +1264,17 @@ angular.module('forms').directive('fieldDirective', function($http, $compile) { var linker = function(scope, element) { scope.field.required = scope.required; + + //Set format only if field is a date + if(scope.field.fieldType === 'date'){ + $scope.dateOptions = { + changeYear: true, + changeMonth: true, + altFormat: "mm/dd/yyyy", + yearRange: '1900:-0', + defaultDate: 0, + }; + } // GET template content from path var templateUrl = getTemplateUrl(scope.field); @@ -1546,7 +1567,7 @@ angular.module('users').config(['$httpProvider', }; }); }]); -'use strict'; + // Setting up route angular.module('users').config(['$stateProvider', @@ -1620,7 +1641,7 @@ angular.module('users').config(['$stateProvider', }). state('signup-success', { url: '/signup-success', - templateUrl: 'modules/users/views/authentication/signup.client.view.html' + templateUrl: 'modules/users/views/authentication/signup-success.client.view.html' }). state('signin', { url: '/signin', @@ -1662,14 +1683,12 @@ angular.module('users').controller('AuthenticationController', ['$scope', '$loca if ($scope.authentication.isAuthenticated()) $state.go('home'); $scope.signin = function() { - // console.log("signin"); - // console.log($scope.credentials); Auth.currentUser = User.login($scope.credentials).then( function(response) { Auth.login(response); $scope.user = $rootScope.user = Auth.ensureHasCurrentUser(User); - if($state.previous.name !== 'home'){ + if($state.previous.name !== 'home' && $state.previous.name !== ''){ $state.go($state.previous.name); }else{ $state.go('home'); @@ -1687,8 +1706,9 @@ angular.module('users').controller('AuthenticationController', ['$scope', '$loca }; $scope.signup = function() { - User.save($scope.registration, - function() { + User.signup($scope.credentials).then( + function(response) { + console.log('signup-success'); $state.go('signup-success'); }, function(error) { @@ -1986,6 +2006,39 @@ angular.module('users') // // }; // } // ]); +'use strict'; + +app.service('Authorizer', function(APP_PERMISSIONS, USER_ROLES) { + return function(user) { + return { + canAccess: function(permissions) { + var i, len, permission; + if (!angular.isArray(permissions)) { + permissions = [permissions]; + } + for (i = 0, len = permissions.length; i < len; i++) { + permission = permissions[i]; + if (APP_PERMISSIONS[permission] === null) { + throw 'Bad permission value'; + } + if (user && user.role) { + switch (permission) { + case APP_PERMISSIONS.viewAdminSettings: + case APP_PERMISSIONS.editAdminSettings: + return user.role === USER_ROLES.admin; + case APP_PERMISSIONS.viewPrivateForm: + case APP_PERMISSIONS.editForm: + return user.role === USER_ROLES.admin || user.role === USER_ROLES.normal; + } + } else { + return false; + } + } + return false; + } + }; + }; +}); // 'use strict'; // angular.module('users').factory('AuthenticationService', function($http, $timeout, $q) { diff --git a/public/dist/application.min.css b/public/dist/application.min.css index 1029bb6f..aaa3ce7c 100644 --- a/public/dist/application.min.css +++ b/public/dist/application.min.css @@ -1 +1 @@ -.navbar-inverse{background-color:#fafafa;border:0}.navbar .navbar-brand{font-size:1.6em;font-weight:900;color:#ff8383}.navbar .navbar-brand:hover,.navbar .navbar-brand:visited{color:#FA787E}.navbar li.dropdown a.dropdown-toggle:hover>*{color:#000}.navbar li.dropdown a.dropdown-toggle>*{color:#d9d9d9}.navbar li.dropdown.open a.dropdown-toggle:hover>*{color:#fff}.navbar .navbar-brand span{text-decoration:underline}.nav.navbar-nav.navbar-right li{padding-right:20px}.content{margin-top:70px}.undecorated-link:hover{text-decoration:none}.ng-cloak,.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}.ng-invalid.ng-dirty{border-color:#FA787E}.ng-valid.ng-dirty{border-color:#78FA89}.browsehappy.jumbotron.hide,body.ng-cloak{display:block}section.hero-section{width:100%}section.hero-section .jumbotron{background-color:transparent;color:#fff}.image-background{position:absolute;top:0;left:0;height:230%;width:100%;z-index:-98;background-image:url(http://yourplaceandmine.ie/wp-content/uploads/2014/09/Daingean-meeting-048_13-1080x675.jpg);background-repeat:no-repeat;background-position:0 50%;background-size:cover}.opacity-background{position:absolute;top:0;left:0;height:230%;width:100%;background-color:rgba(0,0,0,.5);z-index:-97}section.hero-section .jumbotron .signup-btn{background-color:#FA787E;border:none;font-size:2em;padding:.3em .9em;color:#fff;background-color:rgba(250,120,126,.65) #FA787E}.row-height{display:table;table-layout:fixed;height:100%;width:100%}.col-height{display:table-cell;float:none;height:100%}.col-top{vertical-align:top}.col-middle{vertical-align:middle}.col-bottom{vertical-align:bottom}@media (min-width:480px){.row-xs-height{display:table;table-layout:fixed;height:100%;width:100%}.col-xs-height{display:table-cell;float:none;height:100%}.col-xs-top{vertical-align:top}.col-xs-middle{vertical-align:middle}.col-xs-bottom{vertical-align:bottom}}@media (min-width:768px){.row-sm-height{display:table;table-layout:fixed;height:100%;width:100%}.col-sm-height{display:table-cell;float:none;height:100%}.col-sm-top{vertical-align:top}.col-sm-middle{vertical-align:middle}.col-sm-bottom{vertical-align:bottom}}@media (min-width:992px){.row-md-height{display:table;table-layout:fixed;height:100%;width:100%}.col-md-height{display:table-cell;float:none;height:100%}.col-md-top{vertical-align:top}.col-md-middle{vertical-align:middle}.col-md-bottom{vertical-align:bottom}}@media (min-width:1200px){.row-lg-height{display:table;table-layout:fixed;height:100%;width:100%}.col-lg-height{display:table-cell;float:none;height:100%}.col-lg-top{vertical-align:top}.col-lg-middle{vertical-align:middle}.col-lg-bottom{vertical-align:bottom}}form .row.field{padding:1em 0 3em}form .row.field>.field-title{margin-top:.5em;font-size:1.5em}form.submission-form .row.field.statement>.field-title{font-size:1.7em}form.submission-form .row.field.statement>.field-input{font-size:1.2em;color:#ddd}form.submission-form .row.field>.field-input input{width:500px}form.submission-form .row.field .field-input>input:focus{font-size:1em}form .row.field.textfield>.field-input>input{padding:.45em .9em;width:600px}div.config-form>.row{padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05);width:90%}div.config-form>.row>.container:nth-of-type(odd){border-right:1px #ddd solid}div.config-form .row>.field-input{padding-top:1.2em;padding-left:.1em}div.config-form .row>.field-input label{padding-left:1.3em;display:block}.admin-form>.page-header{padding-bottom:0;margin-bottom:40px}.admin-form>.page-header h1{margin-bottom:0}.admin-form>.page-header>.col-xs-3{padding-top:1.4em}.admin-form .form-controls .row{padding:5px}.admin-form .page-header{border:none}.admin-form .tab-content{padding-top:3em}.admin-form .panel-heading{background-color:#f1f1f1}.admin-form .panel-heading:hover{background-color:#fff;cursor:pointer}.admin-form .panel-heading a:hover{text-decoration:none}.current-fields .tool-panel>.panel-default:hover{border-color:#9d9d9d;cursor:pointer}.current-fields .tool-panel>.panel-default .panel-heading{background-color:#fff;color:#9d9d9d!important}.current-fields .tool-panel>.panel-default .panel-heading:hover{background-color:#eee;color:#000!important;cursor:pointer}.current-fields .tool-panel>.panel-default .panel-heading a{color:inherit}.current-fields .tool-panel>.panel-default .panel-heading a:hover{text-decoration:none}.admin-form .add-field{background-color:#ddd}.admin-form .add-field .col-xs-6{padding:.25em .4em}.admin-form .add-field .col-xs-6 .panel-heading{border-width:1px;border-style:solid;border-color:#bbb;border-radius:4px}.status-light{padding-left:.6em}.status-light.status-light-off{color:#BE0000}.status-light.status-light-on{color:#3C0}section>section.ng-scope{padding:0 60px 20px}.form-item.row{text-align:center;border-bottom:6px inset #ccc;background-color:#eee;width:180px;height:215px;margin-bottom:45px}.form-item.row.create-new{background-color:#838383;color:#fff}.form-item.row.create-new.new-form{background-color:#ff8383;z-index:11}.form-item.row.create-new.new-form:hover{background-color:#ff6464}.form-item.new-form a.btn{font-size:.95em}.overlay{position:absolute;top:0;left:0;height:193%;width:inherit;background-color:rgba(0,0,0,.5);z-index:10}.form-item.row.create-new:hover,.form-item.row:hover{border-bottom:8px inset #ccc;background-color:#d9d9d9}.form-item.row.create-new:hover{background-color:#515151}.form-item.row>.title-row{position:relative;top:15px;padding-top:3em;padding-bottom:3.65em}.form-item.row>.title-row h4{font-size:1.3em}.form-item.row.create-new>.title-row{padding:0}.form-item.row.create-new>.title-row h4{font-size:7em}.form-item.row>.details-row{margin-top:3.2em}.form-item.row>.details-row small{font-size:.6em}.form-item.row.create-new>.details-row small{font-size:.95em}@media (min-width:992px){.nav-users{position:fixed}}.remove-account-container{display:inline-block;position:relative}.btn-remove-account{top:10px;right:10px;position:absolute}section.auth{margin-top:5em}section.auth>h3{font-size:3em;font-weight:500;color:#777}section.auth.signup-view>h3{font-size:4.4em;padding-bottom:.5em}section.auth.signup-view.success>h3{padding-bottom:1.2em} \ No newline at end of file +.navbar-inverse{background-color:#fafafa;border:0}.navbar .navbar-brand{font-size:1.6em;font-weight:900;color:#ff8383}.navbar .navbar-brand:hover,.navbar .navbar-brand:visited{color:#FA787E}.navbar li.dropdown a.dropdown-toggle:hover>*{color:#000}.navbar li.dropdown a.dropdown-toggle>*{color:#d9d9d9}.navbar li.dropdown.open a.dropdown-toggle:hover>*{color:#fff}.navbar .navbar-brand span{text-decoration:underline}.nav.navbar-nav.navbar-right li{padding-right:20px}.content{margin-top:70px}.undecorated-link:hover{text-decoration:none}.ng-cloak,.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}.ng-invalid.ng-dirty{border-color:#FA787E}.ng-valid.ng-dirty{border-color:#78FA89}.browsehappy.jumbotron.hide,body.ng-cloak{display:block}section.hero-section{width:100%}section.hero-section .jumbotron{background-color:transparent;color:#fff}.image-background{position:absolute;top:0;left:0;height:230%;width:100%;z-index:-98;background-image:url(http://yourplaceandmine.ie/wp-content/uploads/2014/09/Daingean-meeting-048_13-1080x675.jpg);background-repeat:no-repeat;background-position:0 50%;background-size:cover}.opacity-background{position:absolute;top:0;left:0;height:230%;width:100%;background-color:rgba(0,0,0,.5);z-index:-97}section.hero-section .jumbotron .signup-btn{background-color:#FA787E;border:none;font-size:2em;padding:.3em .9em;color:#fff;background-color:rgba(250,120,126,.65) #FA787E}.row-height{display:table;table-layout:fixed;height:100%;width:100%}.col-height{display:table-cell;float:none;height:100%}.col-top{vertical-align:top}.col-middle{vertical-align:middle}.col-bottom{vertical-align:bottom}@media (min-width:480px){.row-xs-height{display:table;table-layout:fixed;height:100%;width:100%}.col-xs-height{display:table-cell;float:none;height:100%}.col-xs-top{vertical-align:top}.col-xs-middle{vertical-align:middle}.col-xs-bottom{vertical-align:bottom}}@media (min-width:768px){.row-sm-height{display:table;table-layout:fixed;height:100%;width:100%}.col-sm-height{display:table-cell;float:none;height:100%}.col-sm-top{vertical-align:top}.col-sm-middle{vertical-align:middle}.col-sm-bottom{vertical-align:bottom}}@media (min-width:992px){.row-md-height{display:table;table-layout:fixed;height:100%;width:100%}.col-md-height{display:table-cell;float:none;height:100%}.col-md-top{vertical-align:top}.col-md-middle{vertical-align:middle}.col-md-bottom{vertical-align:bottom}}@media (min-width:1200px){.row-lg-height{display:table;table-layout:fixed;height:100%;width:100%}.col-lg-height{display:table-cell;float:none;height:100%}.col-lg-top{vertical-align:top}.col-lg-middle{vertical-align:middle}.col-lg-bottom{vertical-align:bottom}}form .row.field{padding:1em 0 3em}form .row.field>.field-title{margin-top:.5em;font-size:1.5em}form.submission-form .row.field.statement>.field-title{font-size:1.7em}form.submission-form .row.field.statement>.field-input{font-size:1.2em;color:#ddd}form.submission-form .row.field>.field-input input{width:500px}form.submission-form .row.field .field-input>input:focus{font-size:1em}form .row.field.textfield>.field-input>input{padding:.45em .9em;width:600px}div.config-form>.row{padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05);width:90%}div.config-form>.row>.container:nth-of-type(odd){border-right:1px #ddd solid}div.config-form .row>.field-input{padding-top:1.2em;padding-left:.1em}div.config-form .row>.field-input label{padding-left:1.3em;display:block}.admin-form>.page-header{padding-bottom:0;margin-bottom:40px}.admin-form>.page-header h1{margin-bottom:0}.admin-form>.page-header>.col-xs-3{padding-top:1.4em}.admin-form .form-controls .row{padding:5px}.admin-form .page-header{border:none}.admin-form .tab-content{padding-top:3em}.admin-form .panel-heading{background-color:#f1f1f1}.admin-form .panel-heading:hover{background-color:#fff;cursor:pointer}.admin-form .panel-heading a:hover{text-decoration:none}.current-fields .tool-panel>.panel-default:hover{border-color:#9d9d9d;cursor:pointer}.current-fields .tool-panel>.panel-default .panel-heading{background-color:#fff;color:#9d9d9d!important}.current-fields .tool-panel>.panel-default .panel-heading:hover{background-color:#eee;color:#000!important;cursor:pointer}.current-fields .tool-panel>.panel-default .panel-heading a{color:inherit}.current-fields .tool-panel>.panel-default .panel-heading a:hover{text-decoration:none}.admin-form .add-field{background-color:#ddd}.admin-form .add-field .col-xs-6{padding:.25em .4em}.admin-form .add-field .col-xs-6 .panel-heading{border-width:1px;border-style:solid;border-color:#bbb;border-radius:4px}.status-light{padding-left:.6em}.status-light.status-light-off{color:#BE0000}.status-light.status-light-on{color:#3C0}section>section.ng-scope{padding:0 60px 20px}.form-item.row{text-align:center;border-bottom:6px inset #ccc;background-color:#eee;width:180px;height:215px;margin-bottom:45px}.form-item.row.create-new{background-color:#838383;color:#fff}.form-item.row.create-new.new-form{background-color:#ff8383;z-index:11}.form-item.row.create-new.new-form:hover{background-color:#ff6464}.form-item.new-form a.btn{font-size:.95em}.overlay{position:absolute;top:0;left:0;height:193%;width:inherit;background-color:rgba(0,0,0,.5);z-index:10}.form-item.row.create-new:hover,.form-item.row:hover{border-bottom:8px inset #ccc;background-color:#d9d9d9}.form-item.row.create-new:hover{background-color:#515151}.form-item.row>.title-row{position:relative;top:15px;padding-top:3em;padding-bottom:3.65em}.form-item.row>.title-row h4{font-size:1.3em}.form-item.row.create-new>.title-row{padding:0}.form-item.row.create-new>.title-row h4{font-size:7em}.form-item.row>.details-row{margin-top:3.2em}.form-item.row>.details-row small{font-size:.6em}.form-item.row.create-new>.details-row small{font-size:.95em}@media (min-width:992px){.nav-users{position:fixed}}.remove-account-container{display:inline-block;position:relative}.btn-remove-account{top:10px;right:10px;position:absolute}section.auth{margin-top:5em}section.auth>h3{font-size:3em;font-weight:500;color:#777}section.auth.signup-view>h3{font-size:4.4em;padding-bottom:.5em} \ No newline at end of file diff --git a/public/dist/application.min.js b/public/dist/application.min.js index 97db4690..70d418a7 100644 --- a/public/dist/application.min.js +++ b/public/dist/application.min.js @@ -1,2 +1 @@ -"use strict";var ApplicationConfiguration=function(){var applicationModuleName="medform",applicationModuleVendorDependencies=["ngResource","ngAnimate","ui.router","ui.bootstrap","ui.utils","ngRaven"],registerModule=function(moduleName,dependencies){angular.module(moduleName,dependencies||[]),angular.module(applicationModuleName).requires.push(moduleName)};return{applicationModuleName:applicationModuleName,applicationModuleVendorDependencies:applicationModuleVendorDependencies,registerModule:registerModule}}();angular.module(ApplicationConfiguration.applicationModuleName,ApplicationConfiguration.applicationModuleVendorDependencies),angular.module(ApplicationConfiguration.applicationModuleName).config(["$locationProvider",function($locationProvider){$locationProvider.hashPrefix("!")}]),angular.module(ApplicationConfiguration.applicationModuleName).run(["$rootScope","$state","$stateParams",function($rootScope,$state,$stateParams){$rootScope.$state=$state,$rootScope.$stateParams=$stateParams,$rootScope.$on("$stateChangeSuccess",function(event,toState,toParams,fromState){console.log(fromState),$state.previous=fromState,"home"===toState.name&&$rootScope.authentication.isAuthenticated()&&(event.preventDefault(),$state.go("listForms"))})}]),angular.element(document).ready(function(){"#_=_"===window.location.hash&&(window.location.hash="#!"),angular.bootstrap(document,[ApplicationConfiguration.applicationModuleName])}),ApplicationConfiguration.registerModule("core",["users"]),ApplicationConfiguration.registerModule("forms",["ngFileUpload","users"]),ApplicationConfiguration.registerModule("users"),angular.module("core").config(["$stateProvider","$urlRouterProvider",function($stateProvider,$urlRouterProvider,Authorization){$urlRouterProvider.otherwise("/"),$stateProvider.state("home",{url:"/",templateUrl:"modules/core/views/home.client.view.html"})}]),angular.module("core").controller("HeaderController",["$rootScope","$scope","Menus","$state","Auth","User",function($rootScope,$scope,Menus,$state,Auth,User){$scope.user=$rootScope.user=Auth.ensureHasCurrentUser(User),$scope.authentication=$rootScope.authentication=Auth,$rootScope.languages=$scope.languages=["english","french","spanish"],$scope.isCollapsed=!1,$scope.hideNav=!1,$scope.menu=Menus.getMenu("topbar"),$scope.signout=function(){var promise=User.logout();promise.then(function(){Auth.logout(),$rootScope.user=null,$state.go("home")},function(reason){console.log("Logout Failed: "+reason)})},$scope.toggleCollapsibleMenu=function(){$scope.isCollapsed=!$scope.isCollapsed},$scope.$on("$stateChangeSuccess",function(event,toState,toParams,fromState,fromParams){$scope.isCollapsed=!1,$scope.hideNav=!1,angular.isDefined(toState.data)&&angular.isDefined(toState.data.hideNav)&&($scope.hideNav=toState.data.hideNav)})}]),angular.module("core").controller("HomeController",["$rootScope","$scope","User","Auth","$state",function($rootScope,$scope,User,Auth,$state){$scope=$rootScope,$scope.user=Auth.ensureHasCurrentUser(User),$scope.authentication=Auth}]),angular.module("core").service("Menus",[function(){this.defaultRoles=["*"],this.menus={};var shouldRender=function(user){if(!user)return this.isPublic;if(~this.roles.indexOf("*"))return!0;for(var userRoleIndex in user.roles)for(var roleIndex in this.roles)if(console.log(this.roles[roleIndex]),console.log(this.roles[roleIndex]===user.roles[userRoleIndex]),this.roles[roleIndex]===user.roles[userRoleIndex])return!0;return!1};this.validateMenuExistance=function(menuId){if(menuId&&menuId.length){if(this.menus[menuId])return!0;throw new Error("Menu does not exists")}throw new Error("MenuId was not provided")},this.getMenu=function(menuId){return this.validateMenuExistance(menuId),this.menus[menuId]},this.addMenu=function(menuId,isPublic,roles){return this.menus[menuId]={isPublic:isPublic||!1,roles:roles||this.defaultRoles,items:[],shouldRender:shouldRender},this.menus[menuId]},this.removeMenu=function(menuId){this.validateMenuExistance(menuId),delete this.menus[menuId]},this.addMenuItem=function(menuId,menuItemTitle,menuItemURL,menuItemType,menuItemUIRoute,isPublic,roles,position){return this.validateMenuExistance(menuId),this.menus[menuId].items.push({title:menuItemTitle,link:menuItemURL,menuItemType:menuItemType||"item",menuItemClass:menuItemType,uiRoute:menuItemUIRoute||"/"+menuItemURL,isPublic:null===isPublic||"undefined"==typeof isPublic?this.menus[menuId].isPublic:isPublic,roles:null===roles||"undefined"==typeof roles?this.menus[menuId].roles:roles,position:position||0,items:[],shouldRender:shouldRender}),this.menus[menuId]},this.addSubMenuItem=function(menuId,rootMenuItemURL,menuItemTitle,menuItemURL,menuItemUIRoute,isPublic,roles,position){this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)this.menus[menuId].items[itemIndex].link===rootMenuItemURL&&this.menus[menuId].items[itemIndex].items.push({title:menuItemTitle,link:menuItemURL,uiRoute:menuItemUIRoute||"/"+menuItemURL,isPublic:null===isPublic||"undefined"==typeof isPublic?this.menus[menuId].items[itemIndex].isPublic:isPublic,roles:null===roles||"undefined"==typeof roles?this.menus[menuId].items[itemIndex].roles:roles,position:position||0,shouldRender:shouldRender});return this.menus[menuId]},this.removeMenuItem=function(menuId,menuItemURL){this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)this.menus[menuId].items[itemIndex].link===menuItemURL&&this.menus[menuId].items.splice(itemIndex,1);return this.menus[menuId]},this.removeSubMenuItem=function(menuId,submenuItemURL){this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)for(var subitemIndex in this.menus[menuId].items[itemIndex].items)this.menus[menuId].items[itemIndex].items[subitemIndex].link===submenuItemURL&&this.menus[menuId].items[itemIndex].items.splice(subitemIndex,1);return this.menus[menuId]},this.addMenu("topbar",!1,["*"]),this.addMenu("bottombar",!1,["*"])}]),angular.module("forms").run(["Menus",function(Menus){Menus.addMenuItem("topbar","My Forms","forms","","/forms",!1)}]).filter("formValidity",function(){return function(formObj){var formKeys=Object.keys(formObj),fields=(formKeys.filter(function(key){return"$"!==key[0]}),formObj.form_fields),valid_count=fields.filter(function(field){return"object"==typeof field?!!field.fieldValue:void 0}).length;return valid_count}}),angular.module("forms").config(["$stateProvider",function($stateProvider){$stateProvider.state("listForms",{url:"/forms",templateUrl:"modules/forms/views/list-forms.client.view.html"}).state("createForm",{url:"/forms/create",templateUrl:"modules/forms/views/create-form.client.view.html"}).state("viewForm",{url:"/forms/:formId/admin",templateUrl:"modules/forms/views/view-form.client.view.html"}).state("viewPublicForm",{url:"/forms/:formId",templateUrl:"modules/forms/views/view-public-form.client.view.html",data:{hideNav:!0,hideFooter:!1}}).state("editForm",{url:"/forms/:formId/edit",templateUrl:"modules/forms/views/create-form.client.view.html"})}]),angular.module("forms").controller("EditFormController",["$scope","$state","$rootScope","Upload","$stateParams","FormFields","Forms","CurrentForm","$modal","$location","$http",function($scope,$state,$rootScope,Upload,$stateParams,FormFields,Forms,CurrentForm,$modal,$location,$http){$scope.form={},$scope.isNewForm=!1,$scope.log="",$scope.pdfLoading=!1;var _current_upload=null;$stateParams.formId?Forms.get({formId:$stateParams.formId},function(form){$scope.form=angular.fromJson(angular.toJson(form)),console.log($scope.form)}):($scope.form.form_fields=[],$scope.isNewForm=!0),$scope.cancelUpload=function(){_current_upload.abort(),$scope.pdfLoading=!1,$scope.removePDF()},$scope.removePDF=function(){$scope.form.pdf=null,$scope.form.isGenerated=!1,$scope.form.autofillPDFs=!1,console.log("form.pdf: "+$scope.form.pdf+" REMOVED")},$scope.uploadPDF=function(files){if(files&&files.length){var file=files[0];_current_upload=Upload.upload({url:"/upload/pdf",fields:{user:$scope.user,form:$scope.form},file:file}).progress(function(evt){var progressPercentage=parseInt(100*evt.loaded/evt.total);$scope.log="progress: "+progressPercentage+"% "+evt.config.file.name+"\n"+$scope.log,$scope.pdfLoading=!0}).success(function(data,status,headers,config){$scope.log="file "+data.originalname+" uploaded as "+data.name+". JSON: "+JSON.stringify(data)+"\n"+$scope.log,console.log($scope.form.pdf),$scope.form.pdf=angular.fromJson(angular.toJson(data)),$scope.pdfLoading=!1,console.log($scope.log),console.log("$scope.pdf: "+$scope.form.pdf.name),$scope.$$phase||$scope.$apply()}).error(function(err){$scope.pdfLoading=!1,console.log("Error occured during upload.\n"),console.log(err)})}},$rootScope.goToWithId=function(route,id){$state.go(route,{formId:id},{reload:!0})},$rootScope.createOrUpdate=function(){if($scope.isNewForm){new Forms($scope.form);$http.post("/forms",{form:$scope.form}).success(function(data,status,headers){console.log("form created"),$scope.form={},$scope.goToWithId("viewForm",$scope.form._id)}).error(function(errorResponse){console.log(errorResponse),$scope.error=errorResponse})}else $scope.update()},$rootScope.update=function(){new Forms($scope.form);console.log("update form"),console.log($scope.form),$http.put("/forms/"+$scope.form._id,{form:$scope.form}).success(function(data,status,headers){console.log("form updated successfully"),$scope.goToWithId("viewForm",$scope.form._id)}).error(function(err){console.log("Error occured during form UPDATE.\n"),console.log(err)})}}]),angular.module("forms").controller("SubmitFormController",["$scope","$stateParams","$state","Forms","CurrentForm",function($scope,$stateParams,$state,Forms,CurrentForm){$scope.form=Forms.get({formId:$stateParams.formId}),CurrentForm.setForm($scope.form)}]),angular.module("forms").controller("ViewSubmissionController",["$scope","$stateParams","$state","Submissions","$http",function($scope,$stateParams,$state,Submissions,$http){$scope.submissionId=void 0,$scope.findAll=function(){$scope.submissions=Submissions.query({formId:$stateParams.formId})},$scope.findOne=function(){$scope.submission=Submissions.get({submissionId:$scope.submissionId,formId:$stateParams.formId})},$scope.remove=function(submission){submission||(submission=$scope.submission),$http["delete"]("/forms/"+$stateParams.formId+"/submissions/"+submission._id).success(function(data,status,headers){console.log("submission deleted successfully"),alert("submission deleted..")})}}]),angular.module("forms").controller("ViewFormController",["$rootScope","$scope","$stateParams","$state","Forms","CurrentForm","$http",function($rootScope,$scope,$stateParams,$state,Forms,CurrentForm,$http){$scope.myform=CurrentForm.getForm(),$scope.submissions=void 0,$scope.viewSubmissions=!1,$scope.showCreateModal=!1,$scope.table={masterChecker:!0,rows:[]},$scope.setForm=function(form){$scope.myForm=form},$scope.openCreateModal=function(){$scope.showCreateModal||($scope.showCreateModal=!0)},$scope.closeCreateModal=function(){$scope.showCreateModal&&($scope.showCreateModal=!1)},$scope.createNew=function(){var form={};form.title=$scope.myForm.name.$modelValue,form.language=$scope.myForm.language.$modelValue,console.log(form),$scope.showCreateModal=!0,console.log($scope.myForm),$scope.myForm.$valid&&$scope.myForm.$dirty&&$http.post("/forms",{form:form}).success(function(data,status,headers){console.log("form created"),$scope.myForm={},$scope.goToWithId("viewForm",$scope.myform._id)}).error(function(errorResponse){console.log(errorResponse)})},$scope.saveInProgress=!1,$scope.update=function(){$scope.saveInProgress||($scope.saveInProgress=!0,console.log("start update()"),$http.put("/forms/"+$scope.myform._id,{form:$scope.myform}).then(function(response){console.log("form updated successfully"),console.log("$scope.saveInProgress: "+$scope.saveInProgress)})["catch"](function(response){console.log("Error occured during form UPDATE.\n"),console.log(response.data)})["finally"](function(){$scope.saveInProgress=!1}))},$scope.toggleAllCheckers=function(){console.log("toggleAllCheckers");for(var i=0;i<$scope.table.rows.length;i++)$scope.table.rows[i].selected=$scope.table.masterChecker},$scope.toggleObjSelection=function($event,description){$event.stopPropagation(),console.log("checkbox clicked")},$scope.rowClicked=function(obj){console.log("row clicked"),obj.selected=!obj.selected},$scope.showSubmissions=function(){$scope.viewSubmissions=!0,$scope.table.rows.length?$scope.submissions.length||$http.get("/forms/"+$scope.myform._id+"/submissions").success(function(data,status,headers){$scope.submissions=data,$scope.table.rows=data,console.log($scope.table.rows),console.log("form submissions successfully fetched")}).error(function(err){console.log("Could not fetch form submissions.\nError: "+err)}):$http.get("/forms/"+$scope.myform._id+"/submissions").success(function(data,status,headers){console.log(data),$scope.submissions=data,$scope.table.rows=data,console.log("form submissions successfully fetched")}).error(function(err){console.log("Could not fetch form submissions.\nError: "+err)}),console.log($scope.submissions)},$scope.hideSubmissions=function(){$scope.viewSubmissions=!1},$scope.findAll=function(){$scope.myforms=Forms.query()},$scope.findOne=function(){$scope.myform=Forms.get({formId:$stateParams.formId}),CurrentForm.setForm($scope.myform)},$scope.remove=function(form_id){var form={};form_id?form._id=form_id:(form=CurrentForm.getForm(),form||(form=$scope.myform)),$http["delete"]("/forms/"+form._id).success(function(data,status,headers){console.log("form deleted successfully"),form_id||$state.go("listForms"),$scope.myforms.length>0&&($scope.myforms=_.filter($scope.myforms,function(myform){return myform._id!==form._id}))}).error(function(error){console.log("ERROR: Form could not be deleted."),console.error(error)})},$scope.goToWithId=function(route,id){$state.go(route,{formId:id},{reload:!0})},$rootScope.createOrUpdate=function(){if($scope.isNewForm){new Forms($scope.myform);$http.post("/forms",{form:$scope.myform}).success(function(data,status,headers){console.log("form created"),$scope.myform={},$scope.goToWithId("viewForm",$scope.myform._id)}).error(function(errorResponse){console.log(errorResponse.data.message),$scope.error=errorResponse.data.message})}else $rootScope.update()};$rootScope.update=function(){$rootScope.saveInProgress=!0,console.log("update form"),$http.put("/forms/"+$scope.myform._id,{form:$scope.myform}).then(function(response){console.log("form updated successfully")})["catch"](function(response){console.log("Error occured during form UPDATE.\n"),console.log(response.data)})["finally"](function(){$rootScope.saveInProgress=!1,console.log("update form")})},$rootScope.resetForm=function(){$scope.myform=Forms.get({formId:$stateParams.formId})}}]),angular.module("forms").directive("autoSaveForm",["$rootScope","$timeout",function($rootScope,$timeout){return{require:["^form"],link:function($scope,$element,$attrs,$ctrls){void 0===!$rootScope.watchCount&&($rootScope.watchCount=0);var difference=function(array){var rest=Array.prototype.concat.apply(Array.prototype,Array.prototype.slice.call(arguments,1)),containsEquals=function(obj,target){return null==obj?!1:_.any(obj,function(value){return _.isEqual(value,target)})};return _.filter(array,function(value){return!containsEquals(rest,value)})},$formCtrl=$ctrls[0],savePromise=null;$scope.finishedRender=!1;var expression=$attrs.autoSaveForm||"true";$scope.$on("ngRepeatStarted",function(ngRepeatFinishedEvent){$scope.finishedRender=!1,$rootScope.watchCount=0}),$scope.$on("ngRepeatFinished",function(ngRepeatFinishedEvent){$scope.finishedRender=!0}),$scope.$watch("myform.form_fields",function(newValue,oldValue){0!==difference(oldValue,newValue).length&&void 0!==oldValue&&$scope.finishedRender&&($formCtrl.$dirty||0!==difference(oldValue,newValue).length)&&($rootScope.watchCount++,1===$rootScope.watchCount&&(savePromise&&$timeout.cancel(savePromise),savePromise=$timeout(function(){savePromise=null,$scope.$eval(expression)!==!1&&(console.log("Form data persisted -- setting pristine flag"),$formCtrl.$setPristine())})))},!0)}}}]),angular.module("forms").directive("changeFocus",function(){return{scope:{focusDownId:"@",focusUpId:"@"},link:function(scope,elem,attrs){scope.focusUp=function(){scope.$first||elem[0].previousElementSibling.find("input").focus(),scope.apply()},scope.focusDown=function(){scope.$last||elem[0].nextElementSibling.focus(),scope.apply()},angular.element("#"+scope.focusDownId).bind("click",function(){scope.focusDown()}),angular.element("#"+scope.focusUpId).bind("click",function(){scope.focusUp()})}}}),angular.module("forms").directive("configureFormDirective",["$rootScope","$http","$timeout","timeCounter","Auth","FormFields",function($rootScope,$http,$timeout,timeCounter,Auth,FormFields){return{controller:function($scope){$scope.log="",$scope.pdfLoading=!1,$scope.languages=$rootScope.languages;var _current_upload=null;$scope.createOrUpdate=$rootScope.createOrUpdate,$scope.resetForm=$rootScope.resetForm;$scope.pdfFields;$scope.cancelUpload=function(){_current_upload.abort(),$scope.pdfLoading=!1,$scope.removePDF()},$scope.removePDF=function(){$scope.myform.pdf=null,$scope.myform.isGenerated=!1,$scope.myform.autofillPDFs=!1,console.log("form.pdf: "+$scope.myform.pdf+" REMOVED")},$scope.uploadPDF=function(files){if(files&&files.length){var file=files[0];_current_upload=Upload.upload({url:"/upload/pdf",fields:{user:$scope.user,form:$scope.myform},file:file}).progress(function(evt){var progressPercentage=parseInt(100*evt.loaded/evt.total);$scope.log="progress: "+progressPercentage+"% "+evt.config.file.name+"\n"+$scope.log,$scope.pdfLoading=!0}).success(function(data,status,headers,config){$scope.log="file "+data.originalname+" uploaded as "+data.name+". JSON: "+JSON.stringify(data)+"\n"+$scope.log,console.log($scope.myform.pdf),$scope.myform.pdf=angular.fromJson(angular.toJson(data)),$scope.pdfLoading=!1,console.log($scope.log),console.log("$scope.pdf: "+$scope.myform.pdf.name),$scope.$$phase||$scope.$apply()}).error(function(err){$scope.pdfLoading=!1,console.log("Error occured during upload.\n"),console.log(err)})}}},templateUrl:"./modules/forms/views/directiveViews/form/configure-form.html",restrict:"E",scope:{myform:"=",user:"=",pdfFields:"@",formFields:"@"}}}]),angular.module("forms").directive("editFormDirective",["$rootScope","$q","$http","$timeout","timeCounter","Auth","FormFields",function($rootScope,$q,$http,$timeout,timeCounter,Auth,FormFields){return{templateUrl:"./modules/forms/views/directiveViews/form/edit-form.html",restrict:"E",scope:{myform:"=",user:"="},controller:function($scope){$scope.update=$rootScope.update,$scope.addField={},$scope.addField.types=FormFields.fields,$scope.addField.types.forEach(function(type){return type.lastAddedID=1,type}),$scope.accordion={},$scope.accordion.oneAtATime=!0,$scope.addNewField=function(fieldType){$scope.addField.lastAddedID++;for(var fieldTitle,i=0;i<$scope.addField.types.length;i++)if($scope.addField.types[i].name===fieldType){$scope.addField.types[i].lastAddedID++,console.log($scope.addField.types[i].lastAddedID),fieldTitle=$scope.addField.types[i].value+$scope.addField.types[i].lastAddedID;break}var newField={title:fieldTitle,fieldType:fieldType,fieldValue:"",required:!0,disabled:!1};$scope.myform.form_fields.unshift(newField),console.log($scope.myform.form_fields.length)},$scope.deleteField=function(hashKey){console.log($scope.myform.form_fields);for(var i=0;i<$scope.myform.form_fields.length;i++)if(console.log($scope.myform.form_fields[i].$$hashKey===hashKey),$scope.myform.form_fields[i].$$hashKey===hashKey){$scope.myform.form_fields.splice(i,1);break}},$scope.duplicateField=function(field,field_index){for(var i=0;i<$scope.myform.form_fields.length;i++)if($scope.myform.form_fields[i].field_id===field.field_id){$scope.addNewField($scope.myform.form_fields[i].fieldType);break}},$scope.addOption=function(field){field.field_options||(field.field_options=[]);var lastOptionID=0;field.field_options[field.field_options.length-1]&&(lastOptionID=field.field_options[field.field_options.length-1].option_id);var option_id=lastOptionID+1,newOption={option_id:option_id,option_title:"Option "+option_id,option_value:option_id};field.field_options.push(newOption)},$scope.deleteOption=function(field,option){for(var i=0;ii;i++)if(i in this&&this[i]===item)return i;return-1};angular.module("forms").directive("fieldDirective",function($http,$compile){var getTemplateUrl=function(field){var type=field.fieldType,templateUrl="./modules/forms/views/directiveViews/field/",supported_fields=["textfield","email","textarea","checkbox","date","dropdown","hidden","password","radio"];return __indexOf.call(supported_fields,type)>=0?templateUrl+=type+".html":void 0},linker=function(scope,element){scope.field.required=scope.required;var templateUrl=getTemplateUrl(scope.field);$http.get(templateUrl).success(function(data){element.html(data),$compile(element.contents())(scope)})};return{template:"
{{field.title}}
",restrict:"E",scope:{field:"=",required:"&"},link:linker}}),angular.module("forms").directive("formLocator",function(){return{link:function(scope){scope.$emit("formLocator")}}}),angular.module("forms").directive("formDirective",["$http","$timeout","timeCounter","Auth",function($http,$timeout,timeCounter,Auth){return{controller:function($scope){timeCounter.startClock(),$scope.submit=function(){var _timeElapsed=timeCounter.stopClock();$scope.form.timeElapsed=_timeElapsed,$scope.authentication=Auth,console.log($scope.authentication.isAuthenticated()),$http.post("/forms/"+$scope.form._id,$scope.form).success(function(data,status,headers){console.log("form submitted successfully"),alert("Form submitted.."),$scope.form.submitted=!0}).error(function(error){console.log(error)})},$scope.cancel=function(){alert("Form canceled..")}},templateUrl:"./modules/forms/views/directiveViews/form/form.html",restrict:"E",scope:{form:"="}}}]),angular.module("forms").directive("onFinishRender",function($rootScope,$timeout){return{restrict:"A",link:function(scope,element,attr){scope.$first===!0&&$timeout(function(){$rootScope.$broadcast("ngRepeatStarted")},500),scope.$last===!0&&$timeout(function(){$rootScope.$broadcast("ngRepeatFinished")},500)}}}),angular.module("forms").directive("tableDirective",["$http","$timeout","Auth",function($http,$timeout,Auth){return{controller:function($scope){$scope.toggleChecker=function(checked){for(var rows=$scope.gridOptions.$gridScope.renderedRows,allChecked=!0,r=0;r0&&($scope.myforms=_.filter($scope.myforms,function(myform){return myform._id!==form._id}))}).error(function(error){console.log("ERROR: Form could not be deleted."),console.error(error)})},$scope.createNew=function(){var form={};form.title=$scope.myForm.name.$modelValue,form.language=$scope.myForm.language.$modelValue,console.log(form),$scope.showCreateModal=!0,console.log($scope.myForm),$scope.myForm.$valid&&$scope.myForm.$dirty&&$http.post("/forms",{form:form}).success(function(data,status,headers){console.log("form created"),$scope.myForm={},$scope.goToWithId("viewForm",$scope.myform._id)}).error(function(errorResponse){console.log(errorResponse)})},$scope.saveInProgress=!1,$scope.update=$rootScope.update=function(cb){if(!$scope.saveInProgress){$scope.saveInProgress=!0,$rootScope.saveInProgress=!0,console.log("begin updating form");var err=null;$http.put("/forms/"+$scope.myform._id,{form:$scope.myform}).then(function(response){console.log("form updated successfully"),console.log(response.status)})["catch"](function(response){console.log("Error occured during form UPDATE.\n"),console.log(response.data),err=response.data})["finally"](function(){console.log("finished updating"),$scope.saveInProgress=!1,cb(err)})}},$rootScope.resetForm=function(){$scope.myform=Forms.get({formId:$stateParams.formId})}}]),angular.module("forms").directive("autoSaveForm",["$rootScope","$timeout",function($rootScope,$timeout){return{require:["^form"],link:function($scope,$element,$attrs,$ctrls){void 0===$rootScope.watchCount&&($rootScope.watchCount=0);var difference=function(array){var rest=Array.prototype.concat.apply(Array.prototype,Array.prototype.slice.call(arguments,1)),containsEquals=function(obj,target){return null==obj?!1:_.any(obj,function(value){return _.isEqual(value,target)})};return _.filter(array,function(value){return!containsEquals(rest,value)})},$formCtrl=$ctrls[0],savePromise=null;$scope.finishedRender=!1;$attrs.autoSaveForm||"true";$scope.$on("ngRepeatStarted",function(ngRepeatFinishedEvent){$rootScope.watchCount=0}),$scope.$on("ngRepeatFinished",function(ngRepeatFinishedEvent){$scope.finishedRender=!0}),$scope.$watch("myform.form_fields",function(newValue,oldValue){console.log("watchCount: "+$rootScope.watchCount),0!==difference(oldValue,newValue).length&&void 0!==oldValue&&(console.log("\n\n----------\n$dirty: "+$formCtrl.$dirty),console.log("form_fields changed: "+difference(oldValue,newValue).length),console.log("finishedRender: "+$scope.finishedRender),$scope.finishedRender&&($formCtrl.$dirty||0!==difference(oldValue,newValue).length)&&($rootScope.watchCount++,1===$rootScope.watchCount&&(savePromise&&$timeout.cancel(savePromise),savePromise=$timeout(function(){savePromise=null,$rootScope[$attrs.autoSaveCallback](function(err){err?(console.log("Error form data NOT persisted"),console.log(err)):(console.log("\n\n---------\nUpdate form CLIENT"),console.log(Date.now()),$rootScope.watchCount=0,$formCtrl.$setPristine())})}))))},!0)}}}]),angular.module("forms").directive("changeFocus",function(){return{scope:{focusDownId:"@",focusUpId:"@"},link:function(scope,elem,attrs){scope.focusUp=function(){scope.$first||elem[0].previousElementSibling.find("input").focus(),scope.apply()},scope.focusDown=function(){scope.$last||elem[0].nextElementSibling.focus(),scope.apply()},angular.element("#"+scope.focusDownId).bind("click",function(){scope.focusDown()}),angular.element("#"+scope.focusUpId).bind("click",function(){scope.focusUp()})}}}),angular.module("forms").directive("configureFormDirective",["$rootScope","$http","$timeout","timeCounter","Auth","FormFields",function($rootScope,$http,$timeout,timeCounter,Auth,FormFields){return{controller:function($scope){$scope.log="",$scope.pdfLoading=!1,$scope.languages=$rootScope.languages;var _current_upload=null;$scope.createOrUpdate=$rootScope.createOrUpdate,$scope.resetForm=$rootScope.resetForm;$scope.pdfFields;$scope.cancelUpload=function(){_current_upload.abort(),$scope.pdfLoading=!1,$scope.removePDF()},$scope.removePDF=function(){$scope.myform.pdf=null,$scope.myform.isGenerated=!1,$scope.myform.autofillPDFs=!1,console.log("form.pdf: "+$scope.myform.pdf+" REMOVED")},$scope.uploadPDF=function(files){if(files&&files.length){var file=files[0];_current_upload=Upload.upload({url:"/upload/pdf",fields:{user:$scope.user,form:$scope.myform},file:file}).progress(function(evt){var progressPercentage=parseInt(100*evt.loaded/evt.total);$scope.log="progress: "+progressPercentage+"% "+evt.config.file.name+"\n"+$scope.log,$scope.pdfLoading=!0}).success(function(data,status,headers,config){$scope.log="file "+data.originalname+" uploaded as "+data.name+". JSON: "+JSON.stringify(data)+"\n"+$scope.log,console.log($scope.myform.pdf),$scope.myform.pdf=angular.fromJson(angular.toJson(data)),$scope.pdfLoading=!1,console.log($scope.log),console.log("$scope.pdf: "+$scope.myform.pdf.name),$scope.$$phase||$scope.$apply()}).error(function(err){$scope.pdfLoading=!1,console.log("Error occured during upload.\n"),console.log(err)})}}},templateUrl:"./modules/forms/views/directiveViews/form/configure-form.html",restrict:"E",scope:{myform:"=",user:"=",pdfFields:"@",formFields:"@"}}}]),angular.module("forms").directive("editFormDirective",["$rootScope","$q","$http","$timeout","timeCounter","Auth","FormFields",function($rootScope,$q,$http,$timeout,timeCounter,Auth,FormFields){return{templateUrl:"./modules/forms/views/directiveViews/form/edit-form.html",restrict:"E",scope:{myform:"=",user:"="},controller:function($scope){$scope.update=$rootScope.update,$scope.addField={},$scope.addField.types=FormFields.fields,$scope.addField.types.forEach(function(type){return type.lastAddedID=1,type}),$scope.accordion={},$scope.accordion.oneAtATime=!0,$scope.addNewField=function(fieldType){$scope.addField.lastAddedID++;for(var fieldTitle,i=0;i<$scope.addField.types.length;i++)if($scope.addField.types[i].name===fieldType){$scope.addField.types[i].lastAddedID++,fieldTitle=$scope.addField.types[i].value+$scope.addField.types[i].lastAddedID;break}var newField={title:fieldTitle,fieldType:fieldType,fieldValue:"",required:!0,disabled:!1};$scope.myform.form_fields.unshift(newField),console.log("\n\n---------\nAdded field CLIENT"),console.log(Date.now())},$scope.deleteField=function(hashKey){for(var i=0;i<$scope.myform.form_fields.length;i++)if($scope.myform.form_fields[i].$$hashKey===hashKey){$scope.myform.form_fields.splice(i,1);break}},$scope.duplicateField=function(field,field_index){for(var i=0;i<$scope.myform.form_fields.length;i++)if($scope.myform.form_fields[i].field_id===field.field_id){$scope.addNewField($scope.myform.form_fields[i].fieldType);break}},$scope.addOption=function(field){field.field_options||(field.field_options=[]);var lastOptionID=0;field.field_options[field.field_options.length-1]&&(lastOptionID=field.field_options[field.field_options.length-1].option_id);var option_id=lastOptionID+1,newOption={option_id:option_id,option_title:"Option "+option_id,option_value:option_id};field.field_options.push(newOption)},$scope.deleteOption=function(field,option){for(var i=0;ii;i++)if(i in this&&this[i]===item)return i;return-1};angular.module("forms").directive("fieldDirective",function($http,$compile){var getTemplateUrl=function(field){var type=field.fieldType,templateUrl="./modules/forms/views/directiveViews/field/",supported_fields=["textfield","email","textarea","checkbox","date","dropdown","hidden","password","radio"];return __indexOf.call(supported_fields,type)>=0?templateUrl+=type+".html":void 0},linker=function(scope,element){scope.field.required=scope.required,"date"===scope.field.fieldType&&($scope.dateOptions={changeYear:!0,changeMonth:!0,altFormat:"mm/dd/yyyy",yearRange:"1900:-0",defaultDate:0});var templateUrl=getTemplateUrl(scope.field);$http.get(templateUrl).success(function(data){element.html(data),$compile(element.contents())(scope)})};return{template:"
{{field.title}}
",restrict:"E",scope:{field:"=",required:"&"},link:linker}}),angular.module("forms").directive("formLocator",function(){return{link:function(scope){scope.$emit("formLocator")}}}),angular.module("forms").directive("formDirective",["$http","$timeout","timeCounter","Auth",function($http,$timeout,timeCounter,Auth){return{controller:function($scope){timeCounter.startClock(),$scope.submit=function(){var _timeElapsed=timeCounter.stopClock();$scope.form.timeElapsed=_timeElapsed,$scope.authentication=Auth,console.log($scope.authentication.isAuthenticated()),$http.post("/forms/"+$scope.form._id,$scope.form).success(function(data,status,headers){console.log("form submitted successfully"),alert("Form submitted.."),$scope.form.submitted=!0}).error(function(error){console.log(error)})},$scope.cancel=function(){alert("Form canceled..")}},templateUrl:"./modules/forms/views/directiveViews/form/form.html",restrict:"E",scope:{form:"="}}}]),angular.module("forms").directive("onFinishRender",function($rootScope,$timeout){return{restrict:"A",link:function(scope,element,attr){scope.$first===!0&&$timeout(function(){$rootScope.$broadcast("ngRepeatStarted")},500),scope.$last===!0&&$timeout(function(){$rootScope.$broadcast("ngRepeatFinished")},500)}}}),angular.module("forms").directive("tableDirective",["$http","$timeout","Auth",function($http,$timeout,Auth){return{controller:function($scope){$scope.toggleChecker=function(checked){for(var rows=$scope.gridOptions.$gridScope.renderedRows,allChecked=!0,r=0;ri;i++){if(permission=permissions[i],null===APP_PERMISSIONS[permission])throw"Bad permission value";if(!user||!user.role)return!1;switch(permission){case APP_PERMISSIONS.viewAdminSettings:case APP_PERMISSIONS.editAdminSettings:return user.role===USER_ROLES.admin;case APP_PERMISSIONS.viewPrivateForm:case APP_PERMISSIONS.editForm:return user.role===USER_ROLES.admin||user.role===USER_ROLES.normal}}return!1}}}}),angular.module("users").factory("User",["$window","$q","$timeout","$http","$state",function($window,$q,$timeout,$http,$state){var userService={getCurrent:function(){var deferred=$q.defer();return $http.get("/users/me").success(function(response){deferred.resolve(response)}).error(function(){deferred.reject("User's session has expired")}),deferred.promise},login:function(credentials){var deferred=$q.defer();return $http.post("/auth/signin",credentials).success(function(response){deferred.resolve(response)}).error(function(error){deferred.reject(error.message||error)}),deferred.promise},logout:function(){var deferred=$q.defer();return $http.get("/auth/signout").success(function(response){deferred.resolve(null)}).error(function(error){deferred.reject(error.message||error)}),deferred.promise},signup:function(credentials){var deferred=$q.defer();return $http.post("/auth/signup",credentials).success(function(response){deferred.resolve(response)}).error(function(error){deferred.reject(error.message||error)}),deferred.promise},resetPassword:function(passwordDetails,token){var deferred=$q.defer();return $http.get("/auth/password/"+token,passwordDetails).success(function(response){deferred.resolve()}).error(function(error){deferred.reject(error.message||error)}),deferred.promise},askForPasswordReset:function(credentials){var deferred=$q.defer();return $http.post("/auth/forgot",credentials).success(function(response){deferred.resolve(response)}).error(function(error){deferred.reject(error.message||error)}),deferred.promise}};return userService}]),angular.module("users").factory("Users",["$resource",function($resource){return $resource("users",{},{update:{method:"PUT"}})}]); \ No newline at end of file diff --git a/public/modules/forms/config/forms.client.config.js b/public/modules/forms/config/forms.client.config.js index fe459902..4086df08 100644 --- a/public/modules/forms/config/forms.client.config.js +++ b/public/modules/forms/config/forms.client.config.js @@ -1,6 +1,6 @@ 'use strict'; -// Configuring the Articles module +// Configuring the Forms drop-down menus angular.module('forms').run(['Menus', function(Menus) { // Set top bar menu items @@ -30,4 +30,10 @@ angular.module('forms').run(['Menus', }).length; return valid_count; }; -}); \ No newline at end of file +}).config(['$provide', function ($provide){ + $provide.decorator('accordionDirective', function($delegate) { + var directive = $delegate[0]; + directive.replace = true; + return $delegate; + }); +}]); \ No newline at end of file diff --git a/public/modules/forms/config/forms.client.routes.js b/public/modules/forms/config/forms.client.routes.js index 3b4ca210..a2ebdc1c 100644 --- a/public/modules/forms/config/forms.client.routes.js +++ b/public/modules/forms/config/forms.client.routes.js @@ -17,13 +17,15 @@ angular.module('forms').config(['$stateProvider', state('viewForm', { url: '/forms/:formId/admin', templateUrl: 'modules/forms/views/view-form.client.view.html', + data: { + permissions: [ 'editForm' ] + } }). state('viewPublicForm', { url: '/forms/:formId', templateUrl: 'modules/forms/views/view-public-form.client.view.html', data: { hideNav: true, - hideFooter: false }, }). state('editForm', { diff --git a/public/modules/forms/controllers/create-form.client.controller.js b/public/modules/forms/controllers/create-form.client.controller.js index e3df4c96..d636315c 100644 --- a/public/modules/forms/controllers/create-form.client.controller.js +++ b/public/modules/forms/controllers/create-form.client.controller.js @@ -1,125 +1,122 @@ -'use strict'; +// 'use strict'; -angular.module('forms').controller('EditFormController', ['$scope', '$state', '$rootScope', 'Upload', '$stateParams', 'FormFields', 'Forms', 'CurrentForm', '$modal', '$location', '$http', - function ($scope, $state, $rootScope, Upload, $stateParams, FormFields, Forms, CurrentForm, $modal, $location, $http) { - $scope.form = {}; - $scope.isNewForm = false; - $scope.log = ''; - $scope.pdfLoading = false; - var _current_upload = null; +// angular.module('forms').controller('EditFormController', ['$scope', '$state', '$rootScope', 'Upload', '$stateParams', 'FormFields', 'Forms', 'CurrentForm', '$modal', '$location', '$http', +// function ($scope, $state, $rootScope, Upload, $stateParams, FormFields, Forms, CurrentForm, $modal, $location, $http) { +// $scope.form = {}; +// $scope.isNewForm = false; +// $scope.log = ''; +// $scope.pdfLoading = false; +// var _current_upload = null; - // Get current form if it exists, or create new one - if($stateParams.formId){ - Forms.get({ formId: $stateParams.formId}, function(form){ - $scope.form = angular.fromJson(angular.toJson(form)); - console.log($scope.form); - }); - } else { - $scope.form.form_fields = []; - $scope.isNewForm = true; - } +// // Get current form if it exists, or create new one +// if($stateParams.formId){ +// Forms.get({ formId: $stateParams.formId}, function(form){ +// $scope.form = angular.fromJson(angular.toJson(form)); +// console.log($scope.form); +// }); +// } else { +// $scope.form.form_fields = []; +// $scope.isNewForm = true; +// } - //PDF Functions - $scope.cancelUpload = function(){ - _current_upload.abort(); - $scope.pdfLoading = false; - $scope.removePDF(); - }; +// //PDF Functions +// $scope.cancelUpload = function(){ +// _current_upload.abort(); +// $scope.pdfLoading = false; +// $scope.removePDF(); +// }; - $scope.removePDF = function(){ - $scope.form.pdf = null; - $scope.form.isGenerated = false; - $scope.form.autofillPDFs = false; +// $scope.removePDF = function(){ +// $scope.form.pdf = null; +// $scope.form.isGenerated = false; +// $scope.form.autofillPDFs = false; - console.log('form.pdf: '+$scope.form.pdf+' REMOVED'); - }; +// console.log('form.pdf: '+$scope.form.pdf+' REMOVED'); +// }; - $scope.uploadPDF = function(files) { +// $scope.uploadPDF = function(files) { - if (files && files.length) { - // for (var i = 0; i < files.length; i++) { - var file = files[0]; - _current_upload = Upload.upload({ - url: '/upload/pdf', - fields: { - 'user': $scope.user, - 'form': $scope.form - }, - file: file - }).progress(function (evt) { - var progressPercentage = parseInt(100.0 * evt.loaded / evt.total); - $scope.log = 'progress: ' + progressPercentage + '% ' + - evt.config.file.name + '\n' + $scope.log; - $scope.pdfLoading = true; - }).success(function (data, status, headers, config) { - $scope.log = 'file ' + data.originalname + ' uploaded as '+ data.name +'. JSON: ' + JSON.stringify(data) + '\n' + $scope.log; - console.log($scope.form.pdf); - $scope.form.pdf = angular.fromJson(angular.toJson(data)); - $scope.pdfLoading = false; +// if (files && files.length) { +// // for (var i = 0; i < files.length; i++) { +// var file = files[0]; +// _current_upload = Upload.upload({ +// url: '/upload/pdf', +// fields: { +// 'user': $scope.user, +// 'form': $scope.form +// }, +// file: file +// }).progress(function (evt) { +// var progressPercentage = parseInt(100.0 * evt.loaded / evt.total); +// $scope.log = 'progress: ' + progressPercentage + '% ' + +// evt.config.file.name + '\n' + $scope.log; +// $scope.pdfLoading = true; +// }).success(function (data, status, headers, config) { +// $scope.log = 'file ' + data.originalname + ' uploaded as '+ data.name +'. JSON: ' + JSON.stringify(data) + '\n' + $scope.log; +// console.log($scope.form.pdf); +// $scope.form.pdf = angular.fromJson(angular.toJson(data)); +// $scope.pdfLoading = false; - console.log($scope.log); - console.log('$scope.pdf: '+$scope.form.pdf.name); - if(!$scope.$$phase){ - $scope.$apply(); - } - }).error(function(err){ - $scope.pdfLoading = false; - console.log('Error occured during upload.\n'); - console.log(err); - }); - // } - } - }; +// console.log($scope.log); +// console.log('$scope.pdf: '+$scope.form.pdf.name); +// if(!$scope.$$phase){ +// $scope.$apply(); +// } +// }).error(function(err){ +// $scope.pdfLoading = false; +// console.log('Error occured during upload.\n'); +// console.log(err); +// }); +// // } +// } +// }; - $rootScope.goToWithId = function(route, id) { - $state.go(route, {'formId': id}, {reload: true}); - }; +// $rootScope.goToWithId = function(route, id) { +// $state.go(route, {'formId': id}, {reload: true}); +// }; - // Create new Form - $rootScope.createOrUpdate = function() { +// // Create new Form +// $rootScope.createOrUpdate = function() { - if($scope.isNewForm){ - // Create new Form object - var form = new Forms($scope.form); +// if($scope.isNewForm){ +// // Create new Form object +// var form = new Forms($scope.form); - $http.post('/forms', {form: $scope.form}) - .success(function(data, status, headers){ - console.log('form created'); +// $http.post('/forms', {form: $scope.form}) +// .success(function(data, status, headers){ +// console.log('form created'); - // Clear form fields - $scope.form = {}; - // Redirect after save - $scope.goToWithId('viewForm', $scope.form._id); - }).error(function(errorResponse){ - console.log(errorResponse); - $scope.error = errorResponse; - }); - } else{ - $scope.update(); - } - }; +// // Clear form fields +// $scope.form = {}; +// // Redirect after save +// $scope.goToWithId('viewForm', $scope.form._id); +// }).error(function(errorResponse){ +// console.log(errorResponse); +// $scope.error = errorResponse; +// }); +// } else{ +// $scope.update(function(err){ +// console.log('done updating'); +// }); +// } +// }; - // Update existing Form - $rootScope.update = function() { - var form = new Forms($scope.form); - console.log('update form'); - console.log($scope.form); +// // Update existing Form +// $rootScope.update = function(cb) { +// var form = new Forms($scope.form); +// console.log('update form'); +// console.log($scope.form); - $http.put('/forms/'+$scope.form._id, {form: $scope.form}) - .success(function(data, status, headers){ - console.log('form updated successfully'); - $scope.goToWithId('viewForm', $scope.form._id); - }).error(function(err){ - console.log('Error occured during form UPDATE.\n'); - console.log(err); - }); - // form.$update({formId: $scope.form._id}, function(response) { - // console.log('form successfully updated'); - // $scope.goToWithId('viewForm', response._id); - // }, function(errorResponse) { - // console.log(errorResponse.data.message); - // $scope.error = errorResponse.data.message; - // }); - }; - } -]); +// $http.put('/forms/'+$scope.form._id, {form: $scope.form}) +// .success(function(data, status, headers){ +// console.log('form updated successfully'); +// $scope.goToWithId('viewForm', $scope.form._id); +// cb(null); +// }).error(function(err){ +// console.log('Error occured during form UPDATE.\n'); +// console.log(err); +// cb(err); +// }); +// }; +// } +// ]); diff --git a/public/modules/forms/controllers/view-form.client.controller.js b/public/modules/forms/controllers/view-form.client.controller.js index 8f402c58..a9344488 100644 --- a/public/modules/forms/controllers/view-form.client.controller.js +++ b/public/modules/forms/controllers/view-form.client.controller.js @@ -14,10 +14,30 @@ angular.module('forms').controller('ViewFormController', ['$rootScope', '$scope' rows: [] }; + // Return all user's Forms + $scope.findAll = function() { + $scope.myforms = Forms.query(); + }; + + // Find a specific Form + $scope.findOne = function() { + $scope.myform = Forms.get({ + formId: $stateParams.formId + }); + CurrentForm.setForm($scope.myform); + }; + + $scope.goToWithId = function(route, id) { + $state.go(route, {'formId': id}, {reload: true}); + }; + + + $scope.setForm = function (form) { $scope.myForm = form; }; + //Modal functions $scope.openCreateModal = function(){ if(!$scope.showCreateModal){ $scope.showCreateModal = true; @@ -29,53 +49,9 @@ angular.module('forms').controller('ViewFormController', ['$rootScope', '$scope' } }; - //Create new form - $scope.createNew = function(){ - var form = {}; - form.title = $scope.myForm.name.$modelValue; - form.language = $scope.myForm.language.$modelValue; - console.log(form); - $scope.showCreateModal = true; - - console.log($scope.myForm); - if($scope.myForm.$valid && $scope.myForm.$dirty){ - $http.post('/forms', {form: form}) - .success(function(data, status, headers){ - console.log('form created'); - - // Clear form fields - $scope.myForm = {}; - // Redirect after save - $scope.goToWithId('viewForm', $scope.myform._id); - }).error(function(errorResponse){ - console.log(errorResponse); - // $scope.error = errorResponse.data.message; - }); - } - }; - - $scope.saveInProgress = false; - $scope.update = function() { - if(!$scope.saveInProgress){ - $scope.saveInProgress = true; - - console.log('start update()'); - - $http.put('/forms/'+$scope.myform._id, {form: $scope.myform}) - .then(function(response){ - console.log('form updated successfully'); - console.log('$scope.saveInProgress: '+$scope.saveInProgress); - // $rootScope.goToWithId('viewForm', $scope.myform._id); - }).catch(function(response){ - console.log('Error occured during form UPDATE.\n'); - console.log(response.data); - }).finally(function() { - $scope.saveInProgress = false; - }); - } - }; - - //Table Functions + /* + * Table Functions + */ $scope.toggleAllCheckers = function(){ console.log('toggleAllCheckers'); for(var i=0; i<$scope.table.rows.length; i++){ @@ -85,59 +61,72 @@ angular.module('forms').controller('ViewFormController', ['$rootScope', '$scope' $scope.toggleObjSelection = function($event, description) { $event.stopPropagation(); console.log('checkbox clicked'); - }; - - $scope.rowClicked = function(obj) { - console.log('row clicked'); + }; + $scope.rowClicked = function(obj) { + // console.log('row clicked'); obj.selected = !obj.selected; - }; + }; //show submissions of Form $scope.showSubmissions = function(){ $scope.viewSubmissions = true; - if(!$scope.table.rows.length){ - $http.get('/forms/'+$scope.myform._id+'/submissions') - .success(function(data, status, headers){ - console.log(data); - $scope.submissions = data; - $scope.table.rows = data; - console.log('form submissions successfully fetched'); - }) - .error(function(err){ - console.log('Could not fetch form submissions.\nError: '+err); - }); - } else if(!$scope.submissions.length){ - $http.get('/forms/'+$scope.myform._id+'/submissions') - .success(function(data, status, headers){ - $scope.submissions = data; - $scope.table.rows = data; - console.log($scope.table.rows); - console.log('form submissions successfully fetched'); - }) - .error(function(err){ - console.log('Could not fetch form submissions.\nError: '+err); - }); - } - console.log($scope.submissions); - }; + $http.get('/forms/'+$scope.myform._id+'/submissions') + .success(function(data, status, headers){ + console.log(data[0].form_fields); + + var _data = Array(); + for(var i=0; i .field-input input { - width:500px; + width:100%; } form.submission-form .row.field .field-input > input:focus { font-size:1em; } form .row.field.textfield > .field-input > input{ padding:0.45em 0.9em; - width:600px; + width:100%; + line-height:160%; + } + + form .row.field.dropdown > .field-input { + height: 34px; + overflow: hidden; + } + form .row.field.dropdown > .field-input > select{ + padding:0.45em 0.9em; + width:100%; + background: transparent; + font-size: 16px; + border: 1px solid #ccc; + height: 34px; } div.config-form > .row { diff --git a/public/modules/forms/directives/auto-save.client.directive.js b/public/modules/forms/directives/auto-save.client.directive.js index 5ee9d4a4..6fbc9711 100644 --- a/public/modules/forms/directives/auto-save.client.directive.js +++ b/public/modules/forms/directives/auto-save.client.directive.js @@ -4,16 +4,20 @@ angular.module('forms').directive('autoSaveForm', ['$rootScope', '$timeout', fun return { require: ['^form'], + // scope: { + // callback: '&autoSaveCallback' + // }, link: function($scope, $element, $attrs, $ctrls) { - if(!$rootScope.watchCount === undefined){ + if($rootScope.watchCount === undefined){ $rootScope.watchCount = 0; } + var difference = function(array){ var rest = Array.prototype.concat.apply(Array.prototype, Array.prototype.slice.call(arguments, 1)); var containsEquals = function(obj, target) { - if (obj == null) return false; + if (obj === null) return false; return _.any(obj, function(value) { return _.isEqual(value, target); }); @@ -28,7 +32,7 @@ angular.module('forms').directive('autoSaveForm', ['$rootScope', '$timeout', fun var expression = $attrs.autoSaveForm || 'true'; $scope.$on('ngRepeatStarted', function(ngRepeatFinishedEvent) { - $scope.finishedRender = false; + // $scope.finishedRender = false; $rootScope.watchCount = 0; }); $scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) { @@ -36,20 +40,20 @@ angular.module('forms').directive('autoSaveForm', ['$rootScope', '$timeout', fun }); $scope.$watch('myform.form_fields', function(newValue, oldValue) { - + // console.log('watchCount: '+$rootScope.watchCount); if(difference(oldValue,newValue).length === 0 || oldValue === undefined){ return; } - // console.log('\n\n-------\n$pristine: '+( $formCtrl.$pristine ) ); - // console.log('$dirty: '+( $formCtrl.$dirty ) ); - // console.log('form_fields changed: '+difference(oldValue.form_fields,newValue.form_fields).length ); + // console.log('\n\n----------\n$dirty: '+( $formCtrl.$dirty ) ); + // console.log('form_fields changed: '+difference(oldValue,newValue).length ); // console.log('$valid: '+$formCtrl.$valid); // console.log('finishedRender: '+$scope.finishedRender); // console.log('saveInProgress: '+$scope.saveInProgress); if($scope.finishedRender && ($formCtrl.$dirty || difference(oldValue,newValue).length !== 0) ) { $rootScope.watchCount++; + if($rootScope.watchCount === 1) { if(savePromise) { @@ -59,15 +63,22 @@ angular.module('forms').directive('autoSaveForm', ['$rootScope', '$timeout', fun savePromise = $timeout(function() { savePromise = null; - // Still valid? - // if($formCtrl.$valid) { - if($scope.$eval(expression) !== false) { - console.log('Form data persisted -- setting pristine flag'); - $formCtrl.$setPristine(); - } - // } + $rootScope[$attrs.autoSaveCallback]( + function(err){ + if(!err){ + console.log('Form data persisted -- setting pristine flag'); + console.log('\n\n---------\nUpdate form CLIENT'); + console.log(Date.now()); + $rootScope.watchCount = 0; + $formCtrl.$setPristine(); + }else{ + console.log('Error form data NOT persisted'); + console.log(err); + } + }); }); + } } diff --git a/public/modules/forms/directives/configure-form.client.directive.js b/public/modules/forms/directives/configure-form.client.directive.js index 98920956..ef3c1c73 100644 --- a/public/modules/forms/directives/configure-form.client.directive.js +++ b/public/modules/forms/directives/configure-form.client.directive.js @@ -7,9 +7,10 @@ angular.module('forms').directive('configureFormDirective', ['$rootScope','$http $scope.log = ''; $scope.pdfLoading = false; $scope.languages = $rootScope.languages; + var _current_upload = null; - $scope.createOrUpdate = $rootScope.createOrUpdate; $scope.resetForm = $rootScope.resetForm; + $scope.update = $rootScope.update; var _unbindedPdfFields = $scope.pdfFields; diff --git a/public/modules/forms/directives/edit-form.client.directive.js b/public/modules/forms/directives/edit-form.client.directive.js index c84a0f9e..689f457a 100644 --- a/public/modules/forms/directives/edit-form.client.directive.js +++ b/public/modules/forms/directives/edit-form.client.directive.js @@ -13,6 +13,58 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', '$q', '$ht //Populate local scope with rootScope methods/variables $scope.update = $rootScope.update; + /* + ** FormFields (ui-sortable) drag-and-drop configuration + */ + $scope.dropzone = { + handle: ' .handle' + } + + // $scope.draggable = { + // connectWith: ".dropzone", + // start: function (e, ui) { + // // $scope.$apply(function() { + // // $scope.dragging = true + // // }); + // $('.dropzone').sortable('refresh'); + // }, + // update: function (e, ui) { + // var isInDropzone = $(e.target).parentsUntil('.panel-group').hasClass('dropzone'); + + // console.log('isInDropzone: '+isInDropzone); + // //Disable drag and drop if we aren't in dropzone + // if(!isInDropzone){ + // ui.item.sortable.cancel(); + // } + // }, + // stop: function (e, ui) { + // var isInDropzone = $(e.target).parentsUntil('.panel-group').hasClass('dropzone'); + + // //Disable drag and drop if we aren't in dropzone + // if(isInDropzone){ + // console.log($(e.target)); + // } + + // // if (ui.item.sortable.droptarget === undefined) { + // // $scope.$apply($scope.dragging = false); + // // return; + // // }else if (ui.item.sortable.droptarget[0].classList[0] === "dropzone") { + // // // run code when item is dropped in the dropzone + // // $scope.$apply($scope.dragging = false); + // // }else{ + // // // $scope.$apply($scope.dragging = false); + // // } + // // console.log('has class .dropzone :'+); + // // if ($(e.target).hasClass('dropzone') && ui.item.sortable.droptarget && e.target != ui.item.sortable.droptarget[0] ) { + // // // restore original types + // // $scope.addField.types = FormFields.fields; + // // } + + + // } + // }; + + //Populate AddField with all available form field types $scope.addField = {}; $scope.addField.types = FormFields.fields; @@ -37,7 +89,7 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', '$q', '$ht // console.log($scope.addField.types[i].name === fieldType); if($scope.addField.types[i].name === fieldType){ $scope.addField.types[i].lastAddedID++; - console.log($scope.addField.types[i].lastAddedID); + // console.log($scope.addField.types[i].lastAddedID); fieldTitle = $scope.addField.types[i].value+$scope.addField.types[i].lastAddedID; break; } @@ -52,14 +104,16 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', '$q', '$ht // put newField into fields array $scope.myform.form_fields.unshift(newField); - console.log($scope.myform.form_fields.length); + console.log('\n\n---------\nAdded field CLIENT'); + console.log(Date.now()); + // console.log($scope.myform.form_fields.length); }; // deletes particular field on button click $scope.deleteField = function (hashKey){ - console.log($scope.myform.form_fields); + // console.log($scope.myform.form_fields); for(var i = 0; i < $scope.myform.form_fields.length; i++){ - console.log($scope.myform.form_fields[i].$$hashKey === hashKey); + // console.log($scope.myform.form_fields[i].$$hashKey === hashKey); if($scope.myform.form_fields[i].$$hashKey === hashKey){ $scope.myform.form_fields.splice(i, 1); break; @@ -77,13 +131,12 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', '$q', '$ht // add new option to the field $scope.addOption = function (field){ - if(!field.field_options) - field.field_options = []; + if(!field.fieldOptions) field.fieldOptions = []; var lastOptionID = 0; - if(field.field_options[field.field_options.length-1]) - lastOptionID = field.field_options[field.field_options.length-1].option_id; + if(field.fieldOptions[field.fieldOptions.length-1]) + lastOptionID = field.fieldOptions[field.fieldOptions.length-1].option_id; // new option's id var option_id = lastOptionID + 1; @@ -94,15 +147,15 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', '$q', '$ht 'option_value' : option_id }; - // put new option into field_options array - field.field_options.push(newOption); + // put new option into fieldOptions array + field.fieldOptions.push(newOption); }; // delete particular option $scope.deleteOption = function (field, option){ - for(var i = 0; i < field.field_options.length; i++){ - if(field.field_options[i].option_id === option.option_id){ - field.field_options.splice(i, 1); + for(var i = 0; i < field.fieldOptions.length; i++){ + if(field.fieldOptions[i].option_id === option.option_id){ + field.fieldOptions.splice(i, 1); break; } } @@ -110,10 +163,11 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', '$q', '$ht // decides whether field options block will be shown (true for dropdown and radio fields) $scope.showAddOptions = function (field){ - if(field.fieldType == 'dropdown' || field.fieldType == 'checkbox' || field.fieldType == 'scale' || field.fieldType == 'rating' || field.fieldType == 'radio') + if(field.fieldType === 'dropdown' || field.fieldType === 'checkbox' || field.fieldType === 'scale' || field.fieldType === 'rating' || field.fieldType === 'radio'){ return true; - else + } else { return false; + } }; }, diff --git a/public/modules/forms/directives/field-icon.client.directive.js b/public/modules/forms/directives/field-icon.client.directive.js index 86b26064..da415ec6 100644 --- a/public/modules/forms/directives/field-icon.client.directive.js +++ b/public/modules/forms/directives/field-icon.client.directive.js @@ -10,20 +10,20 @@ angular.module('forms').directive('fieldIconDirective', function($http, $compile }, controller: function($scope){ var iconTypeMap = { - "textfield": "fa fa-pencil-square-o", - "dropdown": "fa fa-th-list", - "date": "fa fa-calendar", - "checkbox": "fa fa-check-square-o", - "radio": "fa fa-dot-circle-o", - "email": "fa fa-envelope-o", - "textarea": "fa fa-pencil-square", - "legal": "fa fa-legal", - "file": "fa fa-cloud-upload", - "rating": "fa fa-star-half-o", - "link": "fa fa-link", - "scale": "fa fa-sliders", - "stripe": "fa fa-credit-card", - "statement": "fa fa-quote-left", + 'textfield': 'fa fa-pencil-square-o', + 'dropdown': 'fa fa-th-list', + 'date': 'fa fa-calendar', + 'checkbox': 'fa fa-check-square-o', + 'radio': 'fa fa-dot-circle-o', + 'email': 'fa fa-envelope-o', + 'textarea': 'fa fa-pencil-square', + 'legal': 'fa fa-legal', + 'file': 'fa fa-cloud-upload', + 'rating': 'fa fa-star-half-o', + 'link': 'fa fa-link', + 'scale': 'fa fa-sliders', + 'stripe': 'fa fa-credit-card', + 'statement': 'fa fa-quote-left', } $scope.typeIcon = iconTypeMap[$scope.typeName]; }, diff --git a/public/modules/forms/directives/field.client.directive.js b/public/modules/forms/directives/field.client.directive.js index 7592d98c..caa77296 100644 --- a/public/modules/forms/directives/field.client.directive.js +++ b/public/modules/forms/directives/field.client.directive.js @@ -9,7 +9,7 @@ var __indexOf = [].indexOf || function(item) { }; angular.module('forms').directive('fieldDirective', function($http, $compile) { - + var getTemplateUrl = function(field) { @@ -33,6 +33,17 @@ angular.module('forms').directive('fieldDirective', function($http, $compile) { var linker = function(scope, element) { scope.field.required = scope.required; + + //Set format only if field is a date + if(scope.field.fieldType === 'date'){ + scope.dateOptions = { + changeYear: true, + changeMonth: true, + altFormat: "mm/dd/yyyy", + yearRange: '1900:-0', + defaultDate: 0, + }; + } // GET template content from path var templateUrl = getTemplateUrl(scope.field); diff --git a/public/modules/forms/directives/form-locator.client.directive.js b/public/modules/forms/directives/form-locator.client.directive.js index 9d9f8ee4..4b9d3c81 100644 --- a/public/modules/forms/directives/form-locator.client.directive.js +++ b/public/modules/forms/directives/form-locator.client.directive.js @@ -4,5 +4,5 @@ angular.module('forms').directive('formLocator', function() { link: function(scope) { scope.$emit('formLocator'); } - } + }; }); \ No newline at end of file diff --git a/public/modules/forms/directives/form.client.directive.js b/public/modules/forms/directives/form.client.directive.js index 509e79cc..aed747bd 100644 --- a/public/modules/forms/directives/form.client.directive.js +++ b/public/modules/forms/directives/form.client.directive.js @@ -29,6 +29,10 @@ angular.module('forms').directive('formDirective', ['$http', '$timeout', 'timeCo alert('Form canceled..'); }; + $scope.reloadForm = function(){ + $scope.form.submitted = false; + } + }, templateUrl: './modules/forms/views/directiveViews/form/form.html', restrict: 'E', diff --git a/public/modules/forms/directives/on-finish-render.client.directive.js b/public/modules/forms/directives/on-finish-render.client.directive.js index 1612b2ae..d4b7fbb2 100644 --- a/public/modules/forms/directives/on-finish-render.client.directive.js +++ b/public/modules/forms/directives/on-finish-render.client.directive.js @@ -16,5 +16,5 @@ angular.module('forms').directive('onFinishRender', function ($rootScope, $timeo }, 500); } } - } + }; }); diff --git a/public/modules/forms/forms.client.module.js b/public/modules/forms/forms.client.module.js index 1a783aa9..c364bb30 100644 --- a/public/modules/forms/forms.client.module.js +++ b/public/modules/forms/forms.client.module.js @@ -1,4 +1,4 @@ 'use strict'; // Use Application configuration module to register a new module -ApplicationConfiguration.registerModule('forms', ['ngFileUpload', 'users']); \ No newline at end of file +ApplicationConfiguration.registerModule('forms', ['ngFileUpload', 'ui.date', 'ui.sortable', 'users']); \ No newline at end of file diff --git a/public/modules/forms/views/create-form.client.view.html b/public/modules/forms/views/create-form.client.view.html deleted file mode 100644 index 5cc03c6a..00000000 --- a/public/modules/forms/views/create-form.client.view.html +++ /dev/null @@ -1,231 +0,0 @@ - - -
-
-

Create your form


-
-

Select field type you want to add to the form below and click on 'Add Field' button. Don't forget to set field properties. After you finish creating the form, you can preview the form by clicking Preview Form button.

-
-
-
-

Edit your form


-
- - -
-
-
-

Form Title

-
-
-

-
- -
-
-

Fields

-
-
- -
-
- - -
- -
-

No fields added yet.

- - - -
- - -
-
Field ID:
-
{{field.client_id}}
-
-
-
Field Type:
-
{{field.fieldType}}
-
- -

- -
-
Field Title:
-
-
-
-
Field Default Value:
-
-
-
-
Field Options:
-
-
- - - Value: {{ option.option_value }} -
- -
-
- -

- -
-
Required:
-
- - -
-
- -

- -
-
Disabled:
-
- - -
-
-
-
-
-
-
- -
-
-

Form PDF

-
-
- -
-
-
Upload your PDF
-
-
-
-
- -
- {{form.pdf.originalname}} -
-
-
- - - - -
- - Upload your PDF -
-
-
-
-
-
-
-
Autogenerate Form?
-
-

-
- - - - - - -
-
- -
-
-
Save Submissions as PDFs?
-
-

-
- - - - - - -
-
- -

-
-
- -
-
- -
-
- -
-
- -

- - - -
- - - -
- - -

- -

-
-
- -
\ No newline at end of file diff --git a/public/modules/forms/views/directiveViews/field/date.html b/public/modules/forms/views/directiveViews/field/date.html index ced056c6..1daacd70 100755 --- a/public/modules/forms/views/directiveViews/field/date.html +++ b/public/modules/forms/views/directiveViews/field/date.html @@ -3,7 +3,7 @@ * required
- +
diff --git a/public/modules/forms/views/directiveViews/field/dropdown.html b/public/modules/forms/views/directiveViews/field/dropdown.html index fcac1641..b97dd331 100755 --- a/public/modules/forms/views/directiveViews/field/dropdown.html +++ b/public/modules/forms/views/directiveViews/field/dropdown.html @@ -1,8 +1,8 @@ -
-
{{field.title}}
-
+