Compare commits

...

192 commits

Author SHA1 Message Date
David Baldwynn
8bda1a3a97 fixed hasAuthorization bug 2017-11-21 17:40:05 -08:00
David Baldwynn
784ae9011e fixed security flaw with removeSensitiveModelData 2017-11-21 17:26:52 -08:00
David Baldwynn
bd4af8aa7c
Merge branch 'fixUXBugs' into updateSetupScript 2017-11-21 16:01:33 -08:00
David Baldwynn
bc2f49b090 Merge branch 'fixUXBugs' into updateSetupScript 2017-11-21 15:59:18 -08:00
David Baldwynn
65a4e3fcba
Merge pull request #272 from tellform/addSeleniumTests
Add selenium tests
2017-11-21 15:57:16 -08:00
David Baldwynn
c561130f75 Merge branch 'fixUXBugs' into addSeleniumTests 2017-11-21 15:56:49 -08:00
David Baldwynn
6213ec227d updated field-icon test 2017-11-21 15:56:10 -08:00
David Baldwynn
1163debc33 made all server models exportable 2017-11-21 15:51:24 -08:00
David Baldwynn
4d4bb65298 disabled create_admin script in test mode 2017-11-21 15:49:58 -08:00
David Baldwynn
4f82a55227 added selenium tests 2017-11-21 15:35:06 -08:00
David Baldwynn
188c48b26c fixed password issue with setup script admin creation 2017-11-21 15:33:07 -08:00
David Baldwynn
df83c6bb93 fixed create_admin script 2017-11-21 15:30:09 -08:00
David Baldwynn
f076b482e9 moved to node v7 2017-11-21 14:06:08 -08:00
David Baldwynn
c9057b4a8a Merge branch '2.20' into addSeleniumTests 2017-11-21 13:45:28 -08:00
David Baldwynn
b2e027e0bf Merge branch '2.20' into updateSetupScript 2017-11-21 13:41:42 -08:00
David Baldwynn
0a4b83186a Merge branch 'fixUXBugs' of github.com:tellform/tellform into fixUXBugs 2017-11-21 13:40:47 -08:00
David Baldwynn
20cc79f218 Merge branch '2.20' into fixUXBugs 2017-11-21 13:40:27 -08:00
David Baldwynn
9ff247fde7 Merge branch 'master' into 2.20 2017-11-21 13:39:27 -08:00
David Baldwynn
0b22d06326
Merge pull request #268 from tellform/fixFormUpdate
Added tests for Form Updated and fixed bugs
2017-11-21 13:34:01 -08:00
David Baldwynn
7c6dc90592 added ASCII art for setup script 2017-11-21 13:33:25 -08:00
David Baldwynn
39125972c3 removed user and pass from mongoose options 2017-11-21 13:12:34 -08:00
David Baldwynn
31985ddda7 added setup script that runs on first run of server 2017-11-21 13:10:43 -08:00
David Baldwynn
563b8c6cd2 Modified setup script to search for admin user by email AND username 2017-11-21 12:37:57 -08:00
David Baldwynn
c97f207699 added username validation to setup script 2017-11-21 12:33:47 -08:00
David Baldwynn
e27f27f7f0 added email validation to setup script 2017-11-21 12:31:13 -08:00
David Baldwynn
acb067d626 fixed incorrect ENV variable name for redis url 2017-11-21 11:50:35 -08:00
David Baldwynn
9d2a548dca Added new form_route tests for update route 2017-11-21 11:46:57 -08:00
David Baldwynn
8c1964146f removed newOptionSchema property from FieldSchema 2017-11-21 11:37:48 -08:00
David Baldwynn
f6546deb59 removed globalId 2017-11-21 11:37:01 -08:00
David Baldwynn
fb26fbd4f9 disabled subdomains for server-side tests 2017-11-21 10:54:23 -08:00
David Baldwynn
d8268e4a9b Added test for preserving visitorData on update and removed debugger statements 2017-11-21 10:50:58 -08:00
David Baldwynn
68858ca1a3 fixed fieldType constant 2017-11-21 10:42:32 -08:00
David Baldwynn
2bd1598a01 Handled case in setup script where admin user already exists 2017-11-21 10:12:29 -08:00
David Baldwynn
a08e5c5317 removed username from .env file in setup script 2017-11-21 09:59:42 -08:00
David Baldwynn
7107bab614 added support for custom smtp server in setup script 2017-11-21 09:58:44 -08:00
David Baldwynn
2993c6e4f8 removed reCAPTCHA token from setup script 2017-11-21 07:58:11 -08:00
David Baldwynn
fca5524751 fixed admin script 2017-11-21 07:39:00 -08:00
David Baldwynn
801677bdd7 fixed angular-ui sorting bug 2017-11-21 07:24:22 -08:00
David Baldwynn
91528889e5 got tests to pass 2017-11-21 00:05:01 -08:00
David Baldwynn
94bcae9d5b added clarifying comment for formatDeviceData 2017-11-20 21:24:20 -08:00
David Baldwynn
88299a2b27 simplified form_field property deletion logic in form_submission model 2017-11-20 21:21:32 -08:00
David Baldwynn
632d2b409d removed submissions property from form model 2017-11-20 21:15:47 -08:00
David Baldwynn
e0ad256904 removed setting analytics to empty array after submissions are deleted 2017-11-20 21:11:28 -08:00
David Baldwynn
2c9e931257 removed unnessecary console.log() statement from passport helpers 2017-11-20 21:07:04 -08:00
David Baldwynn
c7d28d641c removed commented out code in send-email-notifications 2017-11-20 21:05:58 -08:00
David Baldwynn
eb658af30d got jshint to pass 2017-11-20 21:04:07 -08:00
David Baldwynn
a165cdc242 moved privateFields dict to constants.js 2017-11-20 20:58:53 -08:00
David Baldwynn
75d06037a3 removed formValidity filter from admin js 2017-11-20 20:54:38 -08:00
David Baldwynn
9c24d15a32 remove empty test case from header.client.controller.test.js 2017-11-20 20:30:41 -08:00
David Baldwynn
4d8fe4c23a removed redundant ng-true-value and ng-false-value from bs-switch 2017-11-20 20:28:56 -08:00
David Baldwynn
218d96dc2b changed loading to saveInProgress for v-busy button in configure directive 2017-11-20 20:27:14 -08:00
David Baldwynn
00d437ea1e added 'copied' label after copy button is clicked 2017-11-20 20:23:25 -08:00
David Baldwynn
f3f9232c62 removed getUserState from Auth service 2017-11-20 19:58:43 -08:00
David Baldwynn
1c4ce87084 removed deprecated properties from mock models 2017-11-20 19:56:49 -08:00
David Baldwynn
a3e0e89019 removed unused template property from configureTabs array 2017-11-20 18:57:43 -08:00
David Baldwynn
8d3e605261 fixed create new form bug 2017-11-20 18:30:16 -08:00
David Baldwynn
5ad2875378 fixed icon bug on field dragging 2017-11-19 22:42:59 -08:00
David Baldwynn
d74c04fa86 added padding for share panel 2017-11-19 22:10:07 -08:00
David Baldwynn
f897b39549 fixed disabled switch for respondent notifications bug 2017-11-19 21:37:27 -08:00
David Baldwynn
66409c15d8 fixed bugs in listForms duplication 2017-11-19 21:31:26 -08:00
David Baldwynn
9d040e9672 fixed flashing between configure tabs 2017-11-19 21:26:49 -08:00
David Baldwynn
56b7497dca set default form language to user's language 2017-11-19 21:09:57 -08:00
David Baldwynn
14243285bb fixed form create modal bug 2017-11-19 21:07:00 -08:00
David Baldwynn
2523631d96 fixed lcovMerge 2017-11-19 18:42:10 -08:00
David Baldwynn
a8ae7e2105 fixed ng-quill dependency 2017-11-19 18:36:32 -08:00
David Baldwynn
902d2b31e2 fixed test for settings controller 2017-11-19 18:32:18 -08:00
David Baldwynn
0e7ee4261a added jspdf to bower deps 2017-11-19 21:20:46 -05:00
David Baldwynn
e01b4d53e3 disabled key to option when # of options > 26 2017-11-19 21:18:31 -05:00
David Baldwynn
b6b93d15f0 fixed delete button state change bug 2017-11-19 21:15:26 -05:00
David Baldwynn
153d12a4e3 Merge branch '2.20' into fixUXBugs 2017-11-19 20:44:59 -05:00
David Baldwynn
70eec8731c fixed key to option 2017-11-19 16:53:33 -08:00
David Baldwynn
a217567b9b fixed key-to-option attributes 2017-11-19 16:10:30 -08:00
David Baldwynn
58380d23e3 fixed double scrolling bug with radio field 2017-11-19 15:38:08 -08:00
David Baldwynn
1721db9efe slowed down field scrolling 2017-11-19 15:32:15 -08:00
David Baldwynn
ff509f2b5c added updating of translation when user changes profile language 2017-11-19 15:23:38 -08:00
David Baldwynn
6a992d74ad Added tests for password, settings and verify controllers 2017-11-19 15:11:43 -08:00
David Baldwynn
cb3fe0932f refactored user settings controller 2017-11-19 13:34:39 -08:00
David Baldwynn
35ea6276a7 added tests for password controller 2017-11-19 13:31:06 -08:00
David Baldwynn
d1159087d4 added tests for authentication controller 2017-11-19 13:12:18 -08:00
David Baldwynn
0e7eb457cc got clientside tests to pass 2017-11-19 11:12:46 -08:00
David Baldwynn
c911e53609 Merge remote-tracking branch 'origin/fixUXBugs' into 2.20 2017-11-15 14:49:28 -05:00
David Baldwynn
29633b0b8b got all serverside tests to pass 2017-11-15 11:45:39 -08:00
David Baldwynn
995e94f7f5 fixed send email tests 2017-11-15 11:43:36 -08:00
David Baldwynn
ef9265576c removed complex form save logic 2017-11-15 11:23:21 -08:00
David Baldwynn
22fab93a55 added default values for analytics page 2017-11-15 11:02:28 -08:00
David Baldwynn
c21477fb02 added styling for quilljs editor 2017-11-15 10:56:10 -08:00
David Baldwynn
2a0cdcca20 added routable tabs for share form config panel 2017-11-15 10:47:57 -08:00
David Baldwynn
69f1f42816 reorganized css for admin-form 2017-11-15 10:12:01 -08:00
David Baldwynn
ab830fdc9e fixed form footer toggle in config general admin panel 2017-11-15 09:41:19 -08:00
David Baldwynn
3606c9444b fixed by zero mongodb bug for aggregation 2017-11-15 04:31:14 -08:00
David Baldwynn
965962b5e0 removed console.logs from server and client side 2017-11-15 04:19:14 -08:00
David Baldwynn
b37f4f7593 removed unused 'ispreview' attr from submit-form directive 2017-11-15 04:16:47 -08:00
David Baldwynn
d23342ac57 removed textAngular 2017-11-15 04:14:39 -08:00
David Baldwynn
522ee6f54f got 'view my form link' to open a new tab 2017-11-15 04:13:00 -08:00
David Baldwynn
6c41e6fce2 fixed signout glitching bug 2017-11-15 04:10:15 -08:00
David Baldwynn
a193d78d90 fixes language dropdown for form create modal 2017-11-15 03:57:46 -08:00
David Baldwynn
146cf9256d removed unused code from edit-form directive 2017-11-15 03:48:34 -08:00
David Baldwynn
f619b6e1ca added subject quilljs fields for self and respondent notifications 2017-11-15 03:46:40 -08:00
David Baldwynn
98d874783f got email notifications to work with quilljs 2017-11-15 02:46:54 -08:00
David Baldwynn
f3638a1d42 fixed small ux bugs 2017-11-15 00:29:45 -08:00
David Baldwynn
758eb5a786 got textAngular to semi-work with template variables 2017-11-14 23:26:47 -08:00
David Baldwynn
70801dfd96 got working for server 2017-11-14 02:45:08 -05:00
David Baldwynn
49141e6dc0
Merge pull request #262 from tellform/fixResponseCountOnFormList
Fix incorrect responses count for form list view
2017-11-13 23:02:03 -08:00
David Baldwynn
499f2c97a8
Merge pull request #261 from tellform/fixedScrollspy
Fixed ScrollSpy for fields
2017-11-13 22:18:35 -08:00
David Baldwynn
9d51608a62 added email notifications to json form data 2017-11-13 22:09:15 -08:00
David Baldwynn
56bbc68980 fixed shape dropdown for rating field 2017-11-13 21:49:16 -08:00
David Baldwynn
67a3013945 readded page loader gif for form view 2017-11-13 21:26:30 -08:00
David Baldwynn
2e5bd5beb5 fixed form validation bug with radio 2017-11-13 21:25:05 -08:00
David Baldwynn
f5e32e9548 got validity working for rating field 2017-11-13 21:07:27 -08:00
David Baldwynn
04001244d1 fixed prev and next field buttons 2017-11-13 18:22:15 -08:00
David Baldwynn
6848b740b3 moved from on('scroll' to using duScroll for tracking activeField 2017-11-13 18:14:52 -08:00
David Baldwynn
15a508cca7 change name of form assets from form-vendor.min.js to vendor.min.js 2017-11-13 16:56:24 -08:00
David Baldwynn
ed6d917e6b fixed incorrect responses count for form list view 2017-11-13 16:52:42 -08:00
David Baldwynn
09d430a7e1 added cosmetic changes to analytics panel 2017-11-13 15:21:29 -08:00
David Baldwynn
1f9a0ed1e4
Merge pull request #254 from tellform/fixSlowAnalyticsBug
Fix Slow Form Analytics Bug
2017-11-13 15:00:18 -08:00
David Baldwynn
060cd5980f Merge branch 'fixSlowAnalyticsBug' of github.com:tellform/tellform into fixSlowAnalyticsBug 2017-11-13 14:52:52 -08:00
David Baldwynn
28fe150149 Merge branch '2.20' into fixSlowAnalyticsBug 2017-11-13 14:51:53 -08:00
David Baldwynn
421d3b53b8
Merge pull request #260 from tellform/improvedAddOptionUX
Improved UX of Adding an Option
2017-11-11 21:59:47 -08:00
David Baldwynn
f786f24f7c
Merge branch '2.20' into fixSlowAnalyticsBug 2017-11-11 21:50:43 -08:00
David Baldwynn
62953a6ffc changed dropdown add option to list of strings that are seperated by lines 2017-11-11 21:44:53 -08:00
David Baldwynn
4f2682554d merged with master 2017-11-11 20:05:06 -08:00
David Baldwynn
5516e76e19 Merge branch 'fixSlowAnalyticsBug' of github.com:tellform/tellform into fixSlowAnalyticsBug 2017-11-06 15:04:42 -08:00
David Baldwynn
ffb2a23cc6 fixed duplicate npm dep in prod and dev 2017-11-06 15:03:52 -08:00
David Baldwynn
04e71be14a
Merge branch '2.20' into fixSlowAnalyticsBug 2017-11-06 14:47:01 -08:00
David Baldwynn
4fe1e8e848
Merge pull request #230 from tellform/emailNotifications
Add Email Notifications
2017-11-06 14:45:03 -08:00
David Baldwynn
68235423c8
Merge branch 'master' into fixSlowAnalyticsBug 2017-11-06 14:26:04 -08:00
David Baldwynn
a40ecad008 added aggregating for form analytics 2017-11-05 19:40:28 -08:00
David Baldwynn
a77a59c3ba Merge branch '2.20' into emailNotifications 2017-11-05 16:26:09 -08:00
David Baldwynn
43ce60b985 Merge branch 'master' into 2.20 2017-11-05 16:24:19 -08:00
David Baldwynn
8521ede885 Merge branch 'master' into 2.20 2017-11-05 15:43:07 -08:00
David Baldwynn
4e18e70b18 fix html bug with textAngular attribute in config admin panel 2017-11-05 15:37:27 -08:00
David Baldwynn
15d023c133 fixed editable variables in textAngular editor 2017-11-02 14:29:02 -07:00
David Baldwynn
72b3e31819 fixed form expected object but got array error 2017-11-02 09:57:05 -07:00
David Baldwynn
22875abab5 fixed signin validation bug 2017-11-02 09:54:14 -07:00
David Baldwynn
b2f2599fa5 fix css bug for dropdown field in admin view 2017-11-02 09:18:41 -07:00
David Baldwynn
692e4e43f5 updated styling of config panel 2017-11-01 21:15:27 -07:00
David Baldwynn
4d94ed50ee fixed config admin panel field input padding 2017-11-01 19:20:58 -07:00
David Baldwynn
5120a2f287 fixed dropdowns for configure admin panel 2017-11-01 19:13:53 -07:00
David Baldwynn
eaea95078c fix url tab bug with configure tabs 2017-11-01 17:56:03 -07:00
David Baldwynn
522dd85f89 got respondent-notifications to work 2017-11-01 17:13:55 -07:00
David Baldwynn
33368f016c got basic email templating to work 2017-11-01 16:44:05 -07:00
David Baldwynn
9faa514ce9 added backend logic for email notifications and got it to pass server tests 2017-11-01 15:13:31 -07:00
David Baldwynn
a7c0cdc056 got tests for send-email-notification template logic to pass 2017-11-01 13:38:42 -07:00
David Baldwynn
cc62f6db3a Added test for parseTemplate for send-email-notifications 2017-11-01 12:53:52 -07:00
David Baldwynn
ddd4c2b22c Merge branch '2.20' into emailNotifications 2017-11-01 12:46:15 -07:00
David Baldwynn
bbb3137372 got all tests to pass 2017-11-01 12:17:35 -07:00
David Baldwynn
f291eda21e fixed add variable dropdown only showing email fields on subject input for config 2017-11-01 11:27:07 -07:00
David Baldwynn
983d2ce20b fixed bug with detecting whether form has emails for config panel 2017-11-01 11:25:24 -07:00
David Baldwynn
19880b0d07 add support when no email field is detected for self notifications 2017-11-01 11:15:58 -07:00
David Baldwynn
b5b8df90af got subject field to support variables 2017-11-01 11:06:08 -07:00
David Baldwynn
5a6b957495 added variable support in editor 2017-10-31 16:05:50 -07:00
David Baldwynn
aa8efe8508 got static url for configure tabs to work 2017-10-31 12:32:23 -07:00
David Baldwynn
12e1fd0947 got static url for admin tabs to work 2017-10-31 12:28:01 -07:00
David Baldwynn
798d28d92d added routable tabs for configure panel 2017-10-31 10:36:01 -07:00
David Baldwynn
86d8b88231 got url-linked tabs to work for admin panel 2017-10-31 10:21:10 -07:00
David Baldwynn
8f55df1393
Merge pull request #246 from tellform/increaseTestCoverage
Increased Test Coverage
2017-10-30 12:03:53 -07:00
David Baldwynn
22cf37424f added share and design form directives 2017-10-30 12:00:30 -07:00
David Baldwynn
5451475636 added translations for email notification panel 2017-10-30 11:41:34 -07:00
David Baldwynn
3652b41fc8 Merge branch 'master' into emailNotifications 2017-10-30 11:22:07 -07:00
David Baldwynn
56cf745270 added check to not load .env on travis run 2017-10-30 11:12:44 -07:00
David Baldwynn
c871d320f1 remove swig 2017-10-30 11:01:13 -07:00
David Baldwynn
814466cd1d switched to npm for travis 2017-10-30 10:47:54 -07:00
David Baldwynn
56fda0d312 switched travis scripts to yarn 2017-10-30 10:35:05 -07:00
David Baldwynn
5805c9b529 fixed travis test command 2017-10-30 09:54:03 -07:00
David Baldwynn
fc4d9a12f0 fixed lcovMerge for travis 2017-10-30 09:40:09 -07:00
David Baldwynn
26716e256d migrating to yarn 2017-10-29 21:54:30 -07:00
David Baldwynn
8d9e66548b remove references to i18n-lint 2017-10-29 21:37:22 -07:00
David Baldwynn
8ea1f63c96 fixed travis settings 2017-10-29 21:32:02 -07:00
David Baldwynn
b2b09171f3 fixed api tests 2017-10-29 21:22:43 -07:00
David Baldwynn
d0d273a718 removed secure configuration 2017-10-29 20:23:36 -07:00
David Baldwynn
774d256507 removed unused code from user.server.model.js 2017-10-29 20:14:40 -07:00
David Baldwynn
9a0a301c3d got rid of unnessecary user middleware 2017-10-29 20:11:58 -07:00
David Baldwynn
3e3e7ccfe6 removed hasRole and hasAdminRole from passport_helpers 2017-10-29 19:02:01 -07:00
David Baldwynn
e1db98ec95 added tests for api routes 2017-10-29 18:17:20 -07:00
David Baldwynn
99a6cb5009 fixed hasAuthorization bug for API 2017-10-29 17:46:08 -07:00
David Baldwynn
15eb66e455 added test for /users/me routes 2017-10-29 17:34:27 -07:00
David Baldwynn
aed7c6c43b added test for changing password route 2017-10-29 17:20:34 -07:00
David Baldwynn
9fffdf5328 added tests to cover reset password routes 2017-10-29 15:47:01 -07:00
David Baldwynn
3dbfe2f88d added helper function to remove sensitive data from forms/users 2017-10-29 14:15:13 -07:00
David Baldwynn
aa554c8210 added coveralls support 2017-10-29 13:02:41 -07:00
David Baldwynn
759a4efdd1 got all tests to pass 2017-10-29 12:44:12 -07:00
David Baldwynn
88fbf9e51f got user tests to pass 2017-10-29 12:20:49 -07:00
David Baldwynn
6fbfa35473 fixed on-finish-render directive tests 2017-10-29 12:17:54 -07:00
David Baldwynn
f427092144 fixed field directive tests 2017-10-29 12:16:34 -07:00
David Baldwynn
ebcc0c63cd fixed field-icon tests 2017-10-29 12:13:22 -07:00
David Baldwynn
7d65f0161b removed duplicate translation keyword for form admin panel 2017-10-29 12:07:42 -07:00
David Baldwynn
538030c551 got edit-form-directive tests to pass 2017-10-29 12:01:30 -07:00
David Baldwynn
76d6d54cd5 got edit-submissions-form directive tests to pass 2017-10-29 11:54:40 -07:00
David Baldwynn
8de93d4413 Merge branch 'master' into emailNotifications 2017-10-26 08:37:03 -07:00
David Baldwynn
00e1d43858 got basic emailNotifications to work 2017-10-05 21:53:13 -07:00
David Baldwynn
ae946bd619 Merge branch 'master' into emailNotifications 2017-10-05 21:27:57 -07:00
David Baldwynn
346537fea5 added emailNotifications to form model 2017-10-05 21:16:10 -07:00
161 changed files with 14188 additions and 4459 deletions

3
.gitignore vendored
View file

@ -1,3 +1,4 @@
selenium
dist
.vagrant
npm-debug.*
@ -21,7 +22,7 @@ Oscar_Credentials.*
npm-debug.log
node_modules/
public/lib/
public/dist
public/dist/
app/tests/coverage/
.bower-*/
.idea/

View file

@ -21,6 +21,7 @@
"globals": { // Globals variables.
"jasmine": true,
"angular": true,
"devel": false,
"_": true,
"saveAs": true,
"ApplicationConfiguration": true,

View file

@ -11,5 +11,9 @@ services:
addons:
code_climate:
repo_token: 6c3a1b81a09b2338d6f30913c1bcad115026689752cbb499a0a25061cda6fbcf
after_script:
- grunt coverage
install:
- npm install phantomjs
- npm install -g grunt
- npm install
script:
- yarn run travis

View file

@ -9,7 +9,14 @@ var mongoose = require('mongoose'),
FormSubmission = mongoose.model('FormSubmission'),
config = require('../../config/config'),
diff = require('deep-diff'),
_ = require('lodash');
_ = require('lodash'),
nodemailer = require('nodemailer'),
emailNotifications = require('../libs/send-email-notifications'),
constants = require('../libs/constants'),
helpers = require('./helpers.server.controller'),
async = require('async');
var smtpTransport = nodemailer.createTransport(config.mailer.options);
/**
* Delete a forms submissions
@ -27,18 +34,8 @@ exports.deleteSubmissions = function(req, res) {
});
return;
}
form.analytics.visitors = [];
form.save(function(formSaveErr){
if(formSaveErr){
res.status(400).send({
message: errorHandler.getErrorMessage(formSaveErr)
});
return;
}
res.status(200).send('Form submissions successfully deleted');
});
res.status(200).send('Form submissions successfully deleted');
});
};
@ -69,7 +66,54 @@ exports.createSubmission = function(req, res) {
message: errorHandler.getErrorMessage(err)
});
}
res.status(200).send('Form submission successfully saved');
var form = req.body;
var formFieldDict = emailNotifications.createFieldDict(form.form_fields);
async.waterfall([
function(callback) {
if (form.selfNotifications && form.selfNotifications.enabled) {
if(form.selfNotifications.fromField){
form.selfNotifications.fromEmails = formFieldDict[form.selfNotifications.fromField];
} else {
form.selfNotifications.fromEmails = config.mailer.options.from;
}
emailNotifications.send(form.selfNotifications, formFieldDict, smtpTransport, function(err){
if(err){
return callback({
message: 'Failure sending submission self-notification email'
});
}
callback();
});
} else {
callback();
}
},
function(callback) {
if (form.respondentNotifications && form.respondentNotifications.enabled && form.respondentNotifications.toField) {
form.respondentNotifications.toEmails = formFieldDict[form.respondentNotifications.toField];
emailNotifications.send(form.respondentNotifications, formFieldDict, smtpTransport, function(err){
if(err){
return callback({
message: 'Failure sending submission respondent-notification email'
});
}
callback();
});
} else {
callback();
}
}
], function (err) {
if(err){
return res.status(400).send(err);
}
res.status(200).send('Form submission successfully saved');
});
});
};
@ -82,7 +126,7 @@ exports.listSubmissions = function(req, res) {
FormSubmission.find({ form: _form._id }).sort('created').lean().exec(function(err, _submissions) {
if (err) {
console.error(err);
res.status(500).send({
return res.status(500).send({
message: errorHandler.getErrorMessage(err)
});
}
@ -90,29 +134,182 @@ exports.listSubmissions = function(req, res) {
});
};
/**
* Get Visitor Analytics Data for a given Form
*/
exports.getVisitorData = function(req, res) {
Form.aggregate([
{
$match: {
_id: mongoose.Types.ObjectId(req.params.formIdNoMiddleware),
admin: mongoose.Types.ObjectId(req.user.id)
}
},
{
$facet: {
'deviceStatistics': [
{
$unwind: '$analytics.visitors'
},
{
$project: {
_id: 0,
deviceType: '$analytics.visitors.deviceType',
SubmittedTimeElapsed: {
$cond: [
{
$eq: ['$analytics.visitors.isSubmitted', true]
},
'$analytics.visitors.timeElapsed',
0
]
},
SubmittedResponses: {
$cond: [
{
$eq: ['$analytics.visitors.isSubmitted', true]
},
1,
0
]
}
}
},
{
$group: {
_id: '$deviceType',
total_time: { $sum: '$SubmittedTimeElapsed' },
responses: { $sum: '$SubmittedResponses' },
visits: { $sum: 1 }
}
},
{
$project: {
total_time: '$total_time',
responses: '$responses',
visits: '$visits',
average_time: {
$cond: [
{ $eq: [ '$responses', 0 ] },
0,
{ $divide: ['$total_time', '$responses'] }
]
},
conversion_rate: {
$multiply: [
100,
{
$cond: [
{ $eq: [ '$visits', 0 ] },
0,
{ $divide: ['$responses', '$visits'] }
]
}
]
}
}
}
],
'globalStatistics': [
{
$unwind: '$analytics.visitors'
},
{
$project: {
_id: 0,
deviceType: '$analytics.visitors.deviceType',
SubmittedTimeElapsed: {
$cond: [
{
$eq: ['$analytics.visitors.isSubmitted', true]
},
'$analytics.visitors.timeElapsed',
0
]
},
SubmittedResponses: {
$cond: [
{
$eq: ['$analytics.visitors.isSubmitted', true]
},
1,
0
]
}
}
},
{
$group: {
_id: null,
total_time: { $sum: '$SubmittedTimeElapsed' },
responses: { $sum: '$SubmittedResponses' },
visits: { $sum: 1 }
}
},
{
$project: {
_id: 0,
total_time: '$total_time',
responses: '$responses',
visits: '$visits',
average_time: {
$cond: [
{ $eq: [ '$responses', 0 ] },
0,
{ $divide: ['$total_time', '$responses'] }
]
},
conversion_rate: {
$multiply: [
100,
{
$cond: [
{ $eq: [ '$visits', 0 ] },
0,
{ $divide: ['$responses', '$visits'] }
]
}
]
}
}
}
],
}
}
], function(err, results){
if (err) {
console.error(err);
return res.status(500).send({
message: errorHandler.getErrorMessage(err)
});
}
return res.json(results);
});
};
/**
* Create a new form
*/
exports.create = function(req, res) {
if(!req.body.form){
return res.status(400).send({
message: 'Invalid Input'
});
}
var form = new Form(req.body.form);
form.admin = req.user._id;
form.save(function(err) {
debugger;
form.save(function(err, createdForm) {
if (err) {
return res.status(500).send({
message: errorHandler.getErrorMessage(err)
});
}
return res.json(form);
createdForm = helpers.removeSensitiveModelData('private_form', createdForm.toJSON());
return res.json(createdForm);
});
};
@ -123,16 +320,14 @@ exports.read = function(req, res) {
if(!req.user || (req.form.admin.id !== req.user.id) ){
readForRender(req, res);
} else {
var newForm = req.form.toJSON();
if (req.userId) {
if(req.form.admin._id+'' === req.userId+''){
return res.json(newForm);
}
if(!req.form){
return res.status(404).send({
message: 'Form Does Not Exist'
});
}
newForm = helpers.removeSensitiveModelData('private_form', req.form.toJSON());
return res.json(newForm);
}
};
@ -148,9 +343,7 @@ var readForRender = exports.readForRender = function(req, res) {
});
}
delete newForm.lastModified;
delete newForm.__v;
delete newForm.created;
newForm = helpers.removeSensitiveModelData('public_form', newForm.toJSON());
if(newForm.startPage && !newForm.startPage.showStart){
delete newForm.startPage;
@ -166,15 +359,12 @@ exports.update = function(req, res) {
var form = req.form;
var updatedForm = req.body.form;
if(form.form_fields === undefined){
form.form_fields = [];
}
if(form.analytics === undefined){
if(!form.analytics && req.body.form.analytics){
form.analytics = {
visitors: [],
gaCode: ''
}
};
}
if (req.body.changes) {
@ -184,19 +374,23 @@ exports.update = function(req, res) {
diff.applyChange(form._doc, true, change);
});
} else {
if(!updatedForm){
res.status(400).send({
message: 'Updated Form is empty'
});
}
delete updatedForm.__v;
delete updatedForm.lastModified;
delete updatedForm.created;
delete updatedForm.id;
delete updatedForm._id;
delete updatedForm.__v;
//Unless we have 'admin' privileges, updating the form's admin is disabled
if(updatedForm && req.user.roles.indexOf('admin') === -1) {
delete updatedForm.admin;
}
if(form.analytics === null){
form.analytics.visitors = [];
form.analytics.gaCode = '';
}
//Do this so we can create duplicate fields
var checkForValidId = new RegExp('^[0-9a-fA-F]{24}$');
for(var i=0; i < req.body.form.form_fields.length; i++){
@ -214,6 +408,7 @@ exports.update = function(req, res) {
message: errorHandler.getErrorMessage(err)
});
} else {
savedForm = helpers.removeSensitiveModelData('private_form', savedForm.toJSON());
res.json(savedForm);
}
});
@ -245,24 +440,61 @@ exports.list = function(req, res) {
Form.find(searchObj)
.sort('-created')
.select('title language admin submissions isLive')
.populate('admin.username', 'admin._id')
.select('title language isLive')
.lean()
.exec(function(err, forms) {
if (err) {
res.status(400).send({
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
}
var form_ids = forms.map(function(form){
return form._id;
});
//Get number of submissions for each form
FormSubmission.aggregate([
{
$match: {
form: {
$in: form_ids
}
}
},
{
$group: {
_id: '$form',
responses: { $sum: 1 }
}
},
], function(err, results){
if (err) {
console.error(err);
return res.status(500).send({
message: errorHandler.getErrorMessage(err)
});
}
const result_ids = results.map(function(result){
return ''+result._id;
});
var currIndex = -1;
for(var i=0; i<forms.length; i++){
forms[i].numberOfResponses = 0;
if(forms[i].submissions){
forms[i].numberOfResponses = forms[i].submissions.length;
delete forms[i].submissions;
forms[i] = helpers.removeSensitiveModelData('private_form', forms[i]);
currIndex = result_ids.indexOf(forms[i]._id);
if(currIndex > -1){
forms[i].submissionNum = results[currIndex].responses;
} else {
forms[i].submissionNum = 0;
}
}
res.json(forms);
}
});
});
};
@ -275,7 +507,9 @@ exports.formByID = function(req, res, next, id) {
message: 'Form is invalid'
});
}
Form.findById(id)
.select('admin title language form_fields startPage endPage showFooter isLive design analytics.gaCode respondentNotifications selfNotifications')
.populate('admin')
.exec(function(err, form) {
if (err) {
@ -287,12 +521,7 @@ exports.formByID = function(req, res, next, id) {
}
else {
//Remove sensitive information from User object
var _form = form;
_form.admin.password = null;
_form.admin.salt = null;
_form.provider = null;
req.form = _form;
req.form = helpers.removeSensitiveModelData('private_form', form.toJSON());
return next();
}
});
@ -309,7 +538,7 @@ exports.formByIDFast = function(req, res, next, id) {
}
Form.findById(id)
.lean()
.select('title language form_fields startPage endPage hideFooter isLive design analytics.gaCode')
.select('title language form_fields startPage endPage showFooter isLive design analytics.gaCode selfNotifications respondentNotifications')
.exec(function(err, form) {
if (err) {
return next(err);
@ -320,13 +549,7 @@ exports.formByIDFast = function(req, res, next, id) {
}
else {
//Remove sensitive information from User object
var _form = form;
if(_form.admin){
_form.admin.password = null;
_form.admin.salt = null;
_form.provider = null;
}
req.form = _form;
req.form = helpers.removeSensitiveModelData('public_form', form);
return next();
}
});
@ -337,7 +560,8 @@ exports.formByIDFast = function(req, res, next, id) {
*/
exports.hasAuthorization = function(req, res, next) {
var form = req.form;
if (req.form.admin.id !== req.user.id && req.user.roles.indexOf('admin') === -1) {
debugger
if (req.form.admin.id !== req.user.id || req.user.roles.indexOf('admin') > -1) {
res.status(403).send({
message: 'User '+req.user.username+' is not authorized to edit Form: '+form.title
});

View file

@ -0,0 +1,29 @@
'use strict';
const constants = require('../libs/constants');
const _ = require('lodash');
module.exports = {
removeKeysFromDict: function(dict, keys){
for(var i=0; i<keys.length; i++){
var curr_key = keys[i];
if( dict.hasOwnProperty(curr_key) ){
delete dict[curr_key];
}
}
return dict;
},
removeSensitiveModelData: function(type, actual_object){
var object = _.cloneDeep(actual_object);
if(constants.privateFields.hasOwnProperty(type)) {
object = this.removeKeysFromDict(object, constants.privateFields[type]);
}
if(object.admin){
object.admin = this.removeKeysFromDict(object.admin, constants.privateFields.private_user);
}
debugger;
return object;
}
};

View file

@ -12,7 +12,8 @@ var errorHandler = require('../errors.server.controller'),
fs = require('fs'),
i18n = require('i18n'),
async = require('async'),
pug = require('pug');
pug = require('pug'),
helpers = require('../helpers.server.controller');
var nev = require('email-verification')(mongoose);
@ -60,7 +61,7 @@ config_nev();
exports.validateVerificationToken = function(req, res){
const fn = pug.compileFile(__dirname + "/../../views/welcome.email.view.pug");
const fn = pug.compileFile(__dirname + '/../../views/welcome.email.view.pug');
var renderedHtml = fn(res.locals);
var emailTemplate = {
@ -83,7 +84,7 @@ exports.validateVerificationToken = function(req, res){
};
exports.resendVerificationEmail = function(req, res, next){
const fn = pug.compileFile(__dirname + "/../../views/verification.email.view.pug");
const fn = pug.compileFile(__dirname + '/../../views/verification.email.view.pug');
var renderedHtml = fn(res.locals);
var emailTemplate = {
@ -117,7 +118,7 @@ exports.signup = function(req, res) {
var user = new User(req.body);
// Set language to visitor's language
user.language = req.cookies['userLang'];
user.language = req.cookies.userLang;
// Add missing user fields
user.provider = 'local';
@ -125,7 +126,6 @@ exports.signup = function(req, res) {
// Then save the temporary user
nev.createTempUser(user, function (err, existingPersistentUser, newTempUser) {
if (err) {
console.log(err);
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
@ -133,7 +133,7 @@ exports.signup = function(req, res) {
// new user created
if (newTempUser) {
const fn = pug.compileFile(__dirname + "/../../views/verification.email.view.pug");
const fn = pug.compileFile(__dirname + '/../../views/verification.email.view.pug');
var renderedHtml = fn(res.locals);
var URL = newTempUser[nev.options.URLFieldName];
@ -179,6 +179,8 @@ exports.signin = function(req, res, next) {
}
res.cookie('langCookie', user.language, { maxAge: 90000, httpOnly: true });
user = helpers.removeSensitiveModelData('private_user', user.toJSON());
return res.json(user);
});
}
@ -198,16 +200,12 @@ exports.signout = function(req, res) {
/* Generate API Key for User */
exports.generateAPIKey = function(req, res) {
if (!req.isAuthenticated()){
return res.status(400).send({
message: 'User is not Authorized'
});
}
User.findById(req.user.id)
.exec( function(err, user) {
if (err) {
return res.status(400).send(err);
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
}
if (!user) {
@ -226,12 +224,8 @@ exports.generateAPIKey = function(req, res) {
}
var newUser = _user.toObject();
delete newUser.salt;
delete newUser.__v;
delete newUser.passwordHash;
delete newUser.provider;
return res.json(newUser);
return res.json({ id: newUser._id, apiKey: newUser.apiKey });
});
});

View file

@ -3,36 +3,7 @@
/**
* Module dependencies.
*/
var _ = require('lodash'),
mongoose = require('mongoose'),
User = mongoose.model('User');
/**
* User middleware
*/
exports.userByID = function (req, res, next, id) {
if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).send({
message: 'User is invalid'
});
}
User.findOne({
_id: id
}).exec(function (err, user) {
if (err) {
return next(err);
} else if (!user) {
return res.status(404).send({
message: 'User does not exist'
});
}
req.profile = user;
next();
});
};
var auth = require('../../../config/passport_helpers');
/**
* Require login routing middleware
*/
@ -45,22 +16,3 @@ exports.requiresLogin = function(req, res, next) {
return next();
}
};
/**
* User authorizations routing middleware
*/
exports.hasAuthorization = function(roles) {
var _this = this;
return function(req, res, next) {
_this.requiresLogin(req, res, function() {
if (_.intersection(req.user.roles, roles).length) {
return next();
} else {
return res.status(403).send({
message: 'User is not authorized'
});
}
});
};
};

View file

@ -81,10 +81,9 @@ exports.forgot = function(req, res) {
}
},
function(token, user, done) {
const fn = pug.compileFile(__dirname + "/../../views/templates/reset-password-email.server.view.pug");
res.locals['url'] = 'http://' + req.headers.host + '/auth/reset/' + token;
const fn = pug.compileFile(__dirname + '/../../views/templates/reset-password-email.server.view.pug');
res.locals.url = 'http://' + req.headers.host + '/auth/reset/' + token;
console.log(res.locals);
var renderedHtml = fn(res.locals);
done(null, renderedHtml, user);
},
@ -98,10 +97,10 @@ exports.forgot = function(req, res) {
};
var userEmail = user.email;
var user = userEmail.split('@')[0];
var emailUsername = userEmail.split('@')[0];
var domain = userEmail.split('@')[1];
var obfuscatedUser = user.substring(0, 1) + user.substring(1).replace(/./g, '*');
var obfuscatedUser = emailUsername.substring(0, 1) + emailUsername.substring(1).replace(/./g, '*');
var domainName = domain.split('.')[0];
var tld = domain.split('.')[1];
@ -114,7 +113,6 @@ exports.forgot = function(req, res) {
}
], function(err, obfuscatedEmail) {
if (err) {
console.log(err);
return res.status(400).send({
message: 'Couldn\'t send reset password email due to internal server errors. Please contact support at team@tellform.com.'
});
@ -142,9 +140,9 @@ exports.validateResetToken = function(req, res) {
});
}
if (!user) {
return res.redirect('/#!/password/reset/invalid');
return res.redirect(400, '/#!/password/reset/invalid');
}
res.redirect('/#!/password/reset/' + req.params.token);
});
};
@ -187,13 +185,13 @@ exports.reset = function(req, res, next) {
done(null, savedUser);
});
} else {
done('Password reset token is invalid or has expired.', null);
done('invalid_reset_token', null);
}
});
},
function(user, done) {
const fn = pug.compileFile(__dirname + "/../../views/templates/reset-password-confirm-email.server.view.pug");
var renderedHtml = fn(res.locals);
const fn = pug.compileFile(__dirname + '/../../views/templates/reset-password-confirm-email.server.view.pug');
const renderedHtml = fn(res.locals);
done(null, renderedHtml, user);
},
// If valid email, send reset email using service
@ -211,12 +209,18 @@ exports.reset = function(req, res, next) {
}
], function(err) {
if (err) {
res.status(500).send({
if(err === 'invalid_reset_token'){
return res.status(400).send({
message: 'Password reset token is invalid or has expired.'
});
}
return res.status(500).send({
message: err.message || err
});
}
return res.json({
res.json({
message: 'Successfully changed your password!'
});
});

View file

@ -5,7 +5,8 @@
*/
var _ = require('lodash'),
errorHandler = require('../errors.server.controller.js'),
mongoose = require('mongoose');
mongoose = require('mongoose'),
helpers = require('../helpers.server.controller');
/**
* Update user details
@ -14,47 +15,38 @@ exports.update = function(req, res) {
// Init Variables
var user = req.user;
// For security measurement we remove the roles from the req.body object
// To improve security we remove the roles from the req.body object
delete req.body.roles;
if (user) {
// Merge existing user
user = _.extend(user, req.body);
user.updated = Date.now();
debugger;
user.save(function(err) {
if (err) {
return res.status(500).send({
message: errorHandler.getErrorMessage(err)
});
}
req.login(user, function(loginErr) {
if (err) {
res.status(500).send(loginErr);
} else {
res.json(user);
}
// Merge existing user
user = _.extend(user, req.body);
user.updated = Date.now();
user.save(function(err) {
if (err) {
return res.status(500).send({
message: errorHandler.getErrorMessage(err)
});
}
req.login(user, function(loginErr) {
if (err) {
res.status(500).send(loginErr);
} else {
user = helpers.removeSensitiveModelData('private_user', user.toJSON());
res.json(user);
}
});
} else {
res.status(401).send({
message: 'User is not signed in'
});
}
});
};
/**
* Send User
*/
exports.getUser = function(req, res) {
var _user = req.user;
delete _user.password;
delete _user.salt;
delete _user.provider;
delete _user.__v;
var user = helpers.removeSensitiveModelData('private_user', req.user.toJSON());
res.json(req.user || null);
res.end();
return res.json(user);
};

View file

@ -1,41 +1,69 @@
'use strict';
module.exports = {
fieldTypes: ['textfield',
'date',
'email',
'link',
'legal',
'url',
'textarea',
'statement',
'welcome',
'thankyou',
'file',
'dropdown',
'scale',
'rating',
'radio',
'checkbox',
'hidden',
'yes_no',
'natural',
'stripe',
'number'],
var constants = module.exports = {
ratingShapeTypes: ['Heart',
'Star',
'thumbs-up',
'thumbs-down',
'Circle',
'Square',
'Check Circle',
'Smile Outlined',
'Hourglass',
'bell',
'Paper Plane',
'Comment',
'Trash'],
extraneousFormFieldProps: [
'validFieldTypes',
'disabled',
'required',
'isSubmission',
'title',
'fieldOptions',
'ratingOptions',
'logicJump',
'description',
'created',
'lastModified',
'deletePreserved'
],
fieldTypes: [
'textfield',
'date',
'email',
'legal',
'textarea',
'link',
'statement',
'dropdown',
'rating',
'radio',
'hidden',
'yes_no',
'number'
],
ratingShapeTypes: [
'Heart',
'Star',
'thumbs-up',
'thumbs-down',
'Circle',
'Square',
'Check Circle',
'Smile Outlined',
'Hourglass',
'bell',
'Paper Plane',
'Comment',
'Trash'
],
ratingShapeTypes: [
'Heart',
'Star',
'thumbs-up',
'thumbs-down',
'Circle',
'Square',
'Check Circle',
'Smile Outlined',
'Hourglass',
'bell',
'Paper Plane',
'Comment',
'Trash'
],
deviceTypes: ['desktop', 'phone', 'tablet', 'other'],
languageTypes: ['en', 'fr', 'es', 'it', 'de'],
@ -56,6 +84,13 @@ module.exports = {
'Deutsch': 'de'
},
privateFields: {
'public_form': ['__v', 'analytics.visitors', 'analytics.views', 'analytics.conversionRate', 'analytics.fields', 'lastModified', 'created'],
'private_form': ['__v'],
'public_user': ['passwordHash', 'password', 'provider', 'salt', 'lastModified', 'created', 'resetPasswordToken', 'resetPasswordExpires', 'token', 'apiKey', '__v'],
'private_user': ['passwordHash', 'password', 'provider', 'salt', 'resetPasswordToken', 'resetPasswordExpires', 'token', '__v']
},
expressionStringTypes: ['field == static',
'field != static',
'field > static',
@ -72,8 +107,9 @@ module.exports = {
userRoleTypes: ['user', 'admin', 'superuser'],
regex: {
username: /^[a-zA-Z0-9\-]+$/,
url: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/,
hexCode: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
email: /^(([^<>()\[\]\\.,;:\s@']+(\.[^<>()\[\]\\.,;:\s@']+)*)|('.+'))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
email: /^(([^<>()\[\]\\.,;:\s@']+(\.[^<>()\[\]\\.,;:\s@']+)*)|('.+'))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
}
};

View file

@ -0,0 +1,48 @@
'use strict';
const jsdom = require('jsdom');
var JSDOM = jsdom.JSDOM;
module.exports = {
send: function(emailSettings, emailTemplateVars, smtpTransport, cb){
var parsedTemplate = this.parseTemplate(emailSettings.htmlTemplate, emailTemplateVars, false);
var parsedSubject = this.parseTemplate(emailSettings.subject, emailTemplateVars, true);
var mailOptions = {
replyTo: emailSettings.fromEmails,
from: 'noreply@tellform.com',
cc: emailSettings.toEmails,
subject: parsedSubject,
html: parsedTemplate
};
smtpTransport.sendMail(mailOptions, function(err){
cb(err);
});
},
parseTemplate: function(emailTemplate, emailTemplateVars, onlyText){
var dom = new JSDOM('<!doctype html>'+emailTemplate);
Object.keys(emailTemplateVars).forEach(function (key) {
var elem = dom.window.document.querySelector('span.placeholder-tag[data-id=\'' + key + '\']');
if(elem !== null){
elem.outerHTML = emailTemplateVars[key];
}
});
if(onlyText){
return dom.window.document.documentElement.textContent;
}
return dom.serialize();
},
createFieldDict: function(form_fields){
var formFieldDict = {};
form_fields.forEach(function(field){
if(field.hasOwnProperty('fieldValue') && field.hasOwnProperty('_id')){
formFieldDict[field._id] = String(field.fieldValue);
}
});
return formFieldDict;
}
};

View file

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

View file

@ -8,19 +8,11 @@ var mongoose = require('mongoose'),
_ = require('lodash'),
timeStampPlugin = require('../libs/timestamp.server.plugin'),
async = require('async'),
Random = require('random-js'),
mt = Random.engines.mt19937();
mt.autoSeed();
constants = require('../libs/constants');
//Mongoose Models
var FieldSchema = require('./form_field.server.model.js');
var FormSubmissionSchema = require('./form_submission.server.model.js'),
FormSubmission = mongoose.model('FormSubmission', FormSubmissionSchema);
var ButtonSchema = new Schema({
url: {
type: String,
@ -47,8 +39,8 @@ var VisitorDataSchema = new Schema({
referrer: {
type: String
},
lastActiveField: {
type: Schema.Types.ObjectId
filledOutFields: {
type: [Schema.Types.ObjectId]
},
timeElapsed: {
type: Number
@ -57,11 +49,12 @@ var VisitorDataSchema = new Schema({
type: Boolean
},
language: {
type: String
type: String,
enum: constants.languageTypes,
default: 'en',
},
ipAddr: {
type: String,
default: ''
type: String
},
deviceType: {
type: String,
@ -100,13 +93,10 @@ var FormSchema = new Schema({
},
visitors: [VisitorDataSchema]
},
form_fields: [FieldSchema],
submissions: [{
type: Schema.Types.ObjectId,
ref: 'FormSubmission'
}],
form_fields: {
type: [FieldSchema],
default: []
},
admin: {
type: Schema.Types.ObjectId,
ref: 'User',
@ -149,17 +139,59 @@ var FormSchema = new Schema({
buttons:[ButtonSchema]
},
hideFooter: {
type: Boolean,
default: false
selfNotifications: {
fromField: {
type: String
},
toEmails: {
type: String
},
subject: {
type: String
},
htmlTemplate: {
type: String
},
enabled: {
type: Boolean,
default: false
}
},
respondentNotifications: {
toField: {
type: String
},
fromEmails: {
type: String,
match: [/.+\@.+\..+/, 'Please fill a valid email address']
},
subject: {
type: String,
default: 'Tellform: Thank you for filling out this TellForm'
},
htmlTemplate: {
type: String,
default: 'Hello, <br><br> Weve received your submission. <br><br> Thank you & have a nice day!',
},
enabled: {
type: Boolean,
default: false
}
},
showFooter: {
type: Boolean,
default: true
},
isLive: {
type: Boolean,
default: true
},
design: {
colors:{
colors: {
backgroundColor: {
type: String,
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
@ -190,98 +222,6 @@ var FormSchema = new Schema({
}
}, formSchemaOptions);
/*
** In-Form Analytics Virtual Attributes
*/
FormSchema.virtual('analytics.views').get(function () {
if(this.analytics && this.analytics.visitors && this.analytics.visitors.length > 0){
return this.analytics.visitors.length;
} else {
return 0;
}
});
FormSchema.virtual('analytics.submissions').get(function () {
return this.submissions.length;
});
FormSchema.virtual('analytics.conversionRate').get(function () {
if(this.analytics && this.analytics.visitors && this.analytics.visitors.length > 0){
return this.submissions.length/this.analytics.visitors.length*100;
} else {
return 0;
}
});
FormSchema.virtual('analytics.fields').get(function () {
var fieldDropoffs = [];
var visitors = this.analytics.visitors;
var that = this;
if(!this.form_fields || this.form_fields.length === 0) {
return null;
}
for(var i=0; i<this.form_fields.length; i++){
var field = this.form_fields[i];
if(field && !field.deletePreserved){
var dropoffViews = _.reduce(visitors, function(sum, visitorObj){
if(visitorObj.lastActiveField+'' === field._id+'' && !visitorObj.isSubmitted){
return sum + 1;
}
return sum;
}, 0);
var continueViews, nextIndex;
if(i !== this.form_fields.length-1){
continueViews = _.reduce(visitors, function(sum, visitorObj){
nextIndex = that.form_fields.indexOf(_.find(that.form_fields, function(o) {
return o._id+'' === visitorObj.lastActiveField+'';
}));
if(nextIndex > i){
return sum + 1;
}
return sum;
}, 0);
} else {
continueViews = _.reduce(visitors, function(sum, visitorObj){
if(visitorObj.lastActiveField+'' === field._id+'' && visitorObj.isSubmitted){
return sum + 1;
}
return sum;
}, 0);
}
var totalViews = dropoffViews+continueViews;
var continueRate = 0;
var dropoffRate = 0;
if(totalViews > 0){
continueRate = (continueViews/totalViews*100).toFixed(0);
dropoffRate = (dropoffViews/totalViews*100).toFixed(0);
}
fieldDropoffs[i] = {
dropoffViews: dropoffViews,
responses: continueViews,
totalViews: totalViews,
continueRate: continueRate,
dropoffRate: dropoffRate,
field: field
};
}
}
return fieldDropoffs;
});
FormSchema.plugin(timeStampPlugin, {
createdPath: 'created',
modifiedPath: 'lastModified',
@ -289,159 +229,16 @@ FormSchema.plugin(timeStampPlugin, {
});
FormSchema.pre('save', function (next) {
switch(this.language){
case 'spanish':
this.language = 'es';
break;
case 'french':
this.language = 'fr';
break;
case 'italian':
this.language = 'it';
break;
case 'german':
this.language = 'de';
break;
default:
break;
if(this.form_fields && this.form_fields.length){
this.form_fields = this.form_fields.filter(function(field){
return !field.deletePreserved;
});
}
next();
});
function getDeletedIndexes(needle, haystack){
var deletedIndexes = [];
if(haystack.length > 0){
for(var i = 0; i < needle.length; i++){
if(haystack.indexOf(needle[i]) === -1){
deletedIndexes.push(i);
}
}
}
return deletedIndexes;
}
function formFieldsAllHaveIds(form_fields){
for(var i=0; i<form_fields.length; i++){
if(!form_fields[i].hasOwnProperty('_id') && !form_fields[i].hasOwnProperty('globalId')){
return false;
}
}
return true;
}
FormSchema.pre('save', function (next) {
var that = this;
var _original;
async.series([
function(cb) {
that.constructor
.findOne({_id: that._id}).exec(function (err, original) {
if (err) {
return cb(err);
} else if (!original){
return next();
} else {
_original = original;
return cb(null);
}
});
},
function(cb) {
if(that.form_fields && that.isModified('form_fields') && formFieldsAllHaveIds(that.toObject().form_fields)){
var current_form = that.toObject(),
old_form_fields = _original.toObject().form_fields,
new_ids = _.map(_.map(current_form.form_fields, 'globalId'), function(id){ return ''+id;}),
old_ids = _.map(_.map(old_form_fields, 'globalId'), function(id){ return ''+id;}),
deletedIds = getDeletedIndexes(old_ids, new_ids);
//Check if any form_fileds were deleted
if( deletedIds.length > 0 ){
var modifiedSubmissions = [];
async.forEachOfSeries(deletedIds,
function (deletedIdIndex, key, cb_id) {
var deleted_id = old_ids[deletedIdIndex];
//Find FormSubmissions that contain field with _id equal to 'deleted_id'
FormSubmission.
find({ form: that, form_fields: {$elemMatch: {globalId: deleted_id} } }).
exec(function(err, submissions){
if(err) {
return cb_id(err);
}
//Preserve fields that have at least one submission
if (submissions.length) {
//Add submissions
modifiedSubmissions.push.apply(modifiedSubmissions, submissions);
}
return cb_id(null);
});
},
function (err) {
if(err){
console.error(err.message);
return cb(err);
}
//Iterate through all submissions with modified form_fields
async.forEachOfSeries(modifiedSubmissions, function (submission, key, callback) {
var submission_form_fields = submission.toObject().form_fields;
var currentform_form_fields = that.toObject().form_fields;
//Iterate through ids of deleted fields
for (var i = 0; i < deletedIds.length; i++) {
var index = _.findIndex(submission_form_fields, function (field) {
var tmp_id = field.globalId + '';
return tmp_id === old_ids[deletedIds[i]];
});
var deletedField = submission_form_fields[index];
//Hide field if it exists
if (deletedField) {
//Delete old form_field
submission_form_fields.splice(index, 1);
deletedField.deletePreserved = true;
//Move deleted form_field to start
submission_form_fields.unshift(deletedField);
currentform_form_fields.unshift(deletedField);
}
}
submission.form_fields = submission_form_fields;
that.form_fields = currentform_form_fields;
return callback(null);
}, function (err) {
return cb(err);
});
});
} else {
return cb(null);
}
} else {
return cb(null);
}
}
],
function(err){
if(err){
return next(err);
}
next();
});
});
FormSchema.index({created: 1});
mongoose.model('Form', FormSchema);
module.exports = mongoose.model('Form');

View file

@ -9,7 +9,8 @@ var mongoose = require('mongoose'),
_ = require('lodash'),
Schema = mongoose.Schema,
LogicJumpSchema = require('./logic_jump.server.model'),
tokgen = require('../libs/tokenGenerator');
tokgen = require('../libs/tokenGenerator'),
constants = require('../libs/constants');
var FieldOptionSchema = new Schema({
option_id: {
@ -34,21 +35,7 @@ var RatingFieldSchema = new Schema({
},
shape: {
type: String,
enum: [
'Heart',
'Star',
'thumbs-up',
'thumbs-down',
'Circle',
'Square',
'Check Circle',
'Smile Outlined',
'Hourglass',
'bell',
'Paper Plane',
'Comment',
'Trash'
]
enum: constants.ratingShapeTypes
},
validShapes: {
type: [String]
@ -62,9 +49,6 @@ function BaseFieldSchema(){
Schema.apply(this, arguments);
this.add({
globalId: {
type: String,
},
isSubmission: {
type: Boolean,
default: false
@ -85,6 +69,7 @@ function BaseFieldSchema(){
ratingOptions: RatingFieldSchema,
fieldOptions: [FieldOptionSchema],
required: {
type: Boolean,
default: true
@ -103,31 +88,12 @@ function BaseFieldSchema(){
},
fieldType: {
type: String,
enum: [
'textfield',
'date',
'email',
'link',
'legal',
'url',
'textarea',
'statement',
'welcome',
'thankyou',
'file',
'dropdown',
'scale',
'rating',
'radio',
'checkbox',
'hidden',
'yes_no',
'natural',
'stripe',
'number'
]
enum: constants.fieldTypes
},
fieldValue: Schema.Types.Mixed
fieldValue: {
type: Schema.Types.Mixed,
default: ''
}
});
this.plugin(timeStampPlugin, {
@ -140,7 +106,7 @@ function BaseFieldSchema(){
this.validFieldTypes = mongoose.model('Field').schema.path('fieldType').enumValues;
if(this.fieldType === 'rating' && this.ratingOptions.validShapes.length === 0){
this.ratingOptions.validShapes = mongoose.model('RatingOptions').schema.path('shape').enumValues;
this.ratingOptions.validShapes = constants.ratingShapeTypes;
}
next();
@ -162,19 +128,14 @@ FormFieldSchema.pre('validate', function(next) {
return(next(error));
}
}else{
} else {
//Setting default values for ratingOptions
if(!this.ratingOptions.steps){
if(!this.ratingOptions.steps) {
this.ratingOptions.steps = 10;
}
if(!this.ratingOptions.shape){
this.ratingOptions.shape = 'Star';
}
//Checking that the fieldValue is between 0 and ratingOptions.steps
if(this.fieldValue+0 > this.ratingOptions.steps || this.fieldValue+0 < 0){
this.fieldValue = 1;
}
}
@ -183,27 +144,18 @@ FormFieldSchema.pre('validate', function(next) {
if(this.fieldOptions && this.fieldOptions.length > 0){
error.errors.ratingOptions = new mongoose.Error.ValidatorError({path:'fieldOptions', message: 'fieldOptions are only allowed for type dropdown, checkbox or radio fields.', type: 'notvalid', value: this.ratingOptions});
console.error(error);
return(next(error));
return next(error);
}
}
return next();
});
//LogicJump Save
FormFieldSchema.pre('save', function(next) {
if(!this.globalId){
this.globalId = tokgen();
}
next();
});
//Submission fieldValue correction
FormFieldSchema.pre('save', function(next) {
if(this.fieldType === 'dropdown' && this.isSubmission){
this.fieldValue = this.fieldValue.option_value;
}
return next();
});

View file

@ -6,7 +6,9 @@
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
timeStampPlugin = require('../libs/timestamp.server.plugin'),
FieldSchema = require('./form_field.server.model.js');
FieldSchema = require('./form_field.server.model'),
helpers = require('../controllers/helpers.server.controller'),
constants = require('../libs/constants');
/**
* Form Submission Schema
@ -55,18 +57,7 @@ FormSubmissionSchema.pre('save', function (next) {
this.form_fields[i].fieldValue = this.form_fields[i].fieldValue.option_value;
}
delete form_fields[i].validFieldTypes;
delete form_fields[i].disabled;
delete form_fields[i].required;
delete form_fields[i].isSubmission;
delete form_fields[i].title;
delete form_fields[i].fieldOptions;
delete form_fields[i].ratingOptions;
delete form_fields[i].logicJump;
delete form_fields[i].description;
delete form_fields[i].created;
delete form_fields[i].lastModified;
delete form_fields[i].deletePreserved;
helpers.removeKeysFromDict(form_fields[i], constants.extraneousFormFieldProps);
}
next();
});
@ -77,19 +68,7 @@ FormSubmissionSchema.path('form_fields', {
form_fields[i].isSubmission = true;
form_fields[i]._id = new mongoose.mongo.ObjectID();
delete form_fields[i].validFieldTypes;
delete form_fields[i].disabled;
delete form_fields[i].required;
delete form_fields[i].isSubmission;
delete form_fields[i].title;
delete form_fields[i].fieldOptions;
delete form_fields[i].ratingOptions;
delete form_fields[i].logicJump;
delete form_fields[i].description;
delete form_fields[i].created;
delete form_fields[i].lastModified;
delete form_fields[i].deletePreserved;
helpers.removeKeysFromDict(form_fields[i], constants.extraneousFormFieldProps);
}
return form_fields;
}
@ -101,4 +80,6 @@ FormSubmissionSchema.plugin(timeStampPlugin, {
useVirtual: false
});
module.exports = FormSubmissionSchema;
mongoose.model('FormSubmission', FormSubmissionSchema);
module.exports = mongoose.model('FormSubmission');

View file

@ -1,26 +0,0 @@
'use strict';
const constants = require('../../libs/constants'),
config = require('../../../config/config');
module.exports = exports = function lastModifiedPlugin (schema, options) {
schema.add({
language: {
type: String,
enum: constants.languageTypes,
default: config.defaultLanguage,
required: options.required || 'Must be a valid language'
}
});
schema.pre('save', function (next) {
var currWord = this.language;
//English is the default backup language
this.language = 'en';
if(constants.wordToLangCode.has(currWord)){
this.language = constants.wordToLangCode[currWord];
}
next();
});
};

View file

@ -9,29 +9,8 @@ var mongoose = require('mongoose'),
config = require('../../config/config'),
timeStampPlugin = require('../libs/timestamp.server.plugin'),
path = require('path'),
querystring = require('querystring');
/**
* A Validation function for local strategy properties
*/
var validateLocalStrategyProperty = function(property) {
var propHasLength;
if (property) {
propHasLength = !!property.length;
} else {
propHasLength = false;
}
return ((this.provider !== 'local' && !this.updated) || propHasLength);
};
/**
* A Validation function for username
*/
var validateUsername = function(username) {
return (username.match(/^[a-zA-Z0-9.-_]+$/) !== null);
};
querystring = require('querystring'),
constants = require('../libs/constants');
/**
* User Schema
@ -52,14 +31,14 @@ var UserSchema = new Schema({
trim: true,
lowercase: true,
unique: 'Account already exists with this email',
match: [/.+\@.+\..+/, 'Please fill a valid email address'],
match: [constants.regex.email, 'Please fill a valid email address'],
required: [true, 'Email is required']
},
username: {
type: String,
unique: true,
lowercase: true,
match: [/^[a-zA-Z0-9\-]+$/, 'Username can only contain alphanumeric characters and \'-\''],
match: [constants.regex.username, 'Username can only contain alphanumeric characters and \'-\''],
required: [true, 'Username is required']
},
passwordHash: {
@ -73,18 +52,16 @@ var UserSchema = new Schema({
type: String,
default: 'local'
},
providerData: {},
additionalProvidersData: {},
roles: {
type: [{
type: String,
enum: ['user', 'admin', 'superuser']
enum: constants.userRoleTypes
}],
default: ['user']
},
language: {
type: String,
enum: ['en', 'fr', 'es', 'it', 'de'],
enum: constants.languageTypes,
default: 'en',
},
lastModified: {
@ -111,10 +88,6 @@ var UserSchema = new Schema({
}
});
UserSchema.virtual('displayName').get(function () {
return this.firstName + ' ' + this.lastName;
});
UserSchema.plugin(timeStampPlugin, {
createdPath: 'created',
modifiedPath: 'lastModified',
@ -135,7 +108,7 @@ UserSchema.virtual('password').get(function () {
/**
* Create instance method for hashing a password
*/
UserSchema.methods.hashPassword = function(password) {
UserSchema.statics.hashPassword = UserSchema.methods.hashPassword = function(password) {
var encoding = 'base64';
var iterations = 10000;
var keylen = 128;
@ -192,4 +165,6 @@ UserSchema.methods.isAdmin = function() {
return false;
};
module.exports = mongoose.model('User', UserSchema);
mongoose.model('User', UserSchema);
module.exports = mongoose.model('User');

View file

@ -31,7 +31,7 @@ module.exports = function(app) {
}
app.route('/forms/:formIdFast([a-zA-Z0-9]+)')
.post(forms.createSubmission)
.post(forms.createSubmission);
app.route('/forms')
.get(auth.isAuthenticatedOrApiKey, forms.list)
@ -47,6 +47,8 @@ module.exports = function(app) {
.get(auth.isAuthenticatedOrApiKey, forms.hasAuthorization, forms.listSubmissions)
.delete(auth.isAuthenticatedOrApiKey, forms.hasAuthorization, forms.deleteSubmissions);
app.route('/forms/:formIdNoMiddleware([a-zA-Z0-9]+)/visitors')
.get(auth.isAuthenticatedOrApiKey, forms.getVisitorData);
// Slower formId middleware
app.param('formId', forms.formByID);

View file

@ -12,6 +12,7 @@ module.exports = function(app) {
var users = require('../../app/controllers/users.server.controller');
// Setting up the users profile api
app.route('/users/password').post(users.requiresLogin, users.changePassword);
app.route('/users/me').get(auth.isAuthenticatedOrApiKey, users.getUser);
app.route('/users').put(auth.isAuthenticatedOrApiKey, users.update);
@ -19,8 +20,7 @@ module.exports = function(app) {
app.route('/auth/verify/:token').get(users.validateVerificationToken);
app.route('/auth/verify').post(users.resendVerificationEmail);
// Setting up the users password api
app.route('/users/password').post(users.requiresLogin, users.changePassword);
// Setting up the password reset api
app.route('/auth/forgot').post(users.forgot);
app.route('/auth/reset/:token').get(users.validateResetToken);
app.route('/auth/reset/:token').post(users.reset);
@ -33,7 +33,4 @@ module.exports = function(app) {
app.route('/auth/signout').get(users.signout);
app.route('/auth/genkey').get(users.requiresLogin, users.generateAPIKey);
// Finish by binding the user middleware
app.param('userId', users.userByID);
};

View file

@ -22,7 +22,6 @@ module.exports = function (io, socket) {
var newVisitor = {
socketId: data.socketId,
referrer: data.referrer,
lastActiveField: data.lastActiveField,
timeElapsed: data.timeElapsed,
isSubmitted: data.isSubmitted,
language: data.language,

View file

@ -7,8 +7,8 @@ require('../../server.js');
*/
var should = require('should'),
mongoose = require('mongoose'),
User = mongoose.model('User'),
Form = mongoose.model('Form');
User = require('../models/user.server.model.js'),
Form = require('../models/form.server.model.js');
/**
* Globals
@ -40,8 +40,8 @@ describe('Form Model Unit Tests:', function() {
language: 'en',
form_fields: [
{'fieldType':'textfield', title:'First Name', 'fieldValue': ''},
{'fieldType':'checkbox', title:'nascar', 'fieldValue': ''},
{'fieldType':'checkbox', title:'hockey', 'fieldValue': ''}
{'fieldType':'legal', title:'nascar', 'fieldValue': ''},
{'fieldType':'legal', title:'hockey', 'fieldValue': ''}
]
});
done();

View file

@ -6,11 +6,26 @@ var should = require('should'),
request = require('supertest'),
Session = require('supertest-session'),
mongoose = require('mongoose'),
User = mongoose.model('User'),
Form = mongoose.model('Form'),
User = require('../models/user.server.model.js'),
Form = require('../models/form.server.model.js'),
FormSubmission = require('../models/form_submission.server.model.js'),
Field = mongoose.model('Field'),
FormSubmission = mongoose.model('FormSubmission'),
async = require('async');
async = require('async'),
_ = require('lodash');
function omitDeep(collection, excludeKeys) {
function omitFn(value) {
if (value && typeof value === 'object') {
excludeKeys.forEach((key) => {
delete value[key];
});
}
}
return _.cloneDeepWith(collection, omitFn);
}
/**
* Globals
@ -24,6 +39,18 @@ var credentials = {
password: 'password'
};
var sampleVisitorData = [{
socketId: 'ntneooe8989eotnoeeo',
referrer: 'http://google.com',
timeElapsed: 89898989,
isSubmitted: true,
language: 'en',
ipAddr: '192.168.1.1',
deviceType: 'desktop',
userAgent: 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36',
filledOutFields: []
}];
/**
* Form routes tests
*/
@ -50,8 +77,8 @@ describe('Form Routes Unit tests', function() {
admin: user.id,
form_fields: [
new Field({'fieldType':'textfield', 'title':'First Name', 'fieldValue': ''}),
new Field({'fieldType':'checkbox', 'title':'nascar', 'fieldValue': ''}),
new Field({'fieldType':'checkbox', 'title':'hockey', 'fieldValue': ''})
new Field({'fieldType':'legal', 'title':'nascar', 'fieldValue': ''}),
new Field({'fieldType':'legal', 'title':'hockey', 'fieldValue': ''})
],
isLive: true
};
@ -69,7 +96,6 @@ describe('Form Routes Unit tests', function() {
.send({form: myForm})
.expect(401)
.end(function(FormSaveErr, FormSaveRes) {
console.log(FormSaveRes.text);
// Call the assertion callback
done(FormSaveErr);
});
@ -92,7 +118,7 @@ describe('Form Routes Unit tests', function() {
FormObj.save(function(err, form) {
if(err) return done(err);
userSession.get('/subdomain/' + credentials.username + '/forms/' + form._id + '/render')
userSession.get('/forms/' + form._id + '/render')
.expect(200)
.end(function(err, res) {
if(err) return done(err)
@ -115,7 +141,7 @@ describe('Form Routes Unit tests', function() {
FormObj.save(function(err, form) {
if(err) return done(err);
userSession.get('/subdomain/' + credentials.username + '/forms/' + form._id + '/render')
userSession.get('/forms/' + form._id + '/render')
.expect(401, {message: 'Form is Not Public'})
.end(function(err, res) {
done(err);
@ -167,7 +193,7 @@ describe('Form Routes Unit tests', function() {
it(' > should not be able to create a Form if body is empty', function(done) {
loginSession.post('/forms')
.send({form: null})
.expect(400, {"message":"Invalid Input"})
.expect(400, {'message':'Invalid Input'})
.end(function(FormSaveErr, FormSaveRes) {
// Call the assertion callback
done(FormSaveErr);
@ -316,8 +342,8 @@ describe('Form Routes Unit tests', function() {
admin: user.id,
form_fields: [
new Field({'fieldType':'textfield', 'title':'First Name', 'fieldValue': ''}),
new Field({'fieldType':'checkbox', 'title':'nascar', 'fieldValue': ''}),
new Field({'fieldType':'checkbox', 'title':'hockey', 'fieldValue': ''})
new Field({'fieldType':'legal', 'title':'nascar', 'fieldValue': ''}),
new Field({'fieldType':'legal', 'title':'hockey', 'fieldValue': ''})
],
isLive: true
};
@ -328,8 +354,8 @@ describe('Form Routes Unit tests', function() {
admin: user.id,
form_fields: [
new Field({'fieldType':'textfield', 'title':'Last Name', 'fieldValue': ''}),
new Field({'fieldType':'checkbox', 'title':'formula one', 'fieldValue': ''}),
new Field({'fieldType':'checkbox', 'title':'football', 'fieldValue': ''})
new Field({'fieldType':'legal', 'title':'formula one', 'fieldValue': ''}),
new Field({'fieldType':'legal', 'title':'football', 'fieldValue': ''})
],
isLive: true
};
@ -365,6 +391,123 @@ describe('Form Routes Unit tests', function() {
});
});
it(' > should preserve visitor data when updating a Form', function(done) {
// Create new Form model instance
var formObject = {
title: 'First Form',
language: 'en',
admin: user.id,
form_fields: [
new Field({'fieldType':'textfield', 'title':'First Name', 'fieldValue': ''}),
new Field({'fieldType':'legal', 'title':'nascar', 'fieldValue': ''}),
new Field({'fieldType':'legal', 'title':'hockey', 'fieldValue': ''})
],
isLive: true,
analytics: {
gaCode: '',
visitors: sampleVisitorData
}
};
var formUpdateObject = {
title: 'Second Form',
language: 'en',
admin: user.id,
form_fields: [
new Field({'fieldType':'textfield', 'title':'Last Name', 'fieldValue': ''}),
new Field({'fieldType':'legal', 'title':'formula one', 'fieldValue': ''}),
new Field({'fieldType':'legal', 'title':'football', 'fieldValue': ''})
],
isLive: true
};
var CurrentForm = new Form(formObject);
// Save the Form
CurrentForm.save(function(err, form) {
if(err) return done(err);
loginSession.put('/forms/' + form.id)
.send({ form: formUpdateObject })
.expect(200)
.end(function(err, res) {
should.not.exist(err);
Form.findById(form.id, function (FormFindErr, UpdatedForm){
should.not.exist(FormFindErr);
should.exist(UpdatedForm);
var updatedFormObj = UpdatedForm.toJSON();
var oldFormObj = CurrentForm.toJSON();
updatedFormObj.analytics.should.deepEqual(oldFormObj.analytics);
done(FormFindErr);
});
});
});
});
it(' > shouldn\'t allow a user to change the id when updating a form', function(done) {
// Create new Form model instance
var formObject = {
title: 'First Form',
language: 'en',
admin: user.id,
form_fields: [
new Field({'fieldType':'textfield', 'title':'First Name', 'fieldValue': ''}),
new Field({'fieldType':'legal', 'title':'nascar', 'fieldValue': ''}),
new Field({'fieldType':'legal', 'title':'hockey', 'fieldValue': ''})
],
isLive: true
};
var formUpdateObject = {
id: mongoose.Types.ObjectId(),
title: 'First Form',
language: 'en',
admin: user.id,
form_fields: [
new Field({'fieldType':'textfield', 'title':'Last Name', 'fieldValue': ''}),
new Field({'fieldType':'legal', 'title':'formula one', 'fieldValue': ''}),
new Field({'fieldType':'legal', 'title':'football', 'fieldValue': ''})
],
isLive: true
};
var CurrentForm = new Form(formObject);
// Save the Form
CurrentForm.save(function(err, InitialForm) {
if(err) return done(err);
loginSession.put('/forms/' + InitialForm.id)
.send({ form: formUpdateObject })
.expect(200)
.end(function(err, OldForm) {
should.not.exist(err);
Form.findById(InitialForm.id, function (FormFindErr, UpdatedForm){
should.not.exist(FormFindErr);
should.exist(UpdatedForm);
var updatedFormObj = UpdatedForm.toJSON();
var oldFormObj = InitialForm.toJSON();
updatedFormObj = omitDeep('lastModified');
oldFormObj = omitDeep('lastModified');
updatedFormObj.should.deepEqual(oldFormObj);
done(FormFindErr);
});
});
});
});
afterEach('should be able to signout user', function(done){
authenticatedSession.get('/auth/signout')
.expect(200)

View file

@ -11,13 +11,12 @@ var should = require('should'),
_ = require('lodash'),
async = require('async'),
config = require('../../config/config'),
FormSubmission = mongoose.model('FormSubmission');
FormSubmission = require('../models/form_submission.server.model.js');
var exampleDemo = {
address: '880-9650 Velit. St.',
city: '',
dateOfBirth: '10',
displayName: 'Test User',
email: 'polydaic@gmail.com',
firstName: 'Test User',
hin: '',
@ -82,9 +81,8 @@ describe('FormSubmission Model Unit Tests:', function() {
user = new User({
firstName: 'Full',
lastName: 'Name',
displayName: 'Full Name',
email: 'test1@test.com'+Date.now(),
username: 'test1'+Date.now(),
email: 'test1@test.com',
username: 'test1',
password: 'password',
provider: 'local'
});
@ -168,7 +166,7 @@ describe('FormSubmission Model Unit Tests:', function() {
});
it('should be able to find FormSubmission by $elemMatch on form_fields id', function(done){
FormSubmission.findOne({ form: myForm._id, form_fields: {$elemMatch: {globalId: myForm.form_fields[0].globalId} } })
FormSubmission.findOne({ form: myForm.id, form_fields: {$elemMatch: {_id: myForm.form_fields[0]._id} } })
.exec(function(err, submission){
should.not.exist(err);
should.exist(submission);
@ -178,76 +176,6 @@ describe('FormSubmission Model Unit Tests:', function() {
});
});
describe('Test FormField and Submission Logic', function() {
beforeEach(function(done){
//Create Submission
mySubmission = new FormSubmission({
form_fields: _.merge(sampleSubmission, myForm.form_fields),
admin: user,
form: myForm,
timeElapsed: 17.55
});
mySubmission.save(function(err){
should.not.exist(err);
done();
});
});
it('should preserve deleted form_fields that have submissions without any problems', function(done) {
var fieldPropertiesToOmit = ['deletePreserved', 'globalId', 'lastModified', 'created', '_id', 'submissionId', 'isSubmission', 'validFieldTypes', 'title'];
var old_fields = myForm.toObject().form_fields;
var new_form_fields = _.clone(myForm.toObject().form_fields);
new_form_fields.splice(0, 1);
myForm.form_fields = new_form_fields;
myForm.save(function(err, _form) {
should.not.exist(err);
should.exist(_form.form_fields);
var actual_fields = _.deepOmit(_form.toObject().form_fields, fieldPropertiesToOmit);
old_fields = _.deepOmit(old_fields, fieldPropertiesToOmit);
should.deepEqual(actual_fields, old_fields, 'old form_fields not equal to newly saved form_fields');
done();
});
});
it('should delete \'preserved\' form_fields whose submissions have been removed without any problems', function(done) {
var old_fields = myForm.toObject().form_fields;
old_fields.splice(0,1);
var new_form_fields = _.clone(myForm.toObject().form_fields);
new_form_fields.splice(0, 1);
myForm.form_fields = new_form_fields;
myForm.save(function(err, _form){
should.not.exist(err);
should.exist(_form.form_fields);
should.exist(old_fields);
var actual_fields = _.deepOmit(_form.toObject().form_fields, ['lastModified', 'created', '_id']);
old_fields = _.deepOmit(old_fields, ['lastModified', 'created', '_id']);
should.deepEqual(JSON.stringify(actual_fields), JSON.stringify(old_fields)); //'old form_fields not equal to newly saved form_fields');
done();
});
});
afterEach(function(done){
mySubmission.remove(function(){
done();
});
});
});
afterEach(function(done) {
Form.remove().exec(function() {
User.remove().exec(function() {

View file

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

View file

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

View file

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

View file

@ -5,8 +5,8 @@
*/
var should = require('should'),
mongoose = require('mongoose'),
User = mongoose.model('User');
User = require('../models/user.server.model.js');
/**
* Globals
*/

View file

@ -4,26 +4,25 @@ var should = require('should'),
app = require('../../server'),
Session = require('supertest-session'),
mongoose = require('mongoose'),
User = mongoose.model('User'),
User = require('../models/user.server.model.js'),
config = require('../../config/config'),
tmpUser = mongoose.model(config.tempUserCollection);
tmpUser = mongoose.model(config.tempUserCollection),
async = require('async');
/**
* Globals
*/
var credentials, _User, activateToken, userSession;
var credentials, _User, userSession;
/**
* Form routes tests
*/
describe('User CRUD tests', function() {
this.timeout(30000);
beforeEach(function() {
before(function() {
// Create user credentials
credentials = {
email: 'test732@test.com',
username: 'test732',
email: 'test099@test.com',
username: 'test099',
password: 'password3223'
};
@ -31,77 +30,424 @@ describe('User CRUD tests', function() {
_User = {
email: credentials.email,
username: credentials.username,
password: credentials.password
password: credentials.password,
firstName: 'John',
lastName: 'Smith'
};
//Initialize Session
userSession = Session(app);
});
it(' > Create, Verify and Activate a User > ', function() {
it('should be able to create a temporary (non-activated) User', function(done) {
userSession.post('/auth/signup')
.send(_User)
.expect(200)
.end(function(FormSaveErr) {
// Handle error
should.not.exist(FormSaveErr);
tmpUser.findOne({username: _User.username}, function (err, user) {
should.not.exist(err);
describe(' > Create, Verify and Activate a User > ', function() {
this.timeout(10000);
it('should be able to create and activate a User', function(done) {
async.waterfall([
function(callback) {
userSession.post('/auth/signup')
.send(_User)
.expect(200)
.end(function(err) {
callback(err);
});
},
function(callback) {
tmpUser.findOne({username: _User.username})
.lean()
.exec(function (err, user) {
should.exist(user);
_User.username.should.equal(user.username);
_User.firstName.should.equal(user.firstName);
_User.lastName.should.equal(user.lastName);
activateToken = user.GENERATED_VERIFYING_URL;
userSession.get('/auth/verify/'+activateToken)
.expect(200)
.end(function(VerifyErr, VerifyRes) {
// Handle error
if (VerifyErr) {
return done(VerifyErr);
}
(VerifyRes.text).should.equal('User successfully verified');
userSession.post('/auth/signin')
.send(credentials)
.expect('Content-Type', /json/)
.expect(200)
.end(function(signinErr, signinRes) {
// Handle signin error
if (signinErr) {
return done(signinErr);
}
var user = signinRes.body;
(user.username).should.equal(credentials.username);
userSession.get('/auth/signout')
.expect(200)
.end(function(signoutErr, signoutRes) {
// Handle signout error
if (signoutErr) {
return done(signoutErr);
}
(signoutRes.text).should.equal('You have successfully logged out.');
done();
});
});
});
callback(err, user.GENERATED_VERIFYING_URL);
});
});
},
function(activateToken, callback) {
userSession.get('/auth/verify/' + activateToken)
.expect(200)
.end(function(err, res) {
(res.text).should.equal('User successfully verified');
callback(err);
});
},
function(callback) {
userSession.post('/auth/signin')
.send(credentials)
.expect('Content-Type', /json/)
.expect(200)
.end(function(err, res) {
(res.body.username).should.equal(credentials.username);
callback(err);
});
},
function(callback) {
userSession.get('/auth/signout')
.expect(200)
.end(function(err, res) {
(res.text).should.equal('You have successfully logged out.');
callback(err);
});
},
function(callback) {
User.findOne({ username: _User.username })
.lean()
.exec(function(err, user){
should.exist(user);
callback(err);
});
}
], function (err) {
done(err);
});
});
after(function(done){
User.remove().exec(done);
});
});
afterEach(function(done) {
describe(' > Reset Password > ', function(){
this.timeout(10000);
beforeEach(function(done){
var UserObj = new User(_User);
UserObj.save(function(err){
done(err);
});
});
it('should be able to reset password of a created User with a valid passwordResetToken', function(done) {
var changedPassword = 'password1234';
var resetPasswordToken;
async.waterfall([
function(callback) {
userSession.post('/auth/forgot')
.send({ username: _User.username })
.expect(200)
.end(function(err) {
callback(err);
});
},
function(callback) {
User.findOne({ username: _User.username })
.lean()
.exec(function(err, user){
if(err){
callback(err);
}
callback(null, user.resetPasswordToken);
});
},
function(resetPasswordToken, callback) {
userSession.get('/auth/reset/' + resetPasswordToken)
.expect(302)
.end(function(err) {
callback(err, resetPasswordToken);
});
},
function(resetPasswordToken, callback) {
userSession.post('/auth/reset/' + resetPasswordToken)
.send({
newPassword: changedPassword,
verifyPassword: changedPassword
})
.expect(200)
.end(function(err, res) {
callback(err, resetPasswordToken);
});
},
function(resetPasswordToken, callback) {
User.findOne({ username: _User.username })
.exec(function(err, user){
should.exist(user);
user.authenticate(changedPassword).should.be.true();
should.not.exist(user.resetPasswordToken);
callback(err);
});
}
], function (err, result) {
credentials.password = changedPassword;
done(err);
});
});
it('should be not able to reset password of a created User with a invalid passwordResetToken', function(done) {
var changedPassword = 'password4321';
var resetPasswordToken = 'thisIsNotAValidToken';
async.waterfall([
function(callback) {
userSession.post('/auth/forgot')
.send({ username: credentials.username })
.expect(200)
.end(function(err, res) {
callback(err);
});
},
function(callback) {
userSession.get('/auth/reset/' + resetPasswordToken)
.expect(400)
.end(function(err) {
callback(err);
});
},
function(callback) {
userSession.post('/auth/reset/' + resetPasswordToken)
.send({
newPassword: changedPassword,
verifyPassword: changedPassword
})
.expect(400)
.end(function(err, res) {
callback(err);
});
},
function(callback) {
User.findOne({ username: _User.username })
.exec(function(err, user){
should.exist(user);
user.authenticate(changedPassword).should.be.false();
callback(err);
});
}
], function (err, result) {
done(err);
});
});
afterEach(function(done){
User.remove({ username: credentials.username }).exec(done);
});
});
describe(' > User Profile Changes > ', function(){
var profileSession = new Session(app);
this.timeout(10000);
beforeEach(function(done){
var UserObj = new User(_User);
UserObj.save(function(err, user){
done(err);
});
});
it('should be able to change password when logged in', function(done) {
var changedPassword = 'aVeryBadPassword';
async.waterfall([
function(callback) {
userSession.post('/auth/signin')
.send({
username: _User.username,
password: _User.password
})
.expect(200)
.end(function(err, res) {
callback(err);
});
},
function(callback) {
userSession.post('/users/password')
.send({
currentPassword: _User.password,
newPassword: changedPassword,
verifyPassword: changedPassword
})
.expect(200)
.end(function(err, res) {
callback(err);
});
},
function(callback) {
User.findOne({ username: _User.username })
.exec(function(err, user){
user.authenticate(changedPassword).should.be.true();
callback(err);
});
}
], function (err) {
done(err);
});
});
it('should be able to update user when logged in', function(done) {
var newUser = {};
newUser.firstName = 'goodnight';
newUser.lastName = 'everyone';
newUser.email = 'grcg@gcrc.com';
newUser.username = 'grcg';
async.waterfall([
function(callback) {
userSession.post('/auth/signin')
.send({
username: _User.username,
password: _User.password
})
.expect(200)
.end(function(err, res) {
callback(err);
});
},
function(callback) {
userSession.put('/users')
.send(newUser)
.expect(200)
.end(function(err, res) {
callback(err);
});
},
function(callback) {
User.findOne({ username: newUser.username })
.exec(function(err, user){
user.firstName.should.equal(newUser.firstName);
user.lastName.should.equal(newUser.lastName);
user.email.should.equal(newUser.email);
user.username.should.equal(newUser.username);
callback(err);
});
}
], function (err) {
done(err);
});
});
it('should be able to fetch user when logged in', function(done) {
async.waterfall([
function(callback) {
userSession.post('/auth/signin')
.send({
username: _User.username,
password: _User.password
})
.expect(200)
.end(function(err, res) {
callback(err);
});
},
function(callback) {
userSession.get('/users/me')
.expect(200)
.end(function(err, res) {
var user = res.body;
user.firstName.should.equal(_User.firstName);
user.lastName.should.equal(_User.lastName);
user.email.should.equal(_User.email);
user.username.should.equal(_User.username);
callback(err);
});
}
], function (err) {
done(err);
});
});
afterEach(function(done){
userSession.get('/auth/signout')
.end(function(err, res) {
User.remove().exec(done);
});
});
});
describe(' > User API > ', function(){
var apiKey;
this.timeout(10000);
before(function(done){
var UserObj = new User(_User);
UserObj.save(function(err, user){
done(err);
});
});
it('should be able to request API Key', function(done) {
async.waterfall([
function(callback) {
userSession.post('/auth/signin')
.send({
username: _User.username,
password: _User.password
})
.expect(200)
.end(function(err, res) {
callback(err);
});
},
function(callback) {
userSession.get('/auth/genkey')
.expect(200)
.end(function(err, res) {
apiKey = res.body.apiKey;
callback(err);
});
},
function(callback) {
userSession.get('/auth/signout')
.expect(200)
.end(function(err, res) {
callback(err);
});
},
function(callback) {
userSession.get('/users/me?apikey=' + apiKey)
.expect(200)
.end(function(err, res) {
var user = res.body;
user.firstName.should.equal(_User.firstName);
user.lastName.should.equal(_User.lastName);
user.email.should.equal(_User.email);
user.username.should.equal(_User.username);
callback(err);
});
},
], function (err) {
done(err);
});
});
it('should be able to update user with API key', function(done) {
var newUser = {};
newUser.firstName = 'goodnight';
newUser.lastName = 'everyone';
newUser.email = 'grcg@gcrc.com';
newUser.username = 'grcg';
async.waterfall([
function(callback) {
userSession.put('/users?apikey=' + apiKey)
.send(newUser)
.expect(200)
.end(function(err, res) {
callback(err);
});
},
function(callback) {
User.findOne({ username: newUser.username })
.exec(function(err, user){
user.firstName.should.equal(newUser.firstName);
user.lastName.should.equal(newUser.lastName);
user.email.should.equal(newUser.email);
user.username.should.equal(newUser.username);
callback(err);
});
}
], function (err) {
done(err);
});
});
after(function(done){
User.remove().exec(done);
});
});
after(function(done) {
User.remove().exec(function () {
tmpUser.remove().exec(function(){
userSession.destroy();

View file

@ -32,7 +32,7 @@ html(lang='en', xmlns='http://www.w3.org/1999/xhtml')
width: 100%;
height: 100%;
z-index: 9999;
background: url('/static/dist/page-loader.gif') 50% 35% no-repeat rgb(249,249,249);
background: url('/static/modules/core/img/loaders/page-loader.gif') 50% 35% no-repeat rgb(249,249,249);
background-size: 50px 50px;
}
// Fav Icon
@ -46,7 +46,7 @@ html(lang='en', xmlns='http://www.w3.org/1999/xhtml')
script(type='text/javascript').
var signupDisabled = !{signupDisabled};
var socketPort = false;
var socketUrl = false;
var socketUrl = "ws.tellform.com";
var subdomainsDisabled = !{subdomainsDisabled};
//Embedding socketPort
@ -59,7 +59,6 @@ html(lang='en', xmlns='http://www.w3.org/1999/xhtml')
script(type='text/javascript').
socketUrl = "!{socketUrl}"
script(src='/static/lib/jquery/dist/jquery.min.js', type='text/javascript')
link(rel='stylesheet', href='/static/lib/font-awesome/css/font-awesome.min.css')
link(rel='stylesheet', href='/static/lib/bootstrap/dist/css/bootstrap.min.css')
@ -78,12 +77,11 @@ html(lang='en', xmlns='http://www.w3.org/1999/xhtml')
//Socket.io Client Dependency
script(src='/static/lib/socket.io-client/dist/socket.io.min.js')
script(src='/static/lib/jquery-ui/jquery-ui.js', type='text/javascript')
//Minified Bower Dependencies
script(src='/static/lib/angular/angular.min.js')
script(src='/static/dist/form-vendor.min.js')
script(src='/static/lib/angular-ui-date/src/date.js', type='text/javascript')
//Bower JS dependencies
each bowerJSFile in bowerFormJSFiles
script(type='text/javascript', src=bowerJSFile)
// end Bower JS dependencies
//Application JavaScript Files
each jsFile in formJSFiles
@ -94,7 +92,7 @@ html(lang='en', xmlns='http://www.w3.org/1999/xhtml')
//Livereload script rendered
script(async='', type='text/javascript', src='http://#{request.hostname}:35729/livereload.js')
script Raven.config('https://825fefd6b4ed4a4da199c1b832ca845c@sentry.tellform.com/2').install();
//script Raven.config('https://825fefd6b4ed4a4da199c1b832ca845c@sentry.tellform.com/2').install();
if google_analytics_id
script window.ga=function(){ga.q.push(arguments)};ga.q=[];ga.l=+new Date;ga('create','{{google_analytics_id}}','auto');ga('send','pageview')

View file

@ -3,7 +3,9 @@ extends layout.server.view.pug
block content
section.content(ui-view='', ng-cloak='')
script(src='/static/lib/file-saver.js/FileSaver.js', type='text/javascript')
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/quill/1.3.4/quill.snow.min.css')
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/quill/1.3.4/quill.bubble.min.css')
link(rel='stylesheet', href='/static/lib/jquery-ui/themes/flick/jquery-ui.min.css')
//Embedding The User Object
script(type='text/javascript').
@ -42,6 +44,10 @@ block content
script(type='text/javascript', src='https://cdnjs.cloudflare.com/ajax/libs/angular-strap/2.3.8/angular-strap.min.js')
script(src='https://cdnjs.cloudflare.com/ajax/libs/quill/1.3.4/quill.min.js')
script(src='https://cdnjs.cloudflare.com/ajax/libs/ng-quill/3.5.1/ng-quill.js')
script(src='https://unpkg.com/quill-placeholder-module@0.2.0/dist/placeholder-module.js')
//Application JavaScript Files
each jsFile in jsFiles
script(type='text/javascript', src=jsFile)

View file

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

View file

@ -13,19 +13,15 @@
"bootstrap": "^3.3.7",
"angular-resource": "~1.4.7",
"angular-cache-buster": "~0.4.3",
"angular-mocks": "~1.4.7",
"angular-bootstrap": "~0.14.3",
"angular-ui-utils": "~3.0.0",
"angular-ui-router": "~0.2.11",
"ng-file-upload": "^12.0.4",
"angular-raven": "~0.5.11",
"angular-ui-date": "~0.0.11",
"lodash": "~3.10.0",
"angular-ui-sortable": "~0.13.4",
"angular-permission": "~1.1.1",
"file-saver.js": "~1.20150507.2",
"angular-bootstrap-colorpicker": "~3.0.19",
"angular-ui-router-tabs": "~1.7.0",
"angular-scroll": "^1.0.0",
"angular-sanitize": "1.4.14",
"v-button": "^1.1.1",
@ -33,7 +29,6 @@
"raven-js": "^3.0.4",
"tableExport.jquery.plugin": "^1.5.1",
"js-yaml": "^3.6.1",
"angular-ui-select": "https://github.com/tellform/ui-select.git#compiled",
"angular-translate": "~2.11.0",
"ng-translate": "*",
"deep-diff": "^0.3.4",
@ -42,17 +37,25 @@
"mobile-detect": "^1.3.3",
"socket.io-client": "^1.7.2",
"css-toggle-switch": "^4.0.2",
"angular-strap": "^2.3.12"
"angular-strap": "^2.3.12",
"angular-ui-select": "^0.19.8",
"angular-bootstrap-switch": "^0.5.2",
"jquery": "^3.2.1",
"ng-quill": "https://github.com/KillerCodeMonkey/ng-quill.git#master",
"angular-ui-router": "^1.0.11",
"angular-permission": "^5.3.2",
"angular-mocks": "^1.6.6",
"quill": "https://github.com/quilljs/quill/releases/download/v1.3.4/quill.tar.gz",
"jspdf": "^1.3.5"
},
"resolutions": {
"angular-bootstrap": "^0.14.0",
"angular": "1.4.14",
"angular-ui-select": "compiled",
"jspdf": "~1.0.178",
"angular-sanitize": "1.4.14",
"angular-ui-sortable": "^0.17.1",
"angular-ui-date": "~0.0.11",
"angular-input-stars-directive": "master"
"jquery": "^3.2.1",
"angular-ui-router": "^1.0.11",
"angular": "1.6",
"angular-mocks": "^1.6.6",
"quill": "e-tag:792062a8d",
"jspdf": "1.1.239 || 1.3.2"
},
"overrides": {
"BOWER-PACKAGE": {

View file

@ -62,6 +62,12 @@ module.exports.removeRootDir = function(files, removeRoot, addRoot) {
/**
* Get the app's bower dependencies
*/
module.exports.getBowerFormJSAssets = function() {
if(process.env.NODE_ENV === 'production'){
return ['/static/lib/angular/angular.min.js', '/static/dist/vendor.min.js', '/static/lib/angular-ui-date/src/date.js']
}
return this.removeRootDir(minBowerFiles('**/**.js'), 'public/', 'static/');
};
module.exports.getBowerJSAssets = function() {
return this.removeRootDir(minBowerFiles('**/**.js'), 'public/', 'static/');
};

15
config/env/all.js vendored
View file

@ -10,16 +10,14 @@ module.exports = {
db: {
uri: process.env.MONGOLAB_URI || process.env.MONGODB_URI || 'mongodb://'+ (process.env.DB_PORT_27017_TCP_ADDR || '127.0.0.1') + '/mean',
options: {
user: '',
pass: ''
useMongoClient: true
}
},
admin:{
admin: {
email: process.env.ADMIN_EMAIL || 'admin@admin.com',
username: process.env.ADMIN_USERNAME || 'root',
password: process.env.ADMIN_PASSWORD || 'root',
roles: ['user', 'admin']
},
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
@ -103,18 +101,18 @@ module.exports = {
'public/config.js',
'public/application.js',
'public/dist/populate_template_cache.js',
'public/dist/form_populate_template_cache.js',
'public/modules/*/*.js',
'public/modules/*/*/*.js',
'public/modules/*/*/*/*.js',
'public/modules/*/*/*/*/*.js',
'!public/modules/*/tests/**/*.js',
'public/form_modules/forms/*.js',
'public/form_modules/forms/directives/*.js',
'public/form_modules/forms/base/config/*.js',
'public/form_modules/forms/base/config/*/*.js',
'public/form_modules/forms/base/**/*.js',
'public/form_modules/forms/base/*/*.js',
'!public/modules/*/tests/**/*.js',
'!public/modules/*/tests/*.js'
],
form_js: [
'public/form-config.js',
@ -123,8 +121,7 @@ module.exports = {
'public/form_modules/forms/*.js',
'public/form_modules/forms/*/*.js',
'public/form_modules/forms/*/*/*.js',
'public/form_modules/forms/*/*/*/*.js',
'public/form_modules/forms/**.js',
'public/form_modules/forms/**/*.js',
'!public/form_modules/**/tests/**/*.js'
],
views: [

View file

@ -6,8 +6,7 @@ module.exports = {
db: {
uri: process.env.MONGODB_URI || 'mongodb://'+( process.env.DB_PORT_27017_TCP_ADDR || '127.0.0.1') +'/mean',
options: {
user: '',
pass: ''
useMongoClient: true
}
},
log: {

View file

@ -4,6 +4,9 @@ module.exports = {
baseUrl: process.env.BASE_URL || process.env.HEROKU_APP_NAME + '.herokuapp.com' || 'tellform.com',
db: {
uri: process.env.MONGODB_URI || process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_PORT_27017_TCP_ADDR || '127.0.0.1') + '/mean',
options: {
useMongoClient: true
}
},
port: process.env.PORT || 5000,
socketUrl: process.env.SOCKET_URL || 'ws.tellform.com',
@ -30,6 +33,6 @@ module.exports = {
assets: {
css: ['public/dist/application.min.css'],
js: ['public/dist/application.min.js', 'public/dist/populate_template_cache.js'],
form_js: ['public/dist/form-application.min.js', 'public/dist/form_populate_template_cache.js', 'public/dist/form-vendor.min.js']
form_js: ['public/dist/form-application.min.js', 'public/dist/form_populate_template_cache.js']
}
};

60
config/env/secure.js vendored
View file

@ -1,60 +0,0 @@
'use strict';
module.exports = {
baseUrl: 'https://forms.polydaic.com',
port: 8443,
db: {
uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || process.env.MONGODB_URI || 'mongodb://127.0.0.1/mean',
options: {
user: '',
pass: ''
}
},
log: {
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny'
format: 'combined',
// Stream defaults to process.stdout
// Uncomment to enable logging to a log on the file system
options: {
stream: 'access.log'
}
},
sessionCookie: {
path: '/',
httpOnly: false,
// If secure is set to true then it will cause the cookie to be set
// only when SSL-enabled (HTTPS) is used, and otherwise it won't
// set a cookie. 'true' is recommended yet it requires the above
// mentioned pre-requisite.
secure: true,
// Only set the maxAge to null if the cookie shouldn't be expired
// at all. The cookie will expunge when the browser is closed.
maxAge: 7200,
// To set the cookie in a specific domain uncomment the following
// setting:
domain: process.env.BASE_URL || 'localhost:3000'
},
assets: {
css: 'public/dist/application.min.css',
js: 'public/dist/application.min.js'
},
mailer: {
from: process.env.MAILER_FROM || '',
options: process.env.MAILER_SMTP_HOST ? { //Uses custom SMTP if MAILER_SMTP_HOST is set
host: process.env.MAILER_SMTP_HOST || '',
port: process.env.MAILER_SMTP_PORT || 587,
secure: process.env.MAILER_SMTP_SECURE || true,
auth: {
user: process.env.MAILER_EMAIL_ID || '',
pass: process.env.MAILER_PASSWORD || ''
}
} : {
service: process.env.MAILER_SERVICE_PROVIDER || '',
auth: {
user: process.env.MAILER_EMAIL_ID || '',
pass: process.env.MAILER_PASSWORD || ''
}
}
}
};

4
config/env/test.js vendored
View file

@ -5,8 +5,7 @@ module.exports = {
db: {
uri: 'mongodb://localhost/mean-test',
options: {
user: '',
pass: ''
useMongoClient: true
}
},
port: 3001,
@ -19,6 +18,7 @@ module.exports = {
//stream: 'access.log'
}
},
subdomainsDisabled: true,
app: {
title: 'TellForm Test'
},

View file

@ -39,8 +39,9 @@ var configureSocketIO = function (app, db) {
var supportedLanguages = ['en', 'de', 'fr', 'it', 'es'];
function containsAnySupportedLanguages(preferredLanguages){
for (var i = 0; i < preferredLanguages.length; i++) {
var currIndex = supportedLanguages.indexOf(preferredLanguages[i]);
var i, currIndex;
for (i = 0; i < preferredLanguages.length; i++) {
currIndex = supportedLanguages.indexOf(preferredLanguages[i]);
if (currIndex > -1) {
return supportedLanguages[currIndex];
}
@ -77,6 +78,7 @@ module.exports = function(db) {
app.locals.socketUrl = config.socketUrl;
}
app.locals.bowerFormJSFiles = config.getBowerFormJSAssets();
app.locals.bowerJSFiles = config.getBowerJSAssets();
app.locals.bowerCssFiles = config.getBowerCSSAssets();
app.locals.bowerOtherFiles = config.getBowerOtherAssets();
@ -226,7 +228,6 @@ module.exports = function(db) {
// Setting the app router and static folder
app.use('/static', express.static(path.resolve('./public')));
app.use('/uploads', express.static(path.resolve('./uploads')));
// CookieParser should be above session
app.use(cookieParser());
@ -237,7 +238,7 @@ module.exports = function(db) {
resave: true,
secret: config.sessionSecret,
store: new MongoStore({
mongooseConnection: db.connection,
mongooseConnection: mongoose.connection,
collection: config.sessionCollection
}),
cookie: config.sessionCookie,
@ -261,6 +262,7 @@ module.exports = function(db) {
//Visitor Language Detection
app.use(function(req, res, next) {
var acceptLanguage = req.headers['accept-language'];
var languages, supportedLanguage;
if(acceptLanguage){
@ -270,13 +272,12 @@ module.exports = function(db) {
if(!req.user && supportedLanguage !== null){
var currLanguage = res.cookie('userLang');
if(currLanguage && currLanguage !== supportedLanguage || !currLanguage){
res.clearCookie('userLang');
res.cookie('userLang', supportedLanguage, { maxAge: 90000, httpOnly: true });
} else if(req.user && (!req.cookies.hasOwnProperty('userLang') || req.cookies.userLang !== req.user.language) ){
res.cookie('userLang', req.user.language, { maxAge: 90000, httpOnly: true });
}
} else if(req.user && (!req.cookies.hasOwnProperty('userLang') || req.cookies['userLang'] !== req.user.language) ){
res.cookie('userLang', req.user.language, { maxAge: 90000, httpOnly: true });
}
next();
});
@ -337,22 +338,6 @@ module.exports = function(db) {
});
});
if (process.env.NODE_ENV === 'secure') {
// Load SSL key and certificate
var privateKey = fs.readFileSync('./config/sslcerts/key.pem', 'utf8');
var certificate = fs.readFileSync('./config/sslcerts/cert.pem', 'utf8');
// Create HTTPS Server
var httpsServer = https.createServer({
key: privateKey,
cert: certificate
}, app);
// Return HTTPS server instance
return httpsServer;
}
app = configureSocketIO(app, db);
// Return Express server instance

View file

@ -63,7 +63,6 @@ logger.setupFileLogger = function setupFileLogger() {
return false;
}
};
/**
@ -76,7 +75,7 @@ logger.getLogOptions = function getLogOptions() {
var _config = _.clone(config, true);
var configFileLogger = _config.log.fileLogger;
if (!_.has(_config, 'log.fileLogger.directoryPath') || !_.has(_config, 'log.fileLogger.fileName')) {
if (process.env.NODE_ENV !== 'test' && !_.has(_config, 'log.fileLogger.directoryPath') || !_.has(_config, 'log.fileLogger.fileName')) {
console.log('unable to find logging file configuration');
return false;
}
@ -97,7 +96,6 @@ logger.getLogOptions = function getLogOptions() {
handleExceptions: true,
humanReadableUnhandledException: true
};
};
/**

View file

@ -6,14 +6,23 @@ module.exports.isAuthenticatedOrApiKey = function isAuthenticated(req, res, next
if (req.isAuthenticated()) {
return next();
}
// Try authenticate with API KEY
if (req.headers.apikey || req.query.apikey || req.body.apikey) {
passport.authenticate('localapikey', function (err, user, info) {
if (err)
return res.sendStatus(500);
if(!req.body.apikey && req.headers.apikey){
req.body.apikey = req.headers.apikey;
} else if(!req.query.apikey && req.headers.apikey){
req.query.apikey = req.headers.apikey;
}
if (!user)
passport.authenticate('localapikey', function (err, user, info) {
if (err) {
return res.status(500).send('Internal Server Error with API. Sorry about that!');
}
if (!user) {
return res.status(401).send(info.message || '');
}
req.login(user, function(loginErr) {
if (loginErr) return res.sendStatus(500);
@ -28,23 +37,3 @@ module.exports.isAuthenticatedOrApiKey = function isAuthenticated(req, res, next
}
};
module.exports.hasRole = function hasRole(roleRequired) {
if (!roleRequired) {
throw new Error('Required role needs to be set');
}
return function(req, res, next) {
return module.exports.isAuthenticated(req, res, function() {
if (req.user && req.user.roles && req.user.roles.indexOf(roleRequired) !== -1){
return next();
}
return res.sendStatus(403);
});
};
};
module.exports.hasAdminRole = function hasAdminRole() {
return module.exports.hasRole('admin');
};

View file

@ -11,13 +11,15 @@ module.exports = function() {
return User.findOne({
'apiKey': apiKey
}, function(err, user) {
if (err)
if (err) {
return done(err);
}
if (!user)
if (!user){
return done(null, false, {
message: 'Unknown API Key'
});
}
return done(null, user);
});

View file

@ -18,16 +18,21 @@ var bowerArray = ['public/lib/angular/angular.min.js',
'public/lib/js-yaml/dist/js-yaml.js',
'public/lib/angular-sanitize/angular-sanitize.min.js'];
const bowerFiles = require('main-bower-files');
const bowerDep = bowerFiles('**/**.js');
module.exports = function(grunt) {
require('jit-grunt')(grunt);
var angularTestDeps = ['public/lib/angular/angular.js', 'public/lib/angular-mocks/angular-mocks.js'];
// Unified Watch Object
var watchFiles = {
serverViews: ['app/views/**/*.pug'],
serverJS: ['gruntfile.js', 'server.js', 'config/**/*.js', 'app/**/*.js', '!app/tests/'],
clientViews: ['public/modules/**/*.html', 'public/form_modules/forms/base/**/*.html', '!public/modules/forms/base/**/*.html',],
clientJS: ['public/form_modules/**/*.js', 'public/modules/**/*.js'],
clientJS: ['public/config.js', 'public/form-config.js', 'public/application.js', 'public/form-application.js', 'public/form_modules/**[!tests]/*.js', 'public/modules/**[!tests]/*.js'],
clientCSS: ['public/modules/**/*.css'],
serverTests: ['app/tests/**/*.js'],
@ -123,7 +128,7 @@ module.exports = function(grunt) {
compress: true
},
files: {
'public/dist/form-vendor.min.js': bowerArray
'public/dist/vendor.min.js': bowerArray
}
}
},
@ -201,66 +206,26 @@ module.exports = function(grunt) {
level: 'log',
terminal: true
},
singleRun: true
singleRun: false
}
},
protractor: {
options: {
configFile: 'protractor.conf.js',
keepAlive: true,
noColor: false
},
e2e: {
options: {
args: {} // Target-specific arguments
}
}
},
mocha_istanbul: {
coverage: {
src: watchFiles.allTests, // a folder works nicely
options: {
mask: '*.test.js',
require: ['server.js']
}
},
coverageClient: {
src: watchFiles.clientTests, // specifying file patterns works as well
options: {
coverageFolder: 'coverageClient',
mask: '*.test.js',
require: ['server.js']
}
},
coverageServer: {
src: watchFiles.serverTests,
options: {
coverageFolder: 'coverageServer',
mask: '*.test.js',
require: ['server.js']
}
},
coveralls: {
src: watchFiles.allTests, // multiple folders also works
options: {
require: ['server.js'],
coverage: true, // this will make the grunt.event.on('coverage') event listener to be triggered
root: './lib', // define where the cover task should consider the root of libraries that are covered by tests
reportFormats: ['cobertura','lcovonly']
require: ['server.js'],
reportFormats: ['html','lcovonly']
}
}
},
istanbul_check_coverage: {
default: {
options: {
coverageFolder: 'coverage*', // will check both coverage folders and merge the coverage results
check: {
lines: 80,
statements: 80
}
}
}
},
lcovMerge: {
options: {
emitters: ['event'],
},
src: ['./coverageServer/*.info', './coverageClient/**/*.info']
},
html2js: {
options: {
base: 'public',
@ -287,7 +252,7 @@ module.exports = function(grunt) {
options: {
module: 'TellForm.templates'
},
src: ['public/modules/**/views/**.html', 'public/modules/**/views/**/*.html', 'public/form_modules/forms/base/**/*.html', '!public/modules/forms/base/**/*.html'],
src: ['public/modules/**/views/**.html', 'public/modules/**/views/**/*.html', 'public/form_modules/forms/base/**/*.html'],
dest: 'public/dist/populate_template_cache.js'
}
},
@ -323,9 +288,7 @@ module.exports = function(grunt) {
});
// Code coverage tasks.
grunt.registerTask('coveralls', ['env:test','mocha_istanbul:coveralls']);
grunt.registerTask('coverage', ['env:test', 'mocha_istanbul:coverage']);
grunt.registerTask('coverage:client', ['env:test', 'mocha_istanbul:coverageClient']);
grunt.registerTask('coveralls', ['test:client', 'karma:unit', 'mocha_istanbul:coverageServer', 'lcovMerge']);
grunt.registerTask('coverage:server', ['env:test', 'mocha_istanbul:coverageServer']);
// Default task(s).
@ -336,7 +299,7 @@ module.exports = function(grunt) {
grunt.registerTask('debug', ['lint', 'html2js:main', 'html2js:forms', 'concurrent:debug']);
// Lint task(s).
grunt.registerTask('lint', ['jshint', 'csslint', 'i18nlint:client', 'i18nlint:server']);
grunt.registerTask('lint', ['jshint', 'csslint']);
grunt.registerTask('lint:tests', ['jshint:allTests']);
// Build task(s).
@ -346,9 +309,11 @@ module.exports = function(grunt) {
grunt.registerTask('setup', ['execute']);
// Test task(s).
grunt.registerTask('test', ['lint:tests', 'test:server', 'test:client']);
grunt.registerTask('test', ['test:server', 'test:client']);
grunt.registerTask('test:server', ['lint:tests', 'env:test', 'mochaTest']);
grunt.registerTask('test:client', ['lint:tests', 'html2js:main', 'html2js:forms', 'env:test', 'karma:unit']);
grunt.registerTask('test:travis', ['coverage:server', 'test:client', 'lcovMerge']);
grunt.registerTask('testdebug', ['env:test', 'karma:debug']);
};

View file

@ -18,7 +18,7 @@ module.exports = function(config) {
frameworks: ['jasmine'],
// List of files / patterns to load in the browser
files: bowerDep.concat(['public/lib/socket.io-client/dist/socket.io.js', 'public/lib/mobile-detect/mobile-detect.js'], applicationConfiguration.assets.js, applicationConfiguration.assets.views, applicationConfiguration.assets.unit_tests),
files: bowerDep.concat(['public/lib/socket.io-client/dist/socket.io.js', 'public/lib/mobile-detect/mobile-detect.js', 'public/lib/quill/quill.js', 'public/lib/ng-quill/src/ng-quill.js'], applicationConfiguration.assets.js, applicationConfiguration.assets.views, applicationConfiguration.assets.unit_tests),
// Test results reporter to use
// Possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
@ -29,11 +29,22 @@ module.exports = function(config) {
'public/modules/**/views/**/*.html': ['ng-html2js'],
'public/modules/**/views/*.html': ['ng-html2js'],
'public/form_modules/forms/base/views/**/*.html': ['ng-html2js'],
'public/form_modules/forms/base/views/*.html': ['ng-html2js']
//'public/modules/*/*.js': ['coverage'],
//'public/modules/*/*[!tests]*/*.js': ['coverage'],
'public/form_modules/forms/base/views/*.html': ['ng-html2js'],
'public/modules/*/*.js': ['coverage'],
'public/modules/*/*[!tests]*/*.js': ['coverage'],
'public/form_modules/*/*.js': ['coverage'],
'public/form_modules/*/*[!tests]*/*.js': ['coverage']
},
// configure coverage reporter
coverageReporter: {
reporters: [
//{ type: 'html', subdir: 'report-html' },
{ type: 'lcov' },
],
dir : 'coverageClient/'
},
ngHtml2JsPreprocessor: {
stripPrefix: 'public/',
prependPrefix: 'static/',

1373
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,8 +13,8 @@
"url": "https://github.com/tellform/tellform.git"
},
"engines": {
"node": "6.x.x",
"npm": "3.x.x"
"node": "7.10.1",
"npm": "4.2.0"
},
"scripts": {
"addcontrib": "all-contributors add",
@ -22,6 +22,7 @@
"start": "grunt",
"test": "grunt test",
"postinstall": "bower install --config.interactive=false",
"travis": "grunt test:travis",
"init": "node scripts/setup.js"
},
"dependencies": {
@ -41,7 +42,7 @@
"express": "~4.13.3",
"express-session": "~1.12.1",
"glob": "^7.0.3",
"grunt": "~0.4.1",
"grunt": "^0.4.5",
"grunt-concurrent": "~2.3.0",
"grunt-contrib-csslint": "~1.0.0",
"grunt-contrib-cssmin": "~1.0.1",
@ -53,11 +54,12 @@
"helmet": "3.5.0",
"i18n": "^0.8.3",
"jit-grunt": "^0.9.1",
"jsdom": "^11.3.0",
"lodash": "^4.17.4",
"main-bower-files": "~2.9.0",
"method-override": "~2.3.0",
"mkdirp": "^0.5.1",
"mongoose": "~4.4.19",
"mongoose": "^4.13.0",
"morgan": "~1.8.1",
"nodemailer": "~4.0.0",
"passport": "~0.3.0",
@ -72,7 +74,6 @@
"request": "^2.83.0",
"socket.io": "^1.4.6",
"socket.io-redis": "^1.0.0",
"swig": "~1.4.1",
"uuid-token-generator": "^0.5.0",
"winston": "^2.3.1"
},
@ -86,11 +87,10 @@
"grunt-closure-compiler": "0.0.21",
"grunt-contrib-concat": "^1.0.1",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-uglify": "^0.11.1",
"grunt-contrib-watch": "~0.6.1",
"grunt-execute": "^0.2.2",
"grunt-i18nlint": "github:jwarby/grunt-i18nlint",
"grunt-karma": "~0.12.1",
"grunt-lcov-merge": "^1.2.3",
"grunt-mocha-istanbul": "^3.0.1",
"grunt-mocha-test": "~0.12.1",
"grunt-newer": "~1.1.1",
@ -98,7 +98,7 @@
"grunt-usemin": "^3.1.1",
"grunt-wiredep": "^3.0.1",
"istanbul": "^0.4.0",
"jasmine-core": "^2.4.1",
"jasmine-core": "^2.6",
"karma": "~0.13.14",
"karma-chrome-launcher": "~0.2.1",
"karma-coverage": "~0.5.3",
@ -111,7 +111,7 @@
"mocha": "^3.1.2",
"mocha-lcov-reporter": "^1.0.0",
"nightwatch": "^0.9.8",
"phantomjs": "^1.9.18",
"phantomjs-prebuilt": "^2.1.15",
"selenium-server": "^3.0.1",
"should": "~7.1.1",
"supertest": "~1.2.0",

View file

@ -25,6 +25,9 @@ angular.module(ApplicationConfiguration.applicationModuleName).constant('USER_RO
superuser: 'superuser'
});
//users url
angular.module(ApplicationConfiguration.applicationModuleName).constant('USERS_URL', '/users');
//form url
angular.module(ApplicationConfiguration.applicationModuleName).constant('FORM_URL', '/forms/:formId');

View file

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

View file

@ -28,9 +28,8 @@ angular.module('view-form')
}
return 0;
};
});
angular.module('view-form').value('supportedFields', [
})
.value('supportedFields', [
'textfield',
'textarea',
'date',
@ -44,7 +43,18 @@ angular.module('view-form').value('supportedFields', [
'yes_no',
'number',
'natural'
]);
])
.constant('VIEW_FORM_URL', '/forms/:formId/render')
.filter('indexToAlphabet', function(){
return function(index){
var char = String.fromCharCode(index + 65);
return char;
};
})
angular.module('view-form').constant('VIEW_FORM_URL', '/forms/:formId/render');
//Angular-Scroll Settings
angular.module('view-form').value('duScrollActiveClass', 'activeField')
.value('duScrollGreedy', true)
.value('duScrollOffset', 100)
.value('duScrollSpyWait', 0);

View file

@ -34,6 +34,9 @@ angular.module('view-form').config(['$translateProvider', function ($translatePr
ADD_NEW_LINE_INSTR: 'Press SHIFT+ENTER to add a newline',
ERROR: 'Error',
LOADING_LABEL: 'Loading',
WAIT_LABEL: 'Please wait',
FORM_404_HEADER: '404 - Form Does Not Exist',
FORM_404_BODY: 'The form you are trying to access does not exist. Sorry about that!',

View file

@ -33,13 +33,16 @@ angular.module('view-form').config(['$translateProvider', function ($translatePr
OPTION_PLACEHOLDER: 'Tapez ou sélectionnez une option',
ADD_NEW_LINE_INSTR: 'Appuyez sur MAJ + ENTRÉE pour ajouter une nouvelle ligne',
ERROR: 'Erreur',
LOADING_LABEL: 'Chargement',
WAIT_LABEL: "Veuillez patienter",
FORM_404_HEADER: '404 - Le formulaire n\'existe pas',
FORM_404_BODY: 'Le formulaire auquel vous essayez d\'accéder n\'existe pas. Désolé pour ça!',
FORM_UNAUTHORIZED_HEADER: 'Non autorisé à accéder au formulaire',
   FORM_UNAUTHORIZED_BODY1: 'Le formulaire auquel vous essayez d\'accéder est actuellement privé et inaccessible publiquement.',
   FORM_UNAUTHORIZED_BODY2: 'Si vous êtes le propriétaire du formulaire, vous pouvez le définir sur "Public" dans le panneau "Configuration" du formulaire admin.',
   FORM_UNAUTHORIZED_BODY1: 'Le formulaire auquel vous essayez d\'accéder est actuellement privé et inaccessible publiquement.',
   FORM_UNAUTHORIZED_BODY2: 'Si vous êtes le propriétaire du formulaire, vous pouvez le définir sur "Public" dans le panneau "Configuration" du formulaire admin.',
});
}]);

View file

@ -33,13 +33,16 @@ angular.module('view-form').config(['$translateProvider', function ($translatePr
OPTION_PLACEHOLDER: 'Geben oder wählen Sie eine Option aus',
ADD_NEW_LINE_INSTR: 'Drücken Sie UMSCHALT + EINGABETASTE, um eine neue Zeile hinzuzufügen',
ERROR: 'Fehler',
LOADING_LABEL: 'Laden',
WAIT_LABEL: 'Bitte warten',
FORM_404_HEADER: '404 - Formular existiert nicht',
FORM_404_BODY: 'Das Formular, auf das Sie zugreifen möchten, existiert nicht. Das tut mir leid!',
FORM_UNAUTHORIZED_HEADER: 'Nicht zum Zugriffsformular berechtigt\' ',
   FORM_UNAUTHORIZED_BODY1: 'Das Formular, auf das Sie zugreifen möchten, ist derzeit privat und nicht öffentlich zugänglich.',
   FORM_UNAUTHORIZED_BODY2: 'Wenn Sie der Eigentümer des Formulars sind, können Sie es im Fenster "Konfiguration" im Formular admin auf "Öffentlich" setzen.',
   FORM_UNAUTHORIZED_BODY1: 'Das Formular, auf das Sie zugreifen möchten, ist derzeit privat und nicht öffentlich zugänglich.',
   FORM_UNAUTHORIZED_BODY2: 'Wenn Sie der Eigentümer des Formulars sind, können Sie es im Fenster "Konfiguration" im Formular admin auf "Öffentlich" setzen.',
});
}]);

View file

@ -33,6 +33,9 @@ angular.module('view-form').config(['$translateProvider', function ($translatePr
OPTION_PLACEHOLDER: 'Digitare o selezionare un\'opzione',
ADD_NEW_LINE_INSTR: 'Premere SHIFT + INVIO per aggiungere una nuova riga',
ERROR: 'Errore',
LOADING_LABEL: 'Caricamento',
WAIT_LABEL: "Attendere prego",
FORM_404_HEADER: '404 - Il modulo non esiste',
FORM_404_BODY: 'La forma che stai cercando di accedere non esiste. Ci dispiace!',

View file

@ -34,6 +34,9 @@ angular.module('view-form').config(['$translateProvider', function ($translatePr
ADD_NEW_LINE_INSTR: 'Presione MAYÚS + ENTRAR para agregar una nueva línea',
ERROR: 'Error',
LOADING_LABEL: 'Cargando',
WAIT_LABEL: 'Espera',
FORM_404_HEADER: '404 - La forma no existe',
FORM_404_BODY: 'El formulario al que intenta acceder no existe. ¡Lo siento por eso!',

View file

@ -1,554 +0,0 @@
.panel-default.startPage {
border-style: dashed;
border-color: #a9a9a9;
border-width:3px;
}
.busy-updating-wrapper {
text-align: center;
font-size: 20px;
position: fixed;
bottom: 0;
right: 55px;
z-index: 1;
}
.busy-submitting-wrapper {
position: fixed;
top: 50%;
left: 0;
right: 0;
bottom: 0;
}
.dropzone h4.panel-title {
height: 17px;
overflow: hidden;
}
.container.admin-form {
margin-top: 70px;
}
.public-form input, .public-form textarea {
background-color: #000000;
background-color: rgba(0,0,0,0);
border: 2px dashed #ddd!important;
}
.public-form input:focus, .public-form textarea:focus {
border: 2px dashed #ddd!important;
outline: 0;
}
/*.public-form input.no-border.ng-invalid, .public-form textarea.no-border {
border-color: none;
}*/
.public-form input.ng-valid, .public-form textarea.ng-valid {
border-color: #20FF20!important;
border-style: solid!important;
border-width: 3px!important;
}
.public-form input.ng-invalid.ng-dirty, .public-form textarea.ng-invalid.ng-dirty {
border-color: #FA787E!important;
border-style: solid!important;
border-width: 3px!important;
}
section.content p.breakwords {
word-break: break-all;
}
.btn {
border: 1px solid #c6c6c6;
}
.btn[type='submit'] {
font-size: 1.5em;
padding: 0.35em 1.2em 0.35em 1.2em;
}
section.content > section > section.container {
margin-top: 70px;
}
/*
** Modal CSS Styles
*/
.modal-header {
padding: 15px;
border-bottom: 1px solid #e5e5e5;
font-size: 18px;
font-weight: normal;
}
.input-block {
display: block;
width: 100%;
}
.modal-footer input[type='text'] {
min-height: 34px;
padding: 7px 8px;
font-size: 13px;
color: #333;
vertical-align: middle;
background-color: #fff;
background-repeat: no-repeat;
background-position: right 8px center;
border: 1px solid #ccc;
border-radius: 3px;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.075);
}
.modal-body > .modal-body-alert {
color: #796620;
background-color: #f8eec7;
border-color: #f2e09a;
margin: -16px -15px 15px;
padding: 10px 15px;
border-style: solid;
border-width: 1px 0;
}
div.form-fields {
position: relative;
padding-top: 35vh;
}
.letter {
position: relative;
display: -moz-inline-stack;
display: inline-block;
vertical-align: top;
zoom: 1;
width: 16px;
padding: 0;
height: 17px;
font-size: 12px;
line-height: 19px;
border: 1px solid #000;
border: 1px solid rgba(0,0,0,.2);
margin-right: 7px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
text-align: center;
font-weight: 700;
}
div.form-submitted > .field.row {
padding-bottom: 2%;
margin-top: 35vh;
}
div.form-submitted > .field.row > div {
font-size: 1.7em;
}
/* Styles for accordion */
form .accordion-edit {
width: inherit;
}
/*Styles for ui-datepicker*/
.ui-datepicker.ui-widget {
z-index: 99!important;
}
form .row.field .field-number {
margin-right: 0.5em;
}
/* Styles for form submission view (/forms/:formID) */
form .row.field {
padding: 1em 0 0 0;
width: inherit;
}
form .row.field > .field-title {
margin-top:0.5em;
font-size:1.2em;
padding-bottom: 1.8em;
width: inherit;
}
form .row.field > .field-input {
font-size: 1.4em;
color: #777;
}
form.submission-form .row.field.statement > .field-title {
font-size:1.7em;
}
form.submission-form .row.field.statement > .field-input {
font-size:1em;
color:#ddd;
}
form.submission-form .select.radio > .field-input input, form.submission-form .select > .field-input input {
width:20%;
}
form.submission-form .field.row.radio .btn.activeBtn {
background-color: rgb(0,0,0)!important;
background-color: rgba(0,0,0,0.7)!important;
color: white;
}
form.submission-form .field.row.radio .btn {
margin-right:1.2em;
}
form.submission-form .select > .field-input .btn {
text-align: left;
margin-bottom:0.7em;
}
form.submission-form .select > .field-input .btn > span {
font-size: 1.10em;
}
/*form.submission-form .field-input > input:focus {
font-size:1em;
}*/
form .field-input > textarea{
padding: 0.45em 0.9em;
width: 100%;
line-height: 160%;
}
form .field-input > input.hasDatepicker{
padding: 0.45em 0.9em;
width: 50%;
line-height: 160%;
}
form .field-input > input.text-field-input{
padding: 0.45em 0.9em;
width: 100%;
line-height: 160%;
}
form .required-error{
color: #ddd;
font-size:0.8em;
}
form .row.field.dropdown > .field-input input {
min-height: 34px;
border-width: 0 0 2px 0;
border-radius: 5px;
}
form .row.field.dropdown > .field-input input:focus {
border: none;
}
form .dropdown > .field-input .ui-select-choices-row-inner {
border-radius: 3px;
margin: 5px;
padding: 10px;
background-color: #000000;
background-color: rgba(0,0,0,0.05);
}
form .dropdown > .field-input .ui-select-choices-row-inner.active, form .dropdown > .field-input .ui-select-choices-row-inner.active:focus {
background-color: #000000;
background-color: rgba(0,0,0,0.1);
}
.config-form {
max-width: 100%;
}
.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);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
}
div.config-form .row.field {
padding-top:1.5em;
}
div.config-form > .row > .container:nth-of-type(odd){
border-right: 1px #ddd solid;
}
div.config-form.design > .row > .container:nth-of-type(odd){
border-right: none;
}
div.config-form .row > .field-input {
padding-left:0.1em;
}
div.config-form .row > .field-input label {
padding-left:1.3em;
display: block;
}
/* Styles for form admin view (/forms/:formID/admin) */
.admin-form > .page-header {
padding-bottom: 0;
margin-bottom: 40px;
}
.admin-form > .page-header h1 {
margin-bottom: 0;
margin-top: 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;
}
/*Styles for admin view tabs */
.admin-form .tab-content {
padding-top: 3em;
}
.admin-form .panel-heading {
background-color: #f1f1f1;
position: relative!important;
}
.admin-form .panel-heading:hover {
background-color: #fff;
cursor: pointer;
}
.admin-form .panel-heading a:hover {
text-decoration: none;
}
.current-fields .panel-body .row.question input[type='text'], .current-fields .panel-body .row.description textarea{
width: 100%;
}
.current-fields .panel-body .row.options input[type='text'] {
width: 80%;
}
/*Override Select2 UI*/
.ui-select-choices.ui-select-dropdown {
top:2.5em!important;
}
.ui-select-toggle {
box-shadow:none!important;
border:none!important;
}
.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;
}
/*Styles for submission table*/
.submissions-table .table-outer.row {
margin: 1.5em 0 2em 0!important;
}
.submissions-table .table-outer .col-xs-12 {
padding-left: 0!important;
border:1px solid #ddd;
overflow-x: scroll;
border-radius:3px;
}
.submissions-table .table > thead > tr > th {
min-width:8em;
}
.submissions-table .table > tbody > tr.selected {
background-color:#efefef;
}
/*Styles for add fields tab*/
.admin-form .add-field {
background-color: #ddd;
padding: 0 2% 0 2%;
border-radius: 3px;
}
.admin-form .add-field .col-xs-6 {
padding: 0.25em 0.4em;
}
.admin-form .add-field .col-xs-6 .panel-heading {
border-width: 1px;
border-style: solid;
border-color: #bbb;
border-radius: 4px;
}
.admin-form .oscar-field-select {
margin: 10px 0 10px;
}
.view-form-btn.span {
padding-right:0.6em;
}
.status-light.status-light-off {
color: #BE0000;
}
.status-light.status-light-on {
color: #33CC00;
}
/* Styles for form list view (/forms) */
section.public-form {
padding: 0 10% 0 10%;
}
section.public-form .form-submitted {
height: 100vh;
}
section.public-form .btn {
border: 1px solid;
}
.form-item {
text-align: center;
border-bottom: 6px inset #ccc;
background-color: #eee;
width: 180px;
/*width:100%;*/
position: relative;
height: 215px;
/*padding-bottom: 25%;*/
margin-bottom: 45px;
}
.form-item.create-new input[type='text']{
width: inherit;
color:black;
border:none;
}
.form-item.create-new {
background-color: rgb(131,131,131);
color: white;
}
/*CREATE-NEW FORM MODAL*/
.form-item.new-form {
background-color: rgb(300,131,131);
z-index: 11;
}
.form-item.new-form:hover {
background-color: rgb(300,100,100);
}
.form-item.new-form input[type='text'] {
margin-top:0.2em;
width: inherit;
color:black;
border:none;
padding: 0.3em 0.6em 0.3em 0.6em;
}
.form-item.new-form .custom-select {
margin-top: 0.2em
}
.form-item.new-form .custom-select select {
background-color: white;
}
.form-item.new-form .details-row {
margin-top: 1em;
}
.form-item.new-form .details-row.submit {
margin-top: 1.7em;
}
.form-item.new-form .details-row.submit .btn {
font-size: 0.95em;
}
.form-item.new-form .title-row {
margin-top: 1em;
top:0;
}
/*Modal overlay (for lightbox effect)*/
.overlay {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: rgb(0,0,0);
background-color: rgba(0,0,0,0.5);
z-index: 10;
}
.overlay.submitform {
background-color: rgb(256,256,256);
background-color: rgba(256,256,256,0.8);
}
.field-directive {
z-index: 9;
padding: 10% 10% 10% 0;
border: 25px transparent solid;
position: relative;
}
.activeField {
z-index: 11;
position: relative;
background-color: transparent;
}
.activeField.field-directive {
display: inline-block;
border-radius: 7px;
width: 100%;
border: 25px transparent solid;
}
.activeField input {
background-color: transparent;
}
.form-item:hover, .form-item.create-new:hover {
border-bottom: 8px inset #ccc;
background-color: #d9d9d9;
}
.form-item.create-new:hover {
background-color: rgb(81,81,81);
}
.form-item > .row.footer {
position: absolute;
bottom: 0;
left: 30%;
}
.form-item .title-row{
position: relative;
top: 15px;
padding-top:3em;
padding-bottom:3.65em;
}
.form-item .title-row h4 {
font-size: 1.3em;
}
.form-item.create-new .title-row{
padding: 0;
}
.form-item.create-new .title-row h4 {
font-size: 7em;
}
.form-item .details-row{
margin-top: 3.2em;
}
.form-item .details-row small {
font-size: 0.6em;
}
.form-item.create-new .details-row small {
font-size: 0.95em;
}

View file

@ -65,8 +65,6 @@ angular.module('view-form').directive('onEnterKey', ['$rootScope', function($roo
var keyCode = event.which || event.keyCode;
if(keyCode === 9 && event.shiftKey) {
console.log('onTabAndShiftKey');
event.preventDefault();
$rootScope.$apply(function() {
$rootScope.$eval($attrs.onTabAndShiftKey);

View file

@ -13,31 +13,37 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
function ($http, TimeCounter, $filter, $rootScope, SendVisitorData, $translate, $timeout) {
return {
templateUrl: 'form_modules/forms/base/views/directiveViews/form/submit-form.client.view.html',
restrict: 'E',
restrict: 'E',
scope: {
myform:'=',
ispreview: '='
myform: '='
},
controller: function($document, $window, $scope){
var NOSCROLL = false;
var FORM_ACTION_ID = 'submit_field';
var FORM_ACTION_ID = 'submit_field';
$scope.forms = {};
//Don't start timer if we are looking at a design preview
if($scope.ispreview){
TimeCounter.restartClock();
var form_fields_count = $scope.myform.visible_form_fields.filter(function(field){
return field.fieldType !== 'statement';
}).length;
$scope.$watch('myform', function(oldVal, newVal){
$scope.myform.visible_form_fields = $scope.myform.form_fields.filter(function(field){
return !field.deletePreserved;
});
});
$scope.updateFormValidity = function(){
$timeout(function(){
var nb_valid = $scope.myform.form_fields.filter(function(field){
return (field.fieldType === 'statement' || field.fieldValue !== '' || !field.required);
}).length;
$scope.translateAdvancementData = {
done: nb_valid,
total: $scope.myform.visible_form_fields.length
};
});
}
var form_fields_count = $scope.myform.visible_form_fields.filter(function(field){
return field.fieldType !== 'statement';
}).length;
var nb_valid = $filter('formValidity')($scope.myform);
$scope.translateAdvancementData = {
done: nb_valid,
total: form_fields_count,
answers_not_completed: form_fields_count - nb_valid
};
$scope.updateFormValidity();
$scope.reloadForm = function(){
//Reset Form
@ -47,7 +53,7 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
return field;
}).value();
$scope.loading = false;
$scope.loading = false;
$scope.error = '';
$scope.selected = {
@ -63,240 +69,179 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
/*
** Field Controls
*/
var evaluateLogicJump = function(field){
var logicJump = field.logicJump;
var evaluateLogicJump = function(field){
var logicJump = field.logicJump;
if(logicJump.enabled){
if (logicJump.expressionString && logicJump.valueB && field.fieldValue) {
var parse_tree = jsep(logicJump.expressionString);
var left, right;
if(logicJump.enabled){
if (logicJump.expressionString && logicJump.valueB && field.fieldValue) {
var parse_tree = jsep(logicJump.expressionString);
var left, right;
if(parse_tree.left.name === 'field'){
left = field.fieldValue;
right = logicJump.valueB;
} else {
left = logicJump.valueB;
right = field.fieldValue;
}
if(parse_tree.left.name === 'field'){
left = field.fieldValue;
right = logicJump.valueB;
} else {
left = logicJump.valueB;
right = field.fieldValue;
}
if(field.fieldType === 'number' || field.fieldType === 'scale' || field.fieldType === 'rating'){
switch(parse_tree.operator) {
case '==':
return (parseInt(left) === parseInt(right));
case '!==':
return (parseInt(left) !== parseInt(right));
case '>':
return (parseInt(left) > parseInt(right));
case '>=':
return (parseInt(left) > parseInt(right));
case '<':
return (parseInt(left) < parseInt(right));
case '<=':
return (parseInt(left) <= parseInt(right));
default:
return false;
}
} else {
switch(parse_tree.operator) {
case '==':
return (left === right);
case '!==':
return (left !== right);
case 'contains':
return (left.indexOf(right) > -1);
case '!contains':
/* jshint -W018 */
return !(left.indexOf(right) > -1);
case 'begins':
return left.startsWith(right);
case '!begins':
return !left.startsWith(right);
case 'ends':
return left.endsWith(right);
case '!ends':
return left.endsWith(right);
default:
return false;
}
}
}
}
};
if(field.fieldType === 'number' || field.fieldType === 'scale' || field.fieldType === 'rating'){
switch(parse_tree.operator) {
case '==':
return (parseInt(left) === parseInt(right));
case '!==':
return (parseInt(left) !== parseInt(right));
case '>':
return (parseInt(left) > parseInt(right));
case '>=':
return (parseInt(left) > parseInt(right));
case '<':
return (parseInt(left) < parseInt(right));
case '<=':
return (parseInt(left) <= parseInt(right));
default:
return false;
}
} else {
switch(parse_tree.operator) {
case '==':
return (left === right);
case '!==':
return (left !== right);
case 'contains':
return (left.indexOf(right) > -1);
case '!contains':
/* jshint -W018 */
return !(left.indexOf(right) > -1);
case 'begins':
return left.startsWith(right);
case '!begins':
return !left.startsWith(right);
case 'ends':
return left.endsWith(right);
case '!ends':
return left.endsWith(right);
default:
return false;
}
}
}
}
};
var getActiveField = function(){
if($scope.selected === null){
console.error('current active field is null');
throw new Error('current active field is null');
}
$rootScope.getActiveField = function(){
if($scope.selected === null){
console.error('current active field is null');
throw new Error('current active field is null');
}
if($scope.selected._id === FORM_ACTION_ID) {
return $scope.myform.form_fields.length - 1;
}
return $scope.selected.index;
};
$scope.isActiveField = function(field){
if($scope.selected._id === field._id) {
return true
}
return false;
};
if($scope.selected._id === FORM_ACTION_ID) {
return $scope.myform.visible_form_fields[$scope.selected.index - 1]._id;
}
return $scope.selected._id;
};
$scope.setActiveField = $rootScope.setActiveField = function(field_id, field_index, animateScroll) {
if($scope.selected === null || (!field_id && field_index === null) ) {
return;
if(!field_id && field_index === null) {
return;
}
if(!field_id){
field_id = $scope.myform.visible_form_fields[field_index]._id;
} else if(field_index === null){
field_index = $scope.myform.visible_form_fields.length
if(field_id === FORM_ACTION_ID){
field_index = $scope.myform.visible_form_fields.length;
} else if(!field_id) {
field_id = $scope.myform.visible_form_fields[field_index]._id;
} else if(field_index === null){
field_index = $scope.myform.visible_form_fields.length
for(var i=0; i < $scope.myform.visible_form_fields.length; i++){
var currField = $scope.myform.visible_form_fields[i];
if(currField['_id'] == field_id){
field_index = i;
break;
}
}
}
if($scope.selected._id === field_id){
return;
}
for(var i=0; i < $scope.myform.visible_form_fields.length; i++){
var currField = $scope.myform.visible_form_fields[i];
if(currField['_id'] == field_id){
field_index = i;
break;
}
}
}
if(!$scope.selected){
$scope.selected = {
_id: '',
index: 0
}
}
if($scope.selected._id === field_id){
return;
}
$scope.selected._id = field_id;
$scope.selected.index = field_index;
var nb_valid = $filter('formValidity')($scope.myform);
$scope.translateAdvancementData = {
done: nb_valid,
total: form_fields_count,
answers_not_completed: form_fields_count - nb_valid
};
if(animateScroll){
NOSCROLL=true;
setTimeout(function() {
$document.scrollToElement(angular.element('.activeField'), -10, 200).then(function() {
NOSCROLL = false;
setTimeout(function() {
if (document.querySelectorAll('.activeField .focusOn').length) {
//Handle default case
document.querySelectorAll('.activeField .focusOn')[0].focus();
} else if(document.querySelectorAll('.activeField input').length) {
//Handle case for rating input
document.querySelectorAll('.activeField input')[0].focus();
} else {
//Handle case for dropdown input
document.querySelectorAll('.activeField .selectize-input')[0].focus();
}
});
});
$document.scrollToElement(angular.element('#'+field_id), -10, 300).then(function() {
if (angular.element('#'+field_id+' .focusOn').length) {
//Handle default case
angular.element('#'+field_id+' .focusOn')[0].focus();
} else if(angular.element('#'+field_id+' input').length) {
//Handle case for rating input
angular.element('#'+field_id+' input')[0].focus();
} else {
//Handle case for dropdown input
angular.element('#'+field_id+'.selectize-input')[0].focus();
}
});
}
} else {
if (angular.element('#'+field_id+' .focusOn').length) {
//Handle default case
angular.element('#'+field_id+' .focusOn')[0].focus();
} else if(angular.element('#'+field_id+' input').length) {
//Handle case for rating input
angular.element('#'+field_id+' input')[0].focus();
} else if(angular.element('#'+field_id+'.selectize-input').length) {
//Handle case for dropdown input
angular.element('#'+field_id+'.selectize-input')[0].focus();
}
}
};
$scope.$watch('selected.index', function(oldValue, newValue){
if(oldValue !== newValue && newValue < $scope.myform.form_fields.length){
//Only send analytics data if form has not been submitted
if(!$scope.myform.submitted){
console.log('SendVisitorData.send()');
SendVisitorData.send($scope.myform, newValue, TimeCounter.getTimeElapsed());
}
}
$rootScope.$on('duScrollspy:becameActive', function($event, $element, $target){
$scope.setActiveField($element.prop('id'), null, false);
$scope.updateFormValidity();
$scope.$apply()
if(!$scope.myform.submitted){
SendVisitorData.send($scope.myform, $rootScope.getActiveField(), TimeCounter.getTimeElapsed());
}
});
//Fire event when window is scrolled
$window.onscroll = function(){
if(!NOSCROLL){
var scrollTop = $(window).scrollTop();
var elemBox = document.getElementsByClassName('activeField')[0].getBoundingClientRect();
var fieldTop = elemBox.top;
var fieldBottom = elemBox.bottom;
var field_id, field_index;
var elemHeight = $('.activeField').height();
var submitSectionHeight = $('.form-actions').height();
var maxScrollTop = $(document).height() - $(window).height();
var fieldWrapperHeight = $('form_fields').height();
var selector = 'form > .field-directive:nth-of-type(' + String($scope.myform.visible_form_fields.length - 1)+ ')'
var fieldDirectiveHeight = $(selector).height()
var scrollPosition = maxScrollTop - submitSectionHeight - fieldDirectiveHeight*1.2;
var fractionToJump = 0.9;
//Focus on field above submit form button
if($scope.selected.index === $scope.myform.visible_form_fields.length){
if(scrollTop < scrollPosition){
field_index = $scope.selected.index-1;
$scope.setActiveField(null, field_index, false);
$rootScope.nextField = $scope.nextField = function(){
if($scope.selected && $scope.selected.index > -1){
if($scope.selected._id !== FORM_ACTION_ID){
var currField = $scope.myform.visible_form_fields[$scope.selected.index];
//Jump to logicJump's destination if it is true
if(currField.logicJump && currField.logicJump.jumpTo && evaluateLogicJump(currField)){
$scope.setActiveField(currField.logicJump.jumpTo, null, true);
} else if($scope.selected.index < $scope.myform.visible_form_fields.length-1){
$scope.setActiveField(null, $scope.selected.index+1, true);
} else {
$scope.setActiveField(FORM_ACTION_ID, null, true);
}
}
//Focus on submit form button
else if($scope.selected.index === $scope.myform.visible_form_fields.length-1 && scrollTop > scrollPosition){
field_index = $scope.selected.index+1;
$scope.setActiveField(FORM_ACTION_ID, field_index, false);
}
//If we scrolled bellow the current field, move to next field
else if(fieldBottom < elemHeight * fractionToJump && $scope.selected.index < $scope.myform.visible_form_fields.length-1 ){
field_index = $scope.selected.index+1;
$scope.setActiveField(null, field_index, false);
}
//If we scrolled above the current field, move to prev field
else if ( $scope.selected.index !== 0 && fieldTop > elemHeight * fractionToJump) {
field_index = $scope.selected.index-1;
$scope.setActiveField(null, field_index, false);
}
} else {
//If selected is not defined go to the first field
$scope.setActiveField(null, 0, true);
}
$scope.$apply();
};
$rootScope.nextField = $scope.nextField = function(){
if($scope.selected && $scope.selected.index > -1){
if($scope.selected._id !== FORM_ACTION_ID){
var currField = $scope.myform.visible_form_fields[$scope.selected.index];
//Jump to logicJump's destination if it is true
if(currField.logicJump && currField.logicJump.jumpTo && evaluateLogicJump(currField)){
$scope.setActiveField(currField.logicJump.jumpTo, null, true);
} else if($scope.selected.index < $scope.myform.visible_form_fields.length-1){
$scope.setActiveField(null, $scope.selected.index+1, true);
} else {
$scope.setActiveField(FORM_ACTION_ID, null, true);
}
} else {
//If we are at the submit actions page, go to the first field
$rootScope.setActiveField(null, 0, true);
}
} else {
//If selected is not defined go to the first field
$rootScope.setActiveField(null, 0, true);
}
};
$rootScope.prevField = $scope.prevField = function(){
console.log('prevField');
console.log($scope.selected);
var selected_index = $scope.selected.index - 1;
var selected_index = $scope.selected.index - 1;
if($scope.selected.index > 0){
$scope.setActiveField(null, selected_index, true);
}
};
$rootScope.goToInvalid = $scope.goToInvalid = function() {
var field_id = $('.row.field-directive .ng-invalid.focusOn, .row.field-directive .ng-untouched.focusOn:not(.ng-valid)').first().parents('.row.field-directive').first().attr('data-id');
$scope.setActiveField(field_id, null, true);
};
var field_id = $('.ng-invalid, .ng-untouched').first().parents('.row.field-directive').first().attr('id');
$scope.setActiveField(field_id, null, true);
};
/*
** Form Display Functions
@ -308,109 +253,98 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun
}
};
var getDeviceData = function(){
var md = new MobileDetect(window.navigator.userAgent);
var deviceType = 'other';
var getDeviceData = function(){
var md = new MobileDetect(window.navigator.userAgent);
var deviceType = 'other';
if (md.tablet()){
deviceType = 'tablet';
} else if (md.mobile()) {
deviceType = 'mobile';
} else if (!md.is('bot')) {
deviceType = 'desktop';
}
if (md.tablet()){
deviceType = 'tablet';
} else if (md.mobile()) {
deviceType = 'mobile';
} else if (!md.is('bot')) {
deviceType = 'desktop';
}
return {
type: deviceType,
name: window.navigator.platform
};
};
return {
type: deviceType,
name: window.navigator.platform
};
};
var getIpAndGeo = function(){
//Get Ip Address and GeoLocation Data
$.ajaxSetup( { 'async': false } );
var geoData = $.getJSON('https://freegeoip.net/json/').responseJSON;
$.ajaxSetup( { 'async': true } );
var getIpAndGeo = function(){
//Get Ip Address and GeoLocation Data
$.ajaxSetup( { 'async': false } );
var geoData = $.getJSON('https://freegeoip.net/json/').responseJSON;
$.ajaxSetup( { 'async': true } );
if(!geoData || !geoData.ip){
geoData = {
ip: 'Adblocker'
};
}
if(!geoData || !geoData.ip){
geoData = {
ip: 'Adblocker'
};
}
return {
ipAddr: geoData.ip,
geoLocation: {
City: geoData.city,
Country: geoData.country_name
}
};
};
return {
ipAddr: geoData.ip,
geoLocation: {
City: geoData.city,
Country: geoData.country_name
}
};
};
$rootScope.submitForm = $scope.submitForm = function() {
if($scope.forms.myForm.$invalid){
$scope.goToInvalid();
return;
}
$rootScope.submitForm = $scope.submitForm = function() {
if($scope.forms.myForm.$invalid){
$scope.goToInvalid();
return;
}
var _timeElapsed = TimeCounter.stopClock();
$scope.loading = true;
var _timeElapsed = TimeCounter.stopClock();
$scope.loading = true;
var form = _.cloneDeep($scope.myform);
var form = _.cloneDeep($scope.myform);
var deviceData = getDeviceData();
form.device = deviceData;
var deviceData = getDeviceData();
form.device = deviceData;
var geoData = getIpAndGeo();
form.ipAddr = geoData.ipAddr;
form.geoLocation = geoData.geoLocation;
var geoData = getIpAndGeo();
form.ipAddr = geoData.ipAddr;
form.geoLocation = geoData.geoLocation;
form.timeElapsed = _timeElapsed;
form.percentageComplete = $filter('formValidity')($scope.myform) / $scope.myform.visible_form_fields.length * 100;
delete form.endPage
delete form.isLive
delete form.provider
delete form.startPage
delete form.visible_form_fields;
delete form.analytics;
delete form.design;
delete form.submissions;
delete form.submitted;
for(var i=0; i < $scope.myform.form_fields.length; i++){
if($scope.myform.form_fields[i].fieldType === 'dropdown' && !$scope.myform.form_fields[i].deletePreserved){
$scope.myform.form_fields[i].fieldValue = $scope.myform.form_fields[i].fieldValue.option_value;
}
//Get rid of unnessecary attributes for each form field
delete form.form_fields[i].submissionId;
delete form.form_fields[i].disabled;
delete form.form_fields[i].ratingOptions;
delete form.form_fields[i].fieldOptions;
delete form.form_fields[i].logicJump;
delete form.form_fields[i].description;
delete form.form_fields[i].validFieldTypes;
delete form.form_fields[i].fieldType;
}
form.timeElapsed = _timeElapsed;
form.percentageComplete = $filter('formValidity')($scope.myform) / $scope.myform.visible_form_fields.length * 100;
delete form.endPage
delete form.isLive
delete form.provider
delete form.startPage
delete form.visible_form_fields;
delete form.analytics;
delete form.design;
delete form.submissions;
delete form.submitted;
for(var i=0; i < $scope.myform.form_fields.length; i++){
if($scope.myform.form_fields[i].fieldType === 'dropdown' && !$scope.myform.form_fields[i].deletePreserved){
$scope.myform.form_fields[i].fieldValue = $scope.myform.form_fields[i].fieldValue.option_value;
}
}
setTimeout(function () {
$scope.submitPromise = $http.post('/forms/' + $scope.myform._id, form)
.success(function (data, status) {
$scope.myform.submitted = true;
$scope.loading = false;
SendVisitorData.send(form, getActiveField(), _timeElapsed);
})
.error(function (error) {
$scope.loading = false;
console.error(error);
$scope.error = error.message;
});
}, 500);
setTimeout(function () {
$scope.submitPromise = $http.post('/forms/' + $scope.myform._id, form)
.then(function (data, status) {
$scope.myform.submitted = true;
$scope.loading = false;
SendVisitorData.send(form, $rootScope.getActiveField(), _timeElapsed);
}, function (error) {
$scope.loading = false;
console.error(error);
$scope.error = error.message;
});
}, 500);
};
//Reload our form
$scope.reloadForm();
$scope.reloadForm();
}
};
}
]);

View file

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

View file

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

View file

@ -22,12 +22,13 @@
<label class="btn col-md-5 col-xs-12"
ng-class="{activeBtn: field.fieldValue == 'true'}">
<input class="focusOn"
name="{{field.fieldType}}{{index}}"
ng-style="{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}"
type="radio" value="true"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
ng-required="field.required"
ng-change="nextField()"/>
ng-click="nextField()"/>
<div class="letter" style="float:left">
{{ 'Y' | translate }}
</div>
@ -36,12 +37,13 @@
<label class="btn col-md-5 col-md-offset-1 col-xs-12"
ng-class="{activeBtn: field.fieldValue == 'false'}">
<input class="focusOn"
name="{{field.fieldType}}{{index}}"
ng-style="{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}"
type="radio" value="false"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
ng-required="field.required"
ng-change="nextField()"/>
ng-click="nextField()"/>
<div class="letter" style="float:left">
{{ 'N' | translate }}
</div>

View file

@ -22,17 +22,16 @@
<label class="btn col-md-4 col-xs-12 col-sm-12"
style="margin: 0.5em; padding-left:30px"
ng-class="{activeBtn: field.fieldValue == field.fieldOptions[$index].option_value}">
<div class="letter" style="float:left">
{{$index+1}}
<div ng-if="field.fieldOptions.length <= 26" class="letter" style="float:left">
{{$index | indexToAlphabet}}
</div>
<input ng-style="{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}"
ng-focus="setActiveField(field._id, null, false)"
name="{{field.fieldType}}{{index}}"
type="radio" class="focusOn"
value="{{option.option_value}}"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
ng-required="field.required"
ng-change="$root.nextField()"/>
ng-click="$root.nextField()"/>
<span ng-bind="option.option_value"></span>
</label>

View file

@ -15,14 +15,13 @@
<div class="col-xs-12 field-input">
<input-stars max="{{field.ratingOptions.steps}}"
ng-init="field.fieldValue = 1"
name="{{field.fieldType}}{{index}}"
on-shape-click="true"
on-star-click="nextField()"
on-star-click="$root.nextField()"
icon-full="{{field.ratingOptions.shape}}"
icon-base="fa fa-3x"
icon-empty="{{field.ratingOptions.shape}}"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
ng-required="field.required"
on-enter-or-tab-key="nextField()"
on-tab-and-shift-key="prevField()"

View file

@ -18,7 +18,7 @@
{{ 'ADD_NEW_LINE_INSTR' | translate }}
</small>
<textarea class="textarea focusOn" type="text"
ng-focus="setActiveField(field._id, null, false)"
name="{{field.fieldType}}{{index}}"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
ng-class="{ 'no-border': !!field.fieldValue }"

View file

@ -1,5 +1,4 @@
<div class="textfield field row"
ng-click="setActiveField(field._id, index, true)">
<div class="textfield field row">
<div class="col-xs-12 field-title row-fluid" ng-style="{'color': design.colors.questionColor}">
<h3 class="col-xs-12">
<small class="field-number">
@ -26,7 +25,6 @@
placeholder="{{placeholder}}"
ng-class="{ 'no-border': !!field.fieldValue }"
class="focusOn text-field-input"
ng-focus="setActiveField(field._id, null, false)"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
value="field.fieldValue"

View file

@ -26,9 +26,9 @@
<label class="btn btn-default col-md-2 col-sm-3 col-xs-7"
style="background: rgba(0,0,0,0.1); text-align:left;">
<input type="radio" value="true"
name="{{field.fieldType}}{{index}}"
class="focusOn"
style="opacity: 0; margin-left: 0px;"
ng-focus="setActiveField(field._id, null, false)"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
ng-required="field.required"
@ -46,8 +46,8 @@
style="background: rgba(0,0,0,0.1); text-align:left;">
<input type="radio" value="false"
name="{{field.fieldType}}{{index}}"
style="opacity:0; margin-left:0px;"
ng-focus="setActiveField(field._id, null, false)"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
ng-required="field.required"

View file

@ -41,13 +41,13 @@
<div class="row form-field-wrapper">
<form name="forms.myForm" novalidate class="submission-form">
<div ng-repeat="field in myform.form_fields" ng-if="!field.deletePreserved" data-index="{{$index}}" data-id="{{field._id}}" ng-class="{activeField: selected._id == field._id }" class="row field-directive">
<div ng-repeat="field in myform.form_fields" ng-if="!field.deletePreserved" id="{{field._id}}" class="row field-directive" du-scrollspy="{{field._id}}">
<field-directive field="field" design="myform.design" index="$index" forms="forms">
</field-directive>
</div>
<div class="row form-actions" id="submit_field" ng-class="{activeField: selected._id == 'submit_field' }"
ng-style="{ 'background-color':myform.design.colors.buttonColor}" style="border-top: 1px solid #ddd; margin-top: 30vh; height: 100vh; margin-left: 1%; margin-right: 1%;"
<div class="row form-actions" id="submit_field" du-scrollspy="submit_field"
ng-style="{ 'background-color':myform.design.colors.buttonColor}"
style="border-top: 1px solid #ddd; margin-top: 30vh; height: 100vh; margin-left: 1%; margin-right: 1%;"
on-tab-and-shift-key="prevField()"
on-tab-key="nextField()"
on-enter-key="submitForm()">
@ -56,8 +56,10 @@
{{ 'COMPLETING_NEEDED' | translate:translateAdvancementData }}
</div>
<button ng-if="!forms.myForm.$invalid" class="Button btn col-sm-2 col-xs-8 focusOn" v-busy="loading" v-busy-label="Please wait" v-pressable ng-disabled="loading || forms.myForm.$invalid" ng-click="submitForm()" on-enter-key-disabled="loading || forms.myForm.$invalid" ng-style="{'background-color':myform.design.colors.buttonColor, 'color':myform.design.colors.buttonTextColor}" style="font-size: 1.6em; margin-left: 1em; margin-top: 1em;">
<button ng-if="!forms.myForm.$invalid" class="Button btn col-sm-2 col-xs-8 focusOn" v-busy="loading" v-busy-label="{{ 'WAIT_LABEL' | translate }}" v-pressable
ng-disabled="loading || forms.myForm.$invalid" ng-click="submitForm()" on-enter-key-disabled="loading || forms.myForm.$invalid"
ng-style="{'background-color':myform.design.colors.buttonColor, 'color':myform.design.colors.buttonTextColor}"
style="font-size: 1.6em; margin-left: 1em; margin-top: 1em;">
{{ 'SUBMIT' | translate }}
</button>
@ -74,7 +76,7 @@
</form>
</div>
<section ng-if="!myform.hideFooter" class="navbar navbar-fixed-bottom" ng-style="{ 'background-color':myform.design.colors.buttonColor, 'padding-top': '15px', 'border-top': '2px '+ myform.design.colors.buttonTextColor +' solid', 'color':myform.design.colors.buttonTextColor}">
<section ng-if="myform.showFooter" class="navbar navbar-fixed-bottom" ng-style="{ 'background-color':myform.design.colors.buttonColor, 'padding-top': '15px', 'border-top': '2px '+ myform.design.colors.buttonTextColor +' solid', 'color':myform.design.colors.buttonTextColor}">
<div class="container-fluid">
<div class="row">
<div class="col-sm-5 col-md-6 col-xs-5" ng-show="!myform.submitted">

View file

@ -11,7 +11,7 @@
function SendVisitorData(Socket, $state) {
// Create a controller method for sending visitor data
function send(form, lastActiveIndex, timeElapsed) {
function send(form, lastActiveId, timeElapsed) {
var lang = window.navigator.userLanguage || window.navigator.language;
lang = lang.slice(0,2);
@ -33,7 +33,7 @@
referrer: document.referrer,
isSubmitted: form.submitted,
formId: form._id,
lastActiveField: form.form_fields[lastActiveIndex]._id,
lastActiveField: lastActiveId,
timeElapsed: timeElapsed,
language: lang,
deviceType: deviceType,

View file

@ -1,25 +1,29 @@
'use strict';
angular.module('view-form').directive('keyToOption', function(){
angular.module('view-form').directive('keyToOption', ['$rootScope', function($rootScope){
return {
restrict: 'A',
scope: {
field: '='
},
link: function($scope, $element, $attrs, $select) {
$element.bind('keydown keypress', function(event) {
link: function($scope, $element, $attrs) {
$('body').on('keypress', function(event) {
var keyCode = event.which || event.keyCode;
var index = parseInt(String.fromCharCode(keyCode))-1;
if (index < $scope.field.fieldOptions.length) {
var index = -1;
if(keyCode <= 122 && keyCode >= 97){
index = keyCode - 97;
} else if (keyCode <= 90 && keyCode >= 65){
index = keyCode - 65;
}
if ($scope.field.fieldOptions.length <= 26 && $scope.field._id === $rootScope.getActiveField() && index !== -1 && index < $scope.field.fieldOptions.length) {
event.preventDefault();
$scope.$apply(function () {
$scope.field.fieldValue = $scope.field.fieldOptions[index].option_value;
});
}
});
}
};
});
}]);

View file

@ -2,5 +2,5 @@
// Use Application configuration module to register a new module
ApplicationConfiguration.registerModule('view-form', [
'ngFileUpload', 'ui.date', 'angular-input-stars'
'ui.date', 'angular-input-stars', 'duScroll'
]);

View file

@ -8,7 +8,9 @@ angular.module('core').config(['$stateProvider', '$urlRouterProvider',
}
]);
angular.module(ApplicationConfiguration.applicationModuleName).run(['$rootScope', 'Auth', '$state', '$stateParams',
var statesWithoutAuth = ['access_denied', 'signin', 'resendVerifyEmail', 'verify', 'signup', 'signup-success', 'forgot', 'reset-invalid', 'reset', 'reset-success'];
angular.module(ApplicationConfiguration.applicationModuleName).run(['$rootScope', '$state', '$stateParams',
function($rootScope, Auth, $state, $stateParams) {
$rootScope.$state = $state;
@ -20,46 +22,35 @@ angular.module(ApplicationConfiguration.applicationModuleName).run(['$rootScope'
state: fromState,
params: fromParams
}
var statesToIgnore = ['', 'home', 'signin', 'resendVerifyEmail', 'verify', 'signup', 'signup-success', 'forgot', 'reset-invalid', 'reset', 'reset-success'];
//Redirect to listForms if user is authenticated
if(statesToIgnore.indexOf(toState.name) > 0){
if(Auth.isAuthenticated()){
event.preventDefault(); // stop current execution
$state.go('listForms'); // go to listForms page
}
}
//Redirect to 'signup' route if user is not authenticated
else if(toState.name !== 'access_denied' && !Auth.isAuthenticated() && toState.name !== 'submitForm'){
event.preventDefault(); // stop current execution
$state.go('listForms'); // go to listForms page
}
});
}
]);
//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;
angular.module(ApplicationConfiguration.applicationModuleName).run(['$rootScope', 'Auth', 'Authorizer', '$state', '$stateParams',
function($rootScope, Auth, Authorizer, $state, $stateParams) {
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
Auth.ensureHasCurrentUser(User);
user = Auth.currentUser;
//Only run permissions check if it is an authenticated state
if(statesWithoutAuth.indexOf(toState.name) > -1){
Auth.ensureHasCurrentUser().then(
function onSuccess(currentUser){
if(currentUser){
var authenticator = new Authorizer(user);
var permissions = toState && toState.data && toState.data.permissions ? toState.data.permissions : null;
if(user){
authenticator = new Authorizer(user);
if( (permissions !== null) ){
if( !authenticator.canAccess(permissions) ){
if( permissions !== null && !authenticator.canAccess(permissions) ){
event.preventDefault();
$state.go('access_denied');
}
}
},
function onError(error){
event.preventDefault();
$state.go('access_denied');
}
}
);
}
});
}]);
}]);

View file

@ -6,7 +6,7 @@ angular.module('core').config(['$translateProvider', function ($translateProvide
MENU: 'MENU',
SIGNUP_TAB: 'Créer un Compte',
SIGNIN_TAB: 'Connexion',
SIGNOUT_TAB: 'Créer un compte',
SIGNOUT_TAB: 'Déconnexion',
EDIT_PROFILE: 'Modifier Mon Profil',
MY_SETTINGS: 'Mes Paramètres',
CHANGE_PASSWORD: 'Changer mon Mot de Pass',

View file

@ -5,51 +5,67 @@ angular.module('core').controller('HeaderController', ['$rootScope', '$scope', '
$rootScope.signupDisabled = $window.signupDisabled;
$scope.user = $rootScope.user = Auth.ensureHasCurrentUser(User);
Auth.ensureHasCurrentUser().then(function(currUser){
$scope.user = $rootScope.user = currUser;
$scope.authentication = $rootScope.authentication = Auth;
$scope.authentication = $rootScope.authentication = Auth;
//Set global app language
$rootScope.language = $scope.user.language;
$translate.use($scope.user.language);
$rootScope.languages = $scope.languages = ['en', 'fr', 'es', 'it', 'de'];
//Set global app language
$rootScope.language = $scope.user.language;
$translate.use($scope.user.language);
$scope.isCollapsed = false;
$rootScope.hideNav = false;
$scope.menu = Menus.getMenu('topbar');
$scope.signout = function() {
var promise = User.logout();
promise.then(function() {
Auth.logout();
Auth.ensureHasCurrentUser(User);
$scope.user = $rootScope.user = null;
$state.go('listForms');
//Refresh view
$state.reload();
},
function(reason) {
console.error('Logout Failed: ' + reason);
});
};
$scope.toggleCollapsibleMenu = function() {
$scope.isCollapsed = !$scope.isCollapsed;
};
// Collapsing the menu after navigation
$scope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
$scope.isCollapsed = false;
$rootScope.hideNav = false;
if ( angular.isDefined( toState.data ) ) {
$scope.menu = Menus.getMenu('topbar');
if ( angular.isDefined( toState.data.hideNav ) ) {
$rootScope.hideNav = toState.data.hideNav;
}
}
});
$rootScope.languages = ['en', 'fr', 'es', 'it', 'de'];
$rootScope.langCodeToWord = {
'en': 'English',
'fr': 'Français',
'es': 'Español',
'it': 'Italiàno',
'de': 'Deutsch'
};
$rootScope.wordToLangCode = {
'English': 'en',
'Français': 'fr',
'Español': 'es',
'Italiàno': 'it',
'Deutsch': 'de'
};
$scope.signout = function() {
var promise = User.logout();
promise.then(function() {
Auth.logout();
$scope.user = $rootScope.user = null;
$state.go('signin', { reload: true });
},
function(reason) {
console.error('Logout Failed: ' + reason);
});
};
$scope.toggleCollapsibleMenu = function() {
$scope.isCollapsed = !$scope.isCollapsed;
};
// Collapsing the menu after navigation
$scope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
$scope.isCollapsed = false;
$rootScope.hideNav = false;
if ( angular.isDefined( toState.data ) ) {
if ( angular.isDefined( toState.data.hideNav ) ) {
$rootScope.hideNav = toState.data.hideNav;
}
}
});
}, function(){
$state.go('signup');
})
}
]);

View file

@ -20,19 +20,21 @@
// Load the main application module
beforeEach(module(ApplicationConfiguration.applicationModuleName));
beforeEach(module('module-templates'));
//Mock Authentication Service
beforeEach(module(function($provide) {
$provide.service('Auth', function() {
return {
ensureHasCurrentUser: function() {
return sampleUser;
return {
then: function(onFulfilled, onRejected, progressBack) {
return onFulfilled(sampleUser);
}
};
},
isAuthenticated: function() {
return true;
},
getUserState: function() {
return true;
}
};
});
@ -45,9 +47,5 @@
$scope: scope
});
}));
it('should expose the authentication service', function() {
expect(scope.authentication).toBeTruthy();
});
});
})();

View file

@ -1,20 +0,0 @@
'use strict';
(function() {
describe('HomeController', function() {
//Initialize global variables
var scope;
// Load the main application module
beforeEach(module(ApplicationConfiguration.applicationModuleName));
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
$controller('HomeController', {
$scope: scope
});
}));
});
})();

View file

@ -6,16 +6,30 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
//Configure Form Tab View
ADVANCED_SETTINGS: 'Advanced Settings',
FORM_NAME: 'Form Name',
FORM_STATUS: 'Form Status',
FORM_NAME: 'Your tellform is called',
FORM_STATUS: 'Status',
PUBLIC: 'Public',
PRIVATE: 'Private',
GA_TRACKING_CODE: 'Google Analytics Tracking Code',
DISPLAY_FOOTER: 'Display Form Footer?',
DISPLAY_FOOTER: 'Form Footer',
SAVE_CHANGES: 'Save Changes',
CANCEL: 'Cancel',
DISPLAY_START_PAGE: 'Display Start Page?',
DISPLAY_END_PAGE: 'Display Custom End Page?',
DISPLAY_START_PAGE: 'Start Page',
DISPLAY_END_PAGE: 'Custom End Page',
GENERAL_TAB: 'General',
SELF_NOTIFICATIONS_TAB: 'Self notifications',
RESPONDENT_NOTIFICATIONS_TAB: 'Respondent notifications',
SEND_NOTIFICATION_TO: 'Send to',
NO_EMAIL_FIELD_WARNING: 'Error: You need an email field in your form to send the email to your form respondent',
REPLY_TO: 'Reply to',
EMAIL_SUBJECT: 'Subject',
EMAIL_MESSAGE: 'Message',
ENABLE_RESPONDENT_NOTIFICATIONS: 'Respondent Notifications are currently',
ENABLE_SELF_NOTIFICATIONS: 'Self Notifications are currently',
TOGGLE_ENABLED: 'Enabled',
TOGGLE_DISABLED: 'Disabled',
ADD_VARIABLE_BUTTON: 'Add variable',
//List Forms View
CREATE_A_NEW_FORM: 'Create a new form',
@ -35,6 +49,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
LOGIC_JUMP: 'Logic Jump',
SHOW_BUTTONS: 'Additional Buttons',
SAVE_START_PAGE: 'Save',
ADD_OPTIONS_PLACEHOLDER: 'Add one choice per line. Minumum of one choice is required',
//Admin Form View
ARE_YOU_SURE: 'Are you ABSOLUTELY sure?',
@ -47,9 +62,12 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
DELETE_FORM_MD: 'Delete Form',
DELETE: 'Delete',
FORM: 'Form',
VIEW: 'View',
VIEW_MY_TELLFORM: 'View my tellform',
LIVE: 'Live',
PREVIEW: 'Preview',
//Share Tab
COPIED_LABEL: 'Copied',
COPY: 'Copy',
COPY_AND_PASTE: 'Copy and Paste this to add your TellForm to your website',
CHANGE_WIDTH_AND_HEIGHT: 'Change the width and height values to suit you best',
@ -153,7 +171,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
MULTIPLE_CHOICE: 'Multiple Choice',
DROPDOWN: 'Dropdown',
DATE: 'Date',
PARAGRAPH_T: 'Paragraph',
PARAGRAPH_FIELD: 'Paragraph',
YES_NO: 'Yes/No',
LEGAL: 'Legal',
RATING: 'Rating',

View file

@ -5,16 +5,31 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
$translateProvider.translations('fr', {
// Configurer la vue de l'onglet Formulaire
ADVANCED_SETTINGS: 'Paramètres avancés',
FORM_NAME: "Nom du formulaire",
FORM_STATUS: 'Statut du formulaire',
FORM_NAME: "Votre tellform est appelé",
FORM_STATUS: 'Statut',
PUBLIC: 'Public',
PRIVATE: "Privé",
GA_TRACKING_CODE: "Code de suivi Google Analytics",
DISPLAY_FOOTER: "Afficher le pied de formulaire?",
DISPLAY_FOOTER: "Pied de formulaire",
SAVE_CHANGES: 'Enregistrer les modifications',
CANCEL: 'Annuler',
DISPLAY_START_PAGE: "Afficher la page de démarrage?",
DISPLAY_END_PAGE: "Afficher la page de fin personnalisée?",
DISPLAY_START_PAGE: "Page de démarrage",
DISPLAY_END_PAGE: "Page de fin personnalisée",
GENERAL_TAB: 'General',
SELF_NOTIFICATIONS_TAB: 'Self notifications',
RESPONDANT_NOTIFICATIONS_TAB: 'Respondent notifications',
SEND_NOTIFICATION_TO: 'Envoyer à',
NO_EMAIL_FIELD_WARNING: 'Erreur: Vous avez besoin d\'un champ e-mail dans votre formulaire pour envoyer l\'e-mail au répondant de votre formulaire',
REPLY_TO: "Répondre à",
EMAIL_SUBJECT: 'Sujet',
EMAIL_MESSAGE: "Message",
ENABLE_RESPONDENT_NOTIFICATIONS: 'Les notifications des répondants sont actuellement',
ENABLE_SELF_NOTIFICATIONS: 'Les notifications automatiques sont actuellement',
TOGGLE_ENABLED: 'Activé',
TOGGLE_DISABLED: 'Désactivé',
ADD_VARIABLE_BUTTON: "Ajouter une variable",
// Afficher les formulaires
CREATE_A_NEW_FORM: "Créer un nouveau formulaire",
@ -34,6 +49,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
LOGIC_JUMP: 'Saut logique',
SHOW_BUTTONS: 'Boutons supplémentaires',
SAVE_START_PAGE: "Enregistrer",
ADD_OPTIONS_PLACEHOLDER: "Ajouter un choix par ligne. Un minimum d'un choix est requis",
// Affichage du formulaire d'administration
ARE_YOU_SURE: 'Es-tu ABSOLUMENT sûr?',
@ -46,9 +62,12 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
DELETE_FORM_MD: "Supprimer le formulaire",
DELETE: "Supprimer",
FORM: 'Formulaire',
VIEW: "Afficher",
VIEW_MY_TELLFORM: "Afficher ma forme",
LIVE: "Live",
PREVIEW: 'Aperçu',
//Share Tab
COPIED_LABEL: 'Copié',
COPY: "Copier",
COPY_AND_PASTE: "Copiez et collez ceci pour ajouter votre TellForm à votre site Web",
CHANGE_WIDTH_AND_HEIGHT: "Changez les valeurs de largeur et de hauteur pour mieux vous convenir",
@ -152,7 +171,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
MULTIPLE_CHOICE: 'Choix multiple',
DROPDOWN: 'Menu Déroulant',
DATE: 'Date',
PARAGRAPH_T: "Paragraphe",
PARAGRAPH_FIELD: "Paragraphe",
OUI_NON: 'Oui / Non',
LEGAL: 'Légal',
RATING: "Évaluation",

View file

@ -5,16 +5,31 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
$translateProvider.translations('de', {
// Konfigurieren der Formularregisterkarte
ADVANCED_SETTINGS: 'Erweiterte Einstellungen',
FORM_NAME: 'Formularname',
FORM_STATUS: 'Formularstatus',
FORM_NAME: 'Ihr tellform heißt',
FORM_STATUS: 'Status',
PUBLIC: 'Öffentlich',
PRIVATE: 'Privat',
GA_TRACKING_CODE: 'Google Analytics Tracking-Code',
DISPLAY_FOOTER: 'Formularfußzeile anzeigen?',
DISPLAY_FOOTER: 'Fußzeile',
SAVE_CHANGES: 'Änderungen speichern',
CANCEL: 'Abbrechen',
DISPLAY_START_PAGE: 'Startseite anzeigen?',
DISPLAY_END_PAGE: 'Benutzerdefinierte Endseite anzeigen?',
DISPLAY_START_PAGE: 'Startseite',
DISPLAY_END_PAGE: 'Benutzerdefinierte Endseite',
GENERAL_TAB: 'Allgemein',
SELF_NOTIFICATIONS_TAB: 'Selbstbenachrichtigungen',
RESPONDANT_NOTIFICATIONS_TAB: 'Beantwortungsbenachrichtigungen',
SEND_NOTIFICATION_TO: 'Senden an',
NO_EMAIL_FIELD_WARNING: 'Fehler: Sie benötigen ein E-Mail-Feld in Ihrem Formular, um die E-Mail an Ihr Formular zu senden.',
REPLY_TO: 'Antworten auf',
EMAIL_SUBJECT: "Betreff",
EMAIL_MESSAGE: 'Nachricht',
ENABLE_RESPONDENT_NOTIFICATIONS: 'Antwortbenachrichtigungen sind derzeit',
ENABLE_SELF_NOTIFICATIONS: 'Selbstbenachrichtigungen sind derzeit',
TOGGLE_ENABLED: 'Aktiviert',
TOGGLE_DISABLED: 'Deaktiviert',
ADD_VARIABLE_BUTTON: 'Variable hinzufügen',
// Listenformularansicht
CREATE_A_NEW_FORM: 'Erstelle ein neues Formular',
@ -34,6 +49,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
LOGIC_JUMP: 'Logischer Sprung',
SHOW_BUTTONS: 'Zusätzliche Schaltflächen',
SAVE_START_PAGE: 'Speichern',
ADD_OPTIONS_PLACEHOLDER: 'Fügen Sie eine Auswahl pro Zeile hinzu. Mindestens eine Wahl ist erforderlich.',
// Admin-Formularansicht
ARE_YOU_SURE: "Bist du ABSOLUT sicher?",
@ -46,12 +62,14 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
DELETE_FORM_MD: 'Formular löschen',
DELETE: 'Löschen',
FORM: 'Formular',
VIEW: 'Ansicht',
VIEW_MY_TELLFORM: 'Mein tellform anzeigen',
LIVE: 'Leben',
PREVIEW: 'Vorschau',
//Share Tab
COPIED_LABEL: 'Kopiert',
COPY: 'Kopieren',
COPY_AND_PASTE: 'Kopieren und einfügen, um Ihre TellForm auf Ihrer Website hinzuzufügen',
CHANGE_WIDTH_AND_HEIGHT: 'Ändern Sie die Werte für Breite und Höhe, um Ihnen am besten zu entsprechen',
POWERED_BY: 'Unterstützt von',
TELLFORM_URL: "Ihr TellForm ist dauerhaft unter dieser URL",
@ -151,7 +169,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
MULTIPLE_CHOICE: 'Mehrfachauswahl',
DROPDOWN: 'Dropdown-Liste',
DATE: 'Datum',
PARAGRAPH_T: "Absatz",
PARAGRAPH_FIELD: "Absatz",
YES_NO: 'Ja / Nein',
LEGAL: "Rechtliche",
RATING: 'Bewertung',

View file

@ -5,16 +5,31 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
$translateProvider.translations('it', {
// Configura la visualizzazione scheda modulo
ADVANCED_SETTINGS: 'Impostazioni avanzate',
FORM_NAME: 'Nome modulo',
FORM_STATUS: 'Stato modulo',
FORM_NAME: 'Il tuo tellform è chiamato',
FORM_STATUS: 'Stato',
PUBLIC: 'pubblico',
PRIVATE: 'Privato',
GA_TRACKING_CODE: 'Codice di monitoraggio di Google Analytics',
DISPLAY_FOOTER: 'Visualizza piè di pagina?',
DISPLAY_FOOTER: 'Piè di pagina',
SAVE_CHANGES: 'Salva modifiche',
CANCEL: 'Annulla',
DISPLAY_START_PAGE: 'Visualizza pagina iniziale?',
DISPLAY_END_PAGE: 'Mostra pagina finale personalizzata?',
DISPLAY_START_PAGE: 'Pagina iniziale',
DISPLAY_END_PAGE: 'Pagina finale personalizzata',
GENERAL_TAB: 'Generale',
SELF_NOTIFICATIONS_TAB: 'Autodiagnosi',
RESPONDANT_NOTIFICATIONS_TAB: 'Notifiche rispondenti',
SEND_NOTIFICATION_TO: 'Invia a',
NO_EMAIL_FIELD_WARNING: 'Errore: Hai bisogno di un campo e-mail nel tuo modulo per inviare l\'email al tuo interlocutore',
REPLY_TO: 'Rispondi a',
EMAIL_SUBJECT: 'Oggetto',
EMAIL_MESSAGE: 'Messaggio',
ENABLE_RESPONDENT_NOTIFICATIONS: 'Notifiche rispondenti sono attualmente',
ENABLE_SELF_NOTIFICATIONS: 'Le notifiche auto sono attualmente',
TOGGLE_ENABLED: 'Abilitato',
TOGGLE_DISABLED: 'disabilitato',
ADD_VARIABLE_BUTTON: 'Aggiungi variabile',
// Visualizzazione dei moduli di elenco
CREATE_A_NEW_FORM: 'Crea un nuovo modulo',
@ -34,6 +49,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
LOGIC_JUMP: 'Jump Logic',
SHOW_BUTTONS: 'Pulsanti aggiuntivi',
SAVE_START_PAGE: 'Salva',
ADD_OPTIONS_PLACEHOLDER: "Aggiungi una scelta per riga. È necessario un minimo di una scelta.",
// Visualizzazione modulo di amministrazione
ARE_YOU_SURE: 'Sei ASSOLUTAMENTE sicuro?',
@ -46,12 +62,14 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
DELETE_FORM_MD: 'Elimina modulo',
DELETE: 'Elimina',
FORM: 'Forma',
VIEW: 'Visualizza',
VIEW_MY_TELLFORM: 'Visualizza la mia informazione',
LIVE: 'Live',
PREVIEW: 'Anteprima',
// Share Tab
COPIED_LABEL: "Copiato",
COPY: 'Copia',
COPY_AND_PASTE: 'Copia e incolla questo per aggiungere il tuo TellForm al tuo sito web',
CHANGE_WIDTH_AND_HEIGHT: 'Modifica i valori di larghezza e di altezza per adattarti al meglio',
POWERED_BY: 'Offerto da',
TELLFORM_URL: 'Il tuo TellForm è permanente in questo URL',
@ -152,7 +170,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
MULTIPLE_CHOICE: 'Scelta multipla',
DROPDOWN: 'Dropdown',
DATE: 'Data',
PARAGRAPH_T: 'Paragrafo',
PARAGRAPH_FIELD: 'Paragrafo',
YES_NO: 'Sì / no',
LEGAL: 'Legale',
RATING: 'Valutazione',

View file

@ -3,19 +3,33 @@
angular.module('forms').config(['$translateProvider', function ($translateProvider) {
$translateProvider.translations('es', {
//Configure Form Tab View
ADVANCED_SETTINGS: 'Configuraciones avanzadas',
FORM_NAME: 'Nombre del formulario',
FORM_STATUS: 'Estado del formulario',
FORM_NAME: 'Tu tellform se llama',
FORM_STATUS: 'Estado',
PUBLIC: 'Público',
PRIVATE: 'Privado',
GA_TRACKING_CODE: 'Código de Google Analytics',
DISPLAY_FOOTER: '¿Mostrar pie de página?',
DISPLAY_FOOTER: 'Pie de página',
SAVE_CHANGES: 'Grabar',
CANCEL: 'Cancelar',
DISPLAY_START_PAGE: '¿Mostrar página de inicio?',
DISPLAY_END_PAGE: '¿Mostrar paǵina de fin?',
DISPLAY_START_PAGE: 'Página de inicio',
DISPLAY_END_PAGE: 'Página final personalizada',
SELF_NOTIFICATIONS_TAB: 'Auto notificaciones',
RESPONDANT_NOTIFICATIONS_TAB: 'Notificaciones de los demandados',
GENERAL_TAB: 'Général',
SEND_NOTIFICATION_TO: 'Enviar a',
NO_EMAIL_FIELD_WARNING: 'Error: necesita un campo de correo electrónico en su formulario para enviar el correo electrónico a su encuestado',
REPLY_TO: 'Responder a',
EMAIL_SUBJECT: 'Asunto',
EMAIL_MESSAGE: 'Mensaje',
ENABLE_RESPONDENT_NOTIFICATIONS: 'Las notificaciones de los demandados son actualmente',
ENABLE_SELF_NOTIFICATIONS: 'Las notificaciones automáticas están actualmente',
TOGGLE_ENABLED: 'Habilitado',
TOGGLE_DISABLED: 'Desactivado',
ADD_VARIABLE_BUTTON: 'Agregar variable',
//List Forms View
CREATE_A_NEW_FORM: 'Crear formulario',
@ -35,6 +49,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
LOGIC_JUMP: 'Salto lógico',
SHOW_BUTTONS: 'Botones adicionales',
SAVE_START_PAGE: 'Grabar',
ADD_OPTIONS_PLACEHOLDER: 'Agregue una opción por línea. Se requiere un mínimo de una opción',
//Admin Form View
ARE_YOU_SURE: '¿Estás absolutamente seguro?',
@ -47,12 +62,14 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
DELETE_FORM_MD: 'Borrar formulario',
DELETE: 'Borrar',
FORM: 'Formulario',
VIEW: 'Vista',
VIEW_MY_TELLFORM: 'Ver mi tellform',
LIVE: 'Online',
PREVIEW: 'Vista previa',
// Share Tab
COPIED_LABEL: 'Copiado',
COPY: 'Copiar',
COPY_AND_PASTE: 'Copiar y pegar esto para agregar su TellForm a su sitio web',
CHANGE_WIDTH_AND_HEIGHT: 'Cambie los valores de ancho y altura para adaptar el formulario a sus necesidades',
POWERED_BY: 'Con la tecnlogía de',
TELLFORM_URL: 'Tu TellForm está en esta URL permanente',
@ -153,7 +170,7 @@ angular.module('forms').config(['$translateProvider', function ($translateProvid
MULTIPLE_CHOICE: 'Opciones múltiples',
DROPDOWN: 'Desplegable',
DATE: 'Fecha',
PARAGRAPH_T: 'Párrafo',
PARAGRAPH_FIELD: 'Párrafo',
YES_NO: 'Si/No',
LEGAL: 'Legal',
RATING: 'Puntaje',

View file

@ -4,19 +4,12 @@
angular.module('forms').controller('AdminFormController', ['$rootScope', '$window', '$scope', '$stateParams', '$state', 'Forms', 'CurrentForm', '$http', '$uibModal', 'myForm', '$filter', '$translate',
function($rootScope, $window, $scope, $stateParams, $state, Forms, CurrentForm, $http, $uibModal, myForm, $filter, $translate) {
//Set active tab to Create
$scope.activePill = 0;
$scope.copied = false;
$scope.onCopySuccess = function (e) {
$scope.copied = true;
};
$scope = $rootScope;
$scope.animationsEnabled = true;
$scope.myform = myForm;
$rootScope.saveInProgress = false;
$scope.oldForm = _.cloneDeep($scope.myform);
$scope.designTabActive = false
CurrentForm.setForm($scope.myform);
@ -36,7 +29,6 @@ angular.module('forms').controller('AdminFormController', ['$rootScope', '$windo
$scope.actualFormURL = window.location.protocol + '//' + window.location.host + $scope.formURL;
}
var refreshFrame = $scope.refreshFrame = function(){
if(document.getElementById('iframe')) {
document.getElementById('iframe').contentWindow.location.reload();
@ -44,13 +36,59 @@ angular.module('forms').controller('AdminFormController', ['$rootScope', '$windo
};
$scope.tabData = [
{
heading: $filter('translate')('CREATE_TAB'),
route: 'viewForm.create',
active: false
},
{
heading: $filter('translate')('CONFIGURE_TAB'),
templateName: 'configure'
route: 'viewForm.configure.general',
active: false
},
{
heading: $filter('translate')('ANALYZE_TAB'),
route: 'viewForm.analyze',
active: false
},
{
heading: $filter('translate')('SHARE_TAB'),
route: 'viewForm.share.share_form',
active: false
},
{
heading: $filter('translate')('DESIGN_TAB'),
route: 'viewForm.design',
active: false
}
];
$scope.designTabActive = false
$scope.go = function(tab){
var currParentState = $state.current.name.split('.').slice(0,2).join('.');
var tabParentState = tab.route.split('.').slice(0,2).join('.');
if(currParentState !== tabParentState && tabParentState !== 'viewForm.configure.general'){
$state.go(tab.route);
}
};
function setActiveTab() {
$scope.tabData.forEach(function(tab) {
var currentTabState = $state.current.name.split('.').slice(0,2).join('.');
var tabRouteState = tab.route.split('.').slice(0,2).join('.');
tab.active = (currentTabState === tabRouteState);
if(tab.active && tab.route === 'viewForm.design'){
$scope.designTabActive = true;
} else {
$scope.designTabActive = false;
}
});
}
setActiveTab();
$scope.$on("$stateChangeSuccess", setActiveTab());
$scope.deactivateDesignTab = function(){
$scope.designTabActive = false
@ -77,7 +115,6 @@ angular.module('forms').controller('AdminFormController', ['$rootScope', '$windo
$scope.deleteModal = $uibModal.open({
animation: $scope.animationsEnabled,
templateUrl: 'formDeleteModal.html',
controller: 'AdminFormController',
resolve: {
myForm: function(){
return $scope.myform;
@ -170,22 +207,11 @@ angular.module('forms').controller('AdminFormController', ['$rootScope', '$windo
if(dataToSend.analytics && dataToSend.analytics.visitors){
delete dataToSend.analytics.visitors;
}
if(dataToSend.submissions){
delete dataToSend.submissions;
}
if(dataToSend.visible_form_fields){
delete dataToSend.visible_form_fields;
}
if(dataToSend.analytics){
delete dataToSend.analytics.visitors;
delete dataToSend.analytics.fields;
delete dataToSend.analytics.submissions;
delete dataToSend.analytics.views;
delete dataToSend.analytics.conversionRate;
}
delete dataToSend.created;
delete dataToSend.lastModified;
delete dataToSend.__v;
@ -214,4 +240,4 @@ angular.module('forms').controller('AdminFormController', ['$rootScope', '$windo
}
]);
]);

View file

@ -1,13 +1,14 @@
'use strict';
// Forms controller
angular.module('forms').controller('ListFormsController', ['$rootScope', '$scope', '$stateParams', '$state', 'GetForms', 'CurrentForm', '$http', '$uibModal', 'myForms',
function($rootScope, $scope, $stateParams, $state, GetForms, CurrentForm, $http, $uibModal, myForms) {
angular.module('forms').controller('ListFormsController', ['$rootScope', '$scope', '$stateParams', '$state', 'GetForms', 'CurrentForm', '$http', '$uibModal', 'myForms', '$window', '$location',
function($rootScope, $scope, $stateParams, $state, GetForms, CurrentForm, $http, $uibModal, myForms, $window, $location) {
$scope = $rootScope;
$scope.forms = {};
$scope.showCreateModal = false;
$scope.myforms = myForms
$scope.formLanguage = $window.$locale;
$rootScope.languageRegExp = {
regExp: /[@!#$%^&*()\-+={}\[\]|\\/'";:`.,~№?<>]+/i,
@ -43,7 +44,6 @@ angular.module('forms').controller('ListFormsController', ['$rootScope', '$scope
});
};
$scope.cancelDeleteModal = function(){
if($scope.deleteModal){
$scope.deleteModal.dismiss('cancel');
@ -65,18 +65,20 @@ angular.module('forms').controller('ListFormsController', ['$rootScope', '$scope
$scope.setForm = function (form) {
$scope.myform = form;
};
$scope.goToWithId = function(route, id) {
$state.go(route, {'formId': id}, {reload: true});
};
$scope.duplicateForm = function(form_index){
var form = _.cloneDeep($scope.myforms[form_index]);
delete form._id;
delete form.id;
form.title += ' copy';
$http.post('/forms', {form: form})
.success(function(data, status, headers){
$scope.myforms.splice(form_index+1, 0, data);
}).error(function(errorResponse){
.then(function(resp_data, status, headers){
var result_form = resp_data.data;
result_form.submissionNum = 0;
$scope.myforms.splice(form_index+1, 0, result_form);
}, function(errorResponse){
console.error(errorResponse);
if(errorResponse === null){
$scope.error = errorResponse.data.message;
@ -86,20 +88,19 @@ angular.module('forms').controller('ListFormsController', ['$rootScope', '$scope
// Create new Form
$scope.createNewForm = function(){
var form = {};
form.title = $scope.forms.createForm.title.$modelValue;
form.language = $scope.forms.createForm.language.$modelValue;
if($scope.forms.createForm.$valid && $scope.forms.createForm.$dirty){
$http.post('/forms', {form: form})
.success(function(data, status, headers){
// Redirect after save
$scope.goToWithId('viewForm.create', data._id+'');
}).error(function(errorResponse){
console.error(errorResponse);
$scope.error = errorResponse.data.message;
});
.then(function(response, status, headers){
// Redirect after save
$state.go('viewForm.create', {formId: response.data.id}, {reload: true});
}, function(errorResponse){
console.error(errorResponse);
$scope.error = errorResponse.data.message;
});
}
};
@ -109,10 +110,10 @@ angular.module('forms').controller('ListFormsController', ['$rootScope', '$scope
}
$http.delete('/forms/'+$scope.myforms[form_index]._id)
.success(function(data, status, headers){
.then(function(data, status, headers){
$scope.myforms.splice(form_index, 1);
$scope.cancelDeleteModal();
}).error(function(error){
}, function(error){
console.error(error);
});
};

View file

@ -1,88 +1,89 @@
.pull-top {
.admin-form .pull-top {
display: inline-block;
vertical-align: top;
float: none;
}
.box {
.admin-form .box {
padding: 0 5px 0 5px!important;
}
.current-fields .field-row {
.admin-form .current-fields .field-row {
padding: 5px 0;
}
.current-fields .panel {
.admin-form .current-fields .panel {
background-color: #f1f1f1;
margin-top: 0!important;
}
.current-fields .panel:hover {
.admin-form .current-fields .panel:hover {
background-color: #fff;
cursor: pointer;
}
.current-fields .panel.tool-panel {
.admin-form .current-fields .panel.tool-panel {
background-color: white;
}
.current-fields .panel-heading {
.admin-form .current-fields .panel-heading {
background-color: #f1f1f1;
position: relative;
}
.current-fields .panel-heading:hover {
.admin-form .current-fields .panel-heading:hover {
background-color: #fff;
cursor: pointer;
}
.current-fields .panel-heading a:hover {
.admin-form .current-fields .panel-heading a:hover {
text-decoration: none;
}
.current-fields .tool-panel.panel:hover {
.admin-form .current-fields .tool-panel.panel:hover {
border-color: #9d9d9d;
background-color: #eee;
cursor: pointer;
}
.current-fields .tool-panel.panel:hover .panel-heading {
.admin-form .current-fields .tool-panel.panel:hover .panel-heading {
background-color: inherit;
color: #000;
cursor: pointer;
}
.current-fields .tool-panel.panel .panel-heading {
.admin-form .current-fields .tool-panel.panel .panel-heading {
background-color: #fff;
color: #9d9d9d;
}
.current-fields .tool-panel.panel .panel-heading a {
.admin-form .current-fields .tool-panel.panel .panel-heading a {
color: inherit;
}
.current-fields .tool-panel.panel .panel-heading a:hover{
.admin-form .current-fields .tool-panel.panel .panel-heading a:hover{
text-decoration: none;
}
/* Custom Tab CSS */
.nav.nav-pills.nav-stacked {
.admin-form .nav.nav-pills.nav-stacked {
width: 16.66666667%;
float: left;
position: relative;
min-height: 1px;
padding-right: 15px;
}
div.tab-content {
.admin-form .tab-content {
width: 83.33333333%;
position: relative;
min-height: 1px;
float:left;
padding-top: 0!important;
padding-top: 0;
}
.panel-default.startPage {
.admin-form .panel-default.startPage {
border-style: dashed;
border-color: #a9a9a9;
border-width:3px;
}
.busy-updating-wrapper {
.admin-form .busy-updating-wrapper {
text-align: center;
font-size: 20px;
position: fixed;
@ -91,7 +92,7 @@ div.tab-content {
z-index: 1;
}
.busy-submitting-wrapper {
.admin-form .busy-submitting-wrapper {
position: fixed;
top: 50%;
left: 0;
@ -99,7 +100,7 @@ div.tab-content {
bottom: 0;
}
.dropzone h4.panel-title {
.admin-form .dropzone h4.panel-title {
height: 17px;
overflow: hidden;
}
@ -142,46 +143,10 @@ div.tab-content {
visibility: visible !important;
border: none;
padding:1px;
background: #000;
background: #000;
background:rgba(0, 0, 0, 0.5) !important;
}
.config-form {
max-width: 100%;
}
.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);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
}
div.config-form .row.field {
padding-top:1.5em;
}
div.config-form > .row > .container:nth-of-type(odd){
border-right: 1px #ddd solid;
}
div.config-form.design > .row > .container:nth-of-type(odd){
border-right: none;
}
div.config-form .row > .field-input {
padding-left:0.1em;
}
div.config-form .row > .field-input label {
padding-left:1.3em;
display: block;
}
/* Styles for form admin view (/forms/:formID/admin) */
.admin-form > .page-header {
padding-bottom: 0;
@ -203,30 +168,6 @@ div.config-form .row.field {
margin-bottom: none;
}
/*Styles for admin view tabs */
.admin-form .tab-content {
padding-top: 3em;
}
/*Styles for submission table*/
.submissions-table .table-outer.row {
margin: 1.5em 0 2em 0!important;
}
.submissions-table .table-outer .col-xs-12 {
padding-left: 0!important;
border:1px solid #ddd;
overflow-x: scroll;
border-radius:3px;
}
.submissions-table .table > thead > tr > th {
min-width:8em;
}
.submissions-table .table > tbody > tr.selected {
background-color:#efefef;
}
/*Styles for add fields tab*/
.admin-form .add-field {
background-color: #ddd;
@ -243,13 +184,25 @@ div.config-form .row.field {
border-radius: 4px;
}
.admin-form .oscar-field-select {
margin: 10px 0 10px;
}
.view-form-btn {
border: none;
}
.view-form-btn.span {
padding-right:0.6em;
}
.notification-row {
display: inline-block;
padding: 0 5px;
}
.status-light {
font-size: 10px;
}
.notification-row .status-light {
padding-top: 15px;
}
.status-light.status-light-off {
color: #BE0000;
}

View file

@ -0,0 +1,79 @@
configure-form-directive .placeholder-tag {
background-color: #999;
border-radius: 3px 3px 3px 3px;
border: 0;
color: #FFFFFF;
font-style: inherit;
font-size: 11px;
padding: 4px 5px;
margin: 0 2px 2px 2px;
font-family: inherit;
white-space: nowrap;
vertical-align: middle;
cursor: pointer !important;
display: inline!important;
width: 100%;
}
configure-form-directive .ui-select input.form-control {
height: 34px;
padding: 6px;
}
configure-form-directive .config-form .btn-secondary {
border-color: #DDDDDD;
}
configure-form-directive .notification-toggle.toggle-switch {
margin: 5px 0;
}
/* QuillJS Custom Theming */
configure-form-directive .ql-editor {
background: white;
font-size: 18px;
}
configure-form-directive .ql-picker.ql-placeholder {
width: 118px;
}
configure-form-directive .ql-picker.ql-placeholder > span.ql-picker-label:before {
content: attr(data-before);
}
configure-form-directive .ql-picker.ql-placeholder > span.ql-picker-options > span.ql-picker-item::before {
content: attr(data-label);
}
configure-form-directive .config-form .row.field {
padding-top:1.5em;
}
configure-form-directive .config-form > .row > .container:nth-of-type(odd){
border-right: 1px #ddd solid;
}
configure-form-directive .config-form .row > .field-input {
padding-left:0.1em;
}
configure-form-directive .config-form .row > .field-input label {
padding-left:1.3em;
display: block;
}
configure-form-directive .config-form {
max-width: 100%;
}
configure-form-directive .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);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
}

View file

@ -0,0 +1,33 @@
design-form-directive .ui-select input.form-control {
height: 34px;
padding: 6px;
}
design-form-directive .config-form .btn-secondary {
border-color: #DDDDDD;
}
design-form-directive .config-form .row.field {
padding-top: 1.5em;
}
design-form-directive .config-form > .row > .container{
border-right: 1px #ddd solid;
border-right: none;
}
design-form-directive .config-form {
max-width: 100%;
}
design-form-directive .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);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
}

View file

@ -1,4 +1,19 @@
/*Styles for submission table*/
.submissions-table .table-outer.row {
margin: 1.5em 0 2em 0!important;
}
.submissions-table .table-outer .col-xs-12 {
padding-left: 0!important;
border:1px solid #ddd;
overflow-x: scroll;
border-radius:3px;
}
.submissions-table .table > thead > tr > th {
min-width:8em;
}
.submissions-table .table > tbody > tr.selected {
background-color:#efefef;
}
.analytics .header-title {
font-size: 1em;

View file

@ -0,0 +1,26 @@
share-form-directive #copyEmbedded {
min-height: fit-content;
width: 100%;
background-color: #36404B;
color: white;
padding: 18px;
}
share-form-directive .share-row {
padding-top: 2.5em;
}
share-form-directive .config-form {
max-width: 100%;
}
share-form-directive .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);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
}

View file

@ -1,23 +1,87 @@
'use strict';
angular.module('forms').directive('configureFormDirective', ['$rootScope', '$http', 'Upload', 'CurrentForm',
function ($rootScope, $http, Upload, CurrentForm) {
angular.module('forms').directive('configureFormDirective', ['$rootScope', '$state', '$translate', '$timeout', '$window',
function ($rootScope, $state, $translate, $timeout, $window) {
return {
templateUrl: 'modules/forms/admin/views/directiveViews/form/configure-form.client.view.html',
restrict: 'E',
scope: {
myform:'=',
user:'=',
pdfFields:'@',
formFields:'@'
myform:'='
},
controller: function($scope){
$scope.log = '';
$rootScope.myform = $scope.myform;
$scope.languages = $rootScope.languages;
$scope.resetForm = $rootScope.resetForm;
$scope.update = $rootScope.update;
Quill.register('modules/placeholder', PlaceholderModule.default(Quill))
$scope.customModules = {
placeholder: {
placeholders: $scope.myform.visible_form_fields.map(function(field){
return {
id: field.id,
label: field.title
};
}),
className: 'placeholder-tag',
delimiters: ['', '']
}
};
$scope.emailFields = $scope.myform.form_fields.filter(function(field){
return field.fieldType === 'email';
});
$scope.formHasEmailField = ($scope.emailFields.length > 0);
/* Tab Routing Logic */
$scope.configureTabs = [
{
heading: $translate.instant('GENERAL_TAB'),
route: 'viewForm.configure.general',
active: false
},
{
heading: $translate.instant('SELF_NOTIFICATIONS_TAB'),
route: 'viewForm.configure.self_notifications',
active: false
},
{
heading: $translate.instant('RESPONDENT_NOTIFICATIONS_TAB'),
route: 'viewForm.configure.respondent_notifications',
active: false
}
];
$scope.go = function(tab){
tab.active = true;
$state.go(tab.route);
};
function setActiveTab() {
$scope.configureTabs.forEach(function(tab) {
tab.active = ($state.current.name === tab.route);
});
}
setActiveTab();
$scope.$on("$stateChangeSuccess", setActiveTab());
$scope.$on('$viewContentLoaded', function ($evt, data) {
$timeout(function(){
if(!$('.ql-picker.ql-placeholder > span.ql-picker-label').attr('data-before')){
$('.ql-picker.ql-placeholder > span.ql-picker-label').attr('data-before', $translate.instant('ADD_VARIABLE_BUTTON'));
}
}, 500);
});
$scope.saveInProgress = false;
$scope.saveChanges = function(){
$scope.saveInProgress = true;
$rootScope.update(false, $scope.myform, false, false, function(){
$scope.saveInProgress = false;
});
};
}
};
}

View file

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

View file

@ -1,4 +1,3 @@
'use strict';
angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormFields', '$uibModal',
@ -17,14 +16,20 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
*/
var newField;
//Populate local scope with rootScope methods/variables
$scope.update = $rootScope.update;
//Setup UI-Sortable
$scope.sortableOptions = {
appendTo: '.dropzone',
//helper: 'clone',
items: '.sortable-fields',
forceHelperSize: true,
forcePlaceholderSize: true,
update: function(e, ui) {
stop: function(e, ui) {
$scope.update(false, $scope.myform, true, false, function(err){
if(err){
console.error(err);
}
});
},
};
@ -37,22 +42,60 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
animation: true,
templateUrl: 'editFieldModal.html',
windowClass: 'edit-modal-window',
scope: $scope,
controller: function($uibModalInstance, $scope) {
$scope.field = curr_field;
$scope.showLogicJump = false;
var fieldOptionsToString = function(){
if(!$scope.field.fieldOptions){
return '';
}
return $scope.field.fieldOptions.map(function(fieldOption){
return fieldOption.option_value;
}).join('\n');
}
$scope.field = curr_field;
$scope.isEdit = isEdit;
$scope.options = {
isEdit: isEdit,
fieldOptionsString: fieldOptionsToString()
}
var stringToFieldOptions = function(fieldOptionsString){
var values = fieldOptionsString.split('\n');
var fieldOptions = [];
for(var i=0; i < values.length; i++){
fieldOptions.push({
option_value: values[i]
});
}
return fieldOptions;
}
$scope.$watch('options.fieldOptionsString', function(newVal, oldVal){
if(newVal !== oldVal){
$scope.field.fieldOptions = stringToFieldOptions(newVal);
}
});
// decides whether field options block will be shown (true for dropdown and radio fields)
$scope.showAddOptions = function (field){
if($scope.field.fieldType === 'dropdown' || $scope.field.fieldType === 'checkbox' || $scope.field.fieldType === 'radio'){
$scope.showListOptions = function () {
if($scope.field.fieldType === 'dropdown' || $scope.field.fieldType === 'radio'){
return true;
} else {
return false;
}
return false;
};
$scope.validShapes = [
// decides whether field options block will be shown (true for rating fields)
$scope.showRatingOptions = function (){
if($scope.field.fieldType === 'rating'){
return true;
}
return false;
};
$scope.validShapes = [
'Heart',
'Star',
'thumbs-up',
@ -68,42 +111,7 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
'Trash'
];
// add new option to the field
$scope.addOption = function(){
if($scope.field.fieldType === 'checkbox' || $scope.field.fieldType === 'dropdown' || $scope.field.fieldType === 'radio'){
if(!$scope.field.fieldOptions){
$scope.field.fieldOptions = [];
}
var lastOptionID = $scope.field.fieldOptions.length+1;
// new option's id
var newOption = {
'option_id' : Math.floor(100000*Math.random()),
'option_title' : 'Option '+lastOptionID,
'option_value' : 'Option ' +lastOptionID
};
// put new option into fieldOptions array
$scope.field.fieldOptions.push(newOption);
}
};
// delete particular option
$scope.deleteOption = function (option){
if($scope.field.fieldType === 'checkbox' || $scope.field.fieldType === 'dropdown' || $scope.field.fieldType === 'radio'){
for(var i = 0; i < $scope.field.fieldOptions.length; i++){
if($scope.field.fieldOptions[i].option_id === option.option_id){
$scope.field.fieldOptions.splice(i, 1);
break;
}
}
}
};
//Populate Name to Font-awesomeName Conversion Map
//Name to Font-Awesome Conversion Map
$scope.select2FA = {
'Heart': 'Heart',
'Star': 'Star',
@ -120,17 +128,8 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
'Trash': 'Trash Can'
};
// decides whether field options block will be shown (true for dropdown and radio fields)
$scope.showRatingOptions = function (){
if($scope.field.fieldType === 'rating'){
return true;
} else {
return false;
}
};
$scope.saveField = function(){
if($scope.isEdit){
if($scope.options.isEdit){
$scope.myform.form_fields[field_index] = $scope.field;
} else {
$scope.myform.form_fields.push(curr_field);
@ -258,19 +257,6 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
});
};
//Populate local scope with rootScope methods/variables
$scope.update = $rootScope.update;
/*
** FormFields (ui-sortable) drag-and-drop configuration
*/
$scope.dropzone = {
handle: '.handle',
containment: '.dropzoneContainer',
cursor: 'grabbing'
};
/*
** Field CRUD Methods
*/
@ -294,7 +280,7 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
required: true,
disabled: false,
deletePreserved: false,
logicJump: {}
logicJump: {},
};
if(fieldType === 'rating'){
@ -305,36 +291,9 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
newField.fieldValue = 0;
}
if($scope.showAddOptions(newField)){
newField.fieldOptions = [];
newField.fieldOptions.push({
'option_id' : Math.floor(100000*Math.random()), //Generate pseudo-random option id
'option_title' : 'Option 0',
'option_value' : 'Option 0'
});
}
$scope.openEditModal(newField, false, $scope.myform.form_fields.length);
$scope.openEditModal(newField, false, $scope.myform.form_fields.length);
};
// decides whether field options block will be shown (true for dropdown and radio fields)
$scope.showAddOptions = function (field){
if(field.fieldType === 'dropdown' || field.fieldType === 'checkbox' || field.fieldType === 'radio'){
return true;
} else {
return false;
}
};
// decides whether field options block will be shown (true for dropdown and radio fields)
$scope.showRatingOptions = function (field){
if(field.fieldType === 'rating'){
return true;
} else {
return false;
}
};
// Delete particular field on button click
$scope.deleteField = function (field_index) {
$scope.myform.form_fields.splice(field_index, 1);

View file

@ -14,15 +14,17 @@ angular.module('forms').directive('editSubmissionsFormDirective', ['$rootScope',
masterChecker: false,
rows: []
};
$scope.analyticsData = {
deviceStatistics: [],
globalStatistics: []
};
$scope.deletionInProgress = false;
$scope.waitingForDeletion = false;
//Waits until deletionInProgress is false before running getSubmissions
$scope.$watch("deletionInProgress",function(newVal, oldVal){
if(newVal === oldVal) return;
if(newVal === false && $scope.waitingForDeletion) {
if(newVal !== oldVal && newVal === false && $scope.waitingForDeletion) {
$scope.getSubmissions();
$scope.waitingForDeletion = false;
}
@ -36,6 +38,52 @@ angular.module('forms').directive('editSubmissionsFormDirective', ['$rootScope',
}
};
/*
** Analytics Functions
*/
var formatGlobalStatistics = function(globalStatData){
if(!globalStatData || !globalStatData.length){
return {
visits: 0,
responses: 0,
conversion_rate: 0,
average_time: 0
};
}
return globalStatData[0];
}
var formatDeviceStatistics = function(deviceStatData){
var newStatItem = function(){
return {
visits: 0,
responses: 0,
conversion_rate: 0,
average_time: 0,
total_time: 0
};
};
var stats = {
desktop: newStatItem(),
tablet: newStatItem(),
phone: newStatItem(),
other: newStatItem()
};
if(deviceStatData && deviceStatData.length){
for(var i=0; i<deviceStatData.length; i++){
var currDevice = deviceStatData[i];
//_id here is deviceType of field due to aggregation in getVisitorData
if(stats[currDevice._id]){
stats[currDevice._id] = currDevice;
}
}
}
return stats;
};
$scope.getSubmissions = function(cb){
$http({
method: 'GET',
@ -73,14 +121,18 @@ angular.module('forms').directive('editSubmissionsFormDirective', ['$rootScope',
method: 'GET',
url: '/forms/'+$scope.myform._id+'/visitors'
}).then(function successCallback(response) {
var defaultFormFields = _.cloneDeep($scope.myform.form_fields);
var data = response.data || [];
var visitors = response.data || [];
$scope.visitors = visitors;
$scope.analyticsData = data[0];
$scope.analyticsData.globalStatistics = formatGlobalStatistics($scope.analyticsData.globalStatistics);
$scope.analyticsData.deviceStatistics = formatDeviceStatistics($scope.analyticsData.deviceStatistics);
});
};
//Initialize analytics data
$scope.analyticsData.globalStatistics = formatGlobalStatistics();
$scope.analyticsData.deviceStatistics = formatDeviceStatistics();
$scope.handleSubmissionsRefresh();
$scope.getVisitors();
@ -99,66 +151,6 @@ angular.module('forms').directive('editSubmissionsFormDirective', ['$rootScope',
}
});
/*
** Analytics Functions
*/
$scope.AverageTimeElapsed = (function(){
var totalTime = 0;
var numSubmissions = $scope.table.rows.length;
for(var i=0; i<$scope.table.rows.length; i++){
totalTime += $scope.table.rows[i].timeElapsed;
}
if(numSubmissions === 0) {
return 0;
}
return (totalTime/numSubmissions).toFixed(0);
})();
$scope.DeviceStatistics = (function(){
var newStatItem = function(){
return {
visits: 0,
responses: 0,
completion: 0,
average_time: 0,
total_time: 0
};
};
var stats = {
desktop: newStatItem(),
tablet: newStatItem(),
phone: newStatItem(),
other: newStatItem()
};
if($scope.myform.analytics && $scope.myform.analytics.visitors) {
var visitors = $scope.myform.analytics.visitors;
for (var i = 0; i < visitors.length; i++) {
var visitor = visitors[i];
var deviceType = visitor.deviceType;
stats[deviceType].visits++;
if (visitor.isSubmitted) {
stats[deviceType].total_time = stats[deviceType].total_time + visitor.timeElapsed;
stats[deviceType].responses++;
}
if(stats[deviceType].visits) {
stats[deviceType].completion = 100*(stats[deviceType].responses / stats[deviceType].visits).toFixed(2);
}
if(stats[deviceType].responses){
stats[deviceType].average_time = (stats[deviceType].total_time / stats[deviceType].responses).toFixed(0);
}
}
}
return stats;
})();
/*
** Table Functions
*/
@ -196,14 +188,13 @@ angular.module('forms').directive('editSubmissionsFormDirective', ['$rootScope',
method: 'DELETE',
data: {deleted_submissions: delete_ids},
headers: {'Content-Type': 'application/json;charset=utf-8'}
}).success(function(data, status){
}).then(function(data, status){
$scope.deletionInProgress = true;
//Remove deleted ids from table
$scope.table.rows = $scope.table.rows.filter(function(field){
return !field.selected;
});
})
.error(function(err){
}, function(err){
$scope.deletionInProgress = true;
console.error(err);
});

View file

@ -1,15 +1,15 @@
'use strict';
angular.module('view-form').directive('fieldIconDirective', function() {
angular.module('forms').directive('fieldIconDirective', function() {
return {
template: '<i class="{{typeIcon}}"></i>',
template: '<i class="{{iconTypeMap[typeName]}}"></i>',
restrict: 'E',
scope: {
typeName: '@'
typeName: '='
},
controller: function($scope){
var iconTypeMap = {
$scope.iconTypeMap = {
'textfield': 'fa fa-pencil-square-o',
'dropdown': 'fa fa-th-list',
'date': 'fa fa-calendar',
@ -27,7 +27,6 @@ angular.module('view-form').directive('fieldIconDirective', function() {
'yes_no': 'fa fa-toggle-on',
'number': 'fa fa-slack'
};
$scope.typeIcon = iconTypeMap[$scope.typeName];
}
};
});

View file

@ -0,0 +1,58 @@
'use strict';
angular.module('forms').directive('shareFormDirective', ['$rootScope', '$translate', '$state',
function ($rootScope, $translate, $state) {
return {
templateUrl: 'modules/forms/admin/views/directiveViews/form/share-form.client.view.html',
restrict: 'E',
scope: {
actualformurl:'='
},
controller: function($scope){
$scope.actualFormURL = $scope.actualformurl;
$scope.isCopied = false;
$scope.onCopySuccess = function(){
$scope.isCopied = true;
}
$scope.embedCode = "<iframe id='iframe' src='" + $scope.actualFormURL + "' style='width:100%;height:500px;'></iframe>"+
"<div style='font-family: Sans-Serif;font-size: 12px;color: #999;opacity: 0.5; padding-top: 5px;'>"+
$translate.instant('POWERED_BY')+
"<a href='https://www.tellform.com' style='color: #999' target='_blank'>TellForm</a>"+
"</div>";
/* Tab Routing Logic */
$scope.shareTabs = [
{
heading: $translate.instant('SHARE_YOUR_FORM'),
route: 'viewForm.share.share_form',
active: false
},
{
heading: $translate.instant('EMBED_YOUR_FORM'),
route: 'viewForm.share.embed_form',
active: false
}
];
$scope.go = function(tab){
$scope.isCopied = false;
tab.active = true;
$state.go(tab.route);
};
function setActiveTab() {
$scope.shareTabs.forEach(function(tab) {
tab.active = ($state.current.name === tab.route);
});
}
setActiveTab();
$scope.$on("$stateChangeSuccess", setActiveTab());
}
};
}
]);

View file

@ -4,7 +4,7 @@
angular.module('forms').service('FormFields', [ '$rootScope', '$translate', 'Auth',
function($rootScope, $translate, Auth) {
var language = Auth.ensureHasCurrentUser().language;
var language = $rootScope.language;
$translate.use(language);
this.types = [
@ -30,7 +30,7 @@ angular.module('forms').service('FormFields', [ '$rootScope', '$translate', 'Aut
},
{
name : 'textarea',
value : $translate.instant('PARAGRAPH'),
value : $translate.instant('PARAGRAPH_FIELD'),
},
{
name : 'yes_no',

View file

@ -41,159 +41,27 @@
</div>
<div class="col-xs-1 col-sm-2">
<small class="pull-right">
<a class="btn btn-secondary view-form-btn" href="{{actualFormURL}}">
<span class="hidden-xs hidden-sm">
{{ 'VIEW' | translate }}
<span ng-show="myform.isLive">
{{ 'LIVE' | translate }}
</span>
<span ng-hide="myform.isLive">{{ 'PREVIEW' | translate }}</span>
<a class="btn btn-secondary view-form-btn" href="{{actualFormURL}}" target="_blank">
<i class="fa fa-external-link"></i>
<span>
{{ 'VIEW_MY_TELLFORM' | translate }}
</span>
<i class="status-light status-light-on fa fa-dot-circle-o" ng-if="myform.isLive"></i>
<i class="status-light status-light-off fa fa-dot-circle-o" ng-if="!myform.isLive"></i>
<i class="status-light status-light-on fa fa-circle" ng-if="myform.isLive"></i>
<i class="status-light status-light-off fa fa-circle" ng-if="!myform.isLive"></i>
</a>
</small>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<uib-tabset active="activePill" vertical="true" type="pills">
<uib-tab index="0" heading="{{ 'CREATE_TAB' | translate }}" select="deactivateDesignTab()">
<edit-form-directive myform="myform"></edit-form-directive>
</uib-tab>
<uib-tab ng-repeat="tab in tabData" index="{{$index+1}}" heading="{{tab.heading}}" select="deactivateDesignTab()">
<div class='row' data-ng-include="'/static/modules/forms/admin/views/adminTabs/'+tab.templateName+'.html'"></div>
</uib-tab>
<uib-tab index="2" heading="{{ 'ANALYZE_TAB' | translate }}" select="deactivateDesignTab()">
<edit-submissions-form-directive myform="myform" user="myform.admin"></edit-submissions-form-directive>
</uib-tab>
<uib-tab ng-if="tabData" heading="{{ 'SHARE_TAB' | translate }}" index="{{tabData.length}}" select="deactivateDesignTab()">
<div class="config-form">
<div class="row">
<div class="col-sm-12">
<uib-tabset active="activePill" vertical="true" type="pills">
<uib-tab index="0" heading="{{ 'SHARE_YOUR_FORM' | translate }}">
<div class="row">
<div class="col-sm-12">
{{ 'TELLFORM_URL' | translate }}
</div>
<div class="col-sm-8 form-input">
<span ngclipboard data-clipboard-target="#copyURL"> <input id="copyURL" ng-value="actualFormURL" class="form-control ng-pristine ng-untouched ng-valid"> </span>
</div>
<div class="col-sm-4">
<button class="btn btn btn-secondary view-form-btn" ngclipboard data-clipboard-target="#copyURL">
{{ 'COPY' | translate }} <i class="fa fa-clipboard" aria-hidden="true"></i>
</button>
</div>
</div>
</uib-tab>
<uib-tab index="1" heading="{{ 'EMBED_YOUR_FORM' | translate }}">
<div class="row">
<div class="col-sm-12">
{{ 'COPY_AND_PASTE' | translate }}
</div>
<div class="col-sm-8 form-input">
<span ngclipboard data-clipboard-target="#copyEmbedded">
<textarea id="copyEmbedded" class="form-control ng-pristine ng-untouched ng-valid" style="min-height:200px; width:100%; background-color: #FFFFCC; color: #30313F;">
&lt;!-- {{ 'CHANGE_WIDTH_AND_HEIGHT' | translate }} --&gt;
<iframe id="iframe" src="{{actualFormURL}}" style="width:100%;height:500px;"></iframe>
<div style="font-family: Sans-Serif;font-size: 12px;color: #999;opacity: 0.5; padding-top: 5px;">{{ 'POWERED_BY' | translate }} <a href="https://www.tellform.com" style="color: #999" target="_blank">TellForm</a></div>
</textarea>
</span>
</div>
<div class="col-sm-4">
<button class="btn btn btn-secondary view-form-btn" ngclipboard data-clipboard-target="#copyEmbedded">
{{ 'COPY' | translate }} <i class="fa fa-clipboard" aria-hidden="true"></i>
</button>
</div>
</div>
</uib-tab>
</uib-tabset>
</div>
</div>
</div>
</uib-tab>
<uib-tab class="design-tab" ng-if="tabData && myform.form_fields.length" heading="{{ 'DESIGN_TAB' | translate }}" index="{{tabData.length}}+1"
select="activateDesignTab()">
<div class="config-form design container">
<div class="row">
<div class="col-sm-4 col-xs-12">
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'BACKGROUND_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text"
ng-model="myform.design.colors.backgroundColor"
ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/"
ng-style="{ 'background-color': myform.design.colors.backgroundColor }"/>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'QUESTION_TEXT_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text" ng-model="myform.design.colors.questionColor" ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/"ng-style="{ 'background-color': myform.design.colors.questionColor }"/>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'ANSWER_TEXT_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text" ng-model="myform.design.colors.answerColor" ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/" ng-style="{ 'background-color': myform.design.colors.answerColor }"/>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'BTN_BACKGROUND_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text"
ng-model="myform.design.colors.buttonColor"
ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/"
ng-style="{ 'background-color': myform.design.colors.buttonColor }"/>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-5">
<h5>{{ 'BTN_TEXT_COLOR' | translate }}</h5>
</div>
<div class="field-input col-sm-6">
<input class="form-control" colorpicker="hex" type="text"
ng-model="myform.design.colors.buttonTextColor"
ng-pattern="/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/"
ng-style="{ 'background-color': myform.design.colors.buttonTextColor }"/>
</div>
</div>
</div>
<div class="col-sm-8 hidden-xs" ng-if="designTabActive">
<div class="public-form" ng-style="{ 'background-color': myform.design.colors.backgroundColor }">
<iframe id="iframe" ng-if="!!formURL" ng-src="{{formURL | trustSrc}}" style="border: none; box-shadow: 0px 0px 10px 0px grey; overflow: hidden; height: 400px; width: 90%; position: absolute;"></iframe>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-offset-4 col-sm-2">
<button class="btn btn-signup btn-rounded" type="button" ng-click="updateDesign(false, myform, false, false)"><i class="icon-arrow-left icon-white"></i>{{ 'SAVE_CHANGES' | translate }}</button>
</div>
<div class="col-sm-1">
<button class="btn btn-secondary btn-rounded" type="button" ng-click="resetForm()"><i class="icon-eye-open icon-white"></i>{{ 'CANCEL' | translate }}</button>
</div>
</div>
</div>
<div >
<uib-tabset vertical="true" type="pills">
<uib-tab ng-repeat="tab in tabData" active="tab.active" select="go(tab)" heading="{{tab.heading}}">
</uib-tab>
</uib-tabset>
</div>
<div class="col-xs-10">
<div ui-view></div>
</div>
</div>
</section>

View file

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

View file

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

View file

@ -0,0 +1,109 @@
<div class="row">
<div class="col-sm-offset-2 col-sm-10">
<div class="row field" ng-if="!formHasEmailField">
<strong>
{{ 'NO_EMAIL_FIELD_WARNING' | translate }}
</strong>
</div>
<div class="row field">
<div class="notification-row">
<i class="status-light status-light-on fa fa-circle" ng-if="myform.respondentNotifications.enabled"></i>
<i class="status-light status-light-off fa fa-circle" ng-if="!myform.respondentNotifications.enabled"></i>
</div>
<div class="notification-row">
<h4>{{ 'ENABLE_RESPONDENT_NOTIFICATIONS' | translate }}</h4>
</div>
<div class="notification-row">
<input bs-switch class="toggle-switch notification-toggle" type="checkbox"
switch-size="small"
ng-model="myform.respondentNotifications.enabled"
switch-on-text="{{ 'ON' | translate }}"
switch-off-text="{{ 'OFF' | translate }}"
switch-active="{{formHasEmailField}}">
</div>
</div>
<div class="row field" ng-if="formHasEmailField">
<div class="field-title col-sm-12">
<h4>{{ 'SEND_NOTIFICATION_TO' | translate }}</h4>
</div>
<div class="col-sm-12 field-input">
<ui-select ng-model="myform.respondentNotifications.toField" theme="selectize">
<ui-select-match placeholder="{{ 'OPTION_PLACEHOLDER' | translate }}">
{{$select.selected.title}}
</ui-select-match>
<ui-select-choices repeat="field.id as field in emailFields | filter: { title: $select.search }">
<span ng-bind-html="field.title | highlight: $select.search">
</span>
</ui-select-choices>
</ui-select>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h4>{{ 'REPLY_TO' | translate }}</h4>
</div>
<div class="col-sm-12 field-input">
<input class="form-control"
type="text"
ng-model="myform.respondentNotifications.replyEmail"
placeholder="noreply@tellform.com">
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h4>{{ 'EMAIL_SUBJECT' | translate }}</h4>
</div>
<div class="col-sm-12 field-input">
<ng-quill-editor modules="customModules"
ng-model="myform.respondentNotifications.subject">
<ng-quill-toolbar>
<div id="ng-quill-toolbar">
<span class="ql-formats">
<select class="ql-placeholder">
<option ng-repeat="placeholder in customModules.placeholder.placeholders" value="{{placeholder.id}}">
{{placeholder.label}}
</option>
</select>
</span>
</div>
</ng-quill-toolbar>
</ng-quill-editor>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h4>{{ 'EMAIL_MESSAGE' | translate }}</h4>
</div>
<div class="col-sm-12 field-input">
<ng-quill-editor modules="customModules"
ng-model="myform.respondentNotifications.htmlTemplate">
<ng-quill-toolbar>
<div id="ng-quill-toolbar">
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
</span>
<span class="ql-formats">
<select class="ql-placeholder">
<option ng-repeat="placeholder in customModules.placeholder.placeholders" value="{{placeholder.id}}">
{{placeholder.label}}
</option>
</select>
</span>
</div>
</ng-quill-toolbar>
</ng-quill-editor>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,103 @@
<div class="row">
<div class="col-sm-offset-2 col-sm-10">
<div class="row field">
<div class='notification-row'>
<i class="status-light status-light-on fa fa-circle" ng-if="myform.selfNotifications.enabled"></i>
<i class="status-light status-light-off fa fa-circle" ng-if="!myform.selfNotifications.enabled"></i>
</div>
<div class='notification-row'>
<h4>{{ 'ENABLE_SELF_NOTIFICATIONS' | translate }}</h4>
</div>
<div class='notification-row'>
<input class="toggle-switch notification-toggle" type="checkbox" switch-size="small"
bs-switch ng-model="myform.selfNotifications.enabled"
switch-on-text="{{ 'ON' | translate }}"
switch-off-text="{{ 'OFF' | translate }}">
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h4>{{ 'SEND_NOTIFICATION_TO' | translate }}</h4>
</div>
<div class="col-sm-12 field-input">
<input class="form-control"
type="text"
ng-model="myform.selfNotifications.toEmails"
placeholder="email@domain.com,email2@domain2.com,etc">
</div>
</div>
<div class="row field" ng-if="formHasEmailField">
<div class="field-title col-sm-12">
<h4>{{ 'REPLY_TO' | translate }}</h4>
</div>
<div class="col-sm-12 ui-select field-input">
<ui-select ng-model="myform.selfNotifications.fromField" theme="selectize">
<ui-select-match placeholder="{{ 'OPTION_PLACEHOLDER' | translate }}">
{{$select.selected.title}}
</ui-select-match>
<ui-select-choices repeat="field.id as field in emailFields | filter: { title: $select.search }">
<span ng-bind-html="field.title | highlight: $select.search">
</span>
</ui-select-choices>
</ui-select>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h4>{{ 'EMAIL_SUBJECT' | translate }}</h4>
</div>
<div class="col-sm-12 field-input">
<ng-quill-editor modules="customModules"
ng-model="myform.selfNotifications.subject">
<ng-quill-toolbar>
<div id="ng-quill-toolbar">
<span class="ql-formats">
<select class="ql-placeholder">
<option ng-repeat="placeholder in customModules.placeholder.placeholders" value="{{placeholder.id}}">
{{placeholder.label}}
</option>
</select>
</span>
</div>
</ng-quill-toolbar>
</ng-quill-editor>
</div>
</div>
<div class="row field">
<div class="field-title col-sm-12">
<h4>{{ 'EMAIL_MESSAGE' | translate }}</h4>
</div>
<div class="col-sm-12 field-input">
<ng-quill-editor modules="customModules"
ng-model="myform.selfNotifications.htmlTemplate">
<ng-quill-toolbar>
<div id="ng-quill-toolbar">
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
</span>
<span class="ql-formats">
<select class="ql-placeholder">
<option ng-repeat="placeholder in customModules.placeholder.placeholders" value="{{placeholder.id}}">
{{placeholder.label}}
</option>
</select>
</span>
</div>
</ng-quill-toolbar>
</ng-quill-editor>
</div>
</div>
</div>
</div>

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more