From 81b8d0dd8835e88a2449a2b360e19eecaa2335cc Mon Sep 17 00:00:00 2001 From: Eduardo Robles Elvira Date: Thu, 24 Jan 2019 10:55:39 +0100 Subject: [PATCH] Go next merge into next (#188) * fix widget scope, add widgets to elbasic * remove old form * add for to labels * fix tooltips, add default help url * fix help tooltip * change version to go v3 * making textarea admin field a textarea * Move admin fields to the end of the list of actions when creating an election * moving admin fields also in the .sidebarlinks var * more sane defaults when creating an election * update logo * :update logo size * making logo bigger * making logo wider * updating election default logo url * reverting last commit * remove import from admin sidebar * set correctly the paths of documentation urls for widgets that include external doc * commit tests to resume development later * use help list from config * fix css * fix help menu css when not logged * fix signup link * check req when reg fields * some fixes, add admin profile modal sketch * syntax fix * add dependency * fixes, fill profile modal, update profile on ok click * syntax fix * fixes, css fixes, refactor, head profile menu click working * add dependency to admin-head-directive * some moarr fixes * CSS fixes * change profile menu icon * profile save button locales * refactor: user editable * starting to add configurable admin gui questions options * do not show empty layouts list * set grunt-merge-json to 0.9.5 because last version gives problems with chalk (https://github.com/eirslett/frontend-maven-plugin/issues/629) * fix empty string * small fix * change version to go v4 * check when profile modal is closed * get template el from avConfig.js * fix el template * add default name to census extra fields * refactor * refactor, add service to index.html, add profile to scope on templateel * fix scope error, fix open modal error * add comments * add some abstract settings to question directive * more abstract settings * add abstract settings to auth config * remove well class on auth config * testing census reg with abs settings * changes to census config * go v5 * fix css * fix loop * fix css * refactor css * fix loop * fix * fix logo css * adding glyphicon * changes to abstract settings * fixes * fix css * add short value * refactor abstract settings, add fields * fixes * refactor scope on abstract settings * use ng-show * observe attrs on abstract settings * fix css, disabled * remove click glitch * fix columns for extra small screens * more css changes * remove isDisabled * bower * fix intl-tel deps * add number input directive * fixes * remove disabled ef, refactor must ef service, call must ef service on elections change on create el * fix syntax * fix extra-fields and census on auth method change * small fix * add attrs to abstract settings widget * moar less * fix css * fix hover * fixes mouseover * fix syntax * hover fixes * fix css * fix collapse css * Onboarding (#128) * starting to add ngOnboarding * initial onboarding test * trying hopscotch instead * changing vendor files * remove unnecesary files * make jshint ignore hopscotch * remove i18next dependency in avAdminElections directive * adding vendor css * trying to add vendor css * solving css problems * fixing a typo * trying to fix css * add missing option to grunt * using abs paths * trying to not use minimized vendor hopscotch files * fix hopscotch css * adding jquery integration for state changes * making easier to find through paths dashboard actions * making easier to find through paths dashboard actions 2 * adding onboarding tour * hopscotch ignore * moving start election to the end * improving onboarding tour service * make it compile * missing semicolon * removing jslint ignore * fix nextTour * fix help tour * adding OnboardingTourService to head scope so that it can be shown in helpList * testing onboarding translations * fix typo * adding onboarding tour i18n * launch wizard on first time too * fix typo * change version to go v6 * add ngOnboarding dep to bower * Fix onboarding (#131) * change version to go v6 * remove unneeded dep * add hopscotch dep, remove ngOnboarding * remove hopscotch * Update bower.json * Update bower.json * Fix abstract settings help background color (#136) * fix some colors in abstract settings help * fix again css * making abstract-settings help links distinguishable * adding margin to images in abstract settings help * remove reference to missing video and adding onboarding i18n for ca and gl in es lang (#138) * improve abstract settings doc css (#140) * improve abstract settings doc css * fix css background-color * Fix onboarding 2 (#141) * fixing onboarding * fixing onboarding * trying to add intro video * adding width to the oyutubevideo bubble * improving video looks * Fix onboarding 2 (#142) * improve abstract settings doc css (#140) * improve abstract settings doc css * fix css background-color * fixing onboarding * fixing onboarding * trying to add intro video * adding width to the oyutubevideo bubble * improving video looks * only end tour if it has started * show admin field names on error * refactor * fixes * refactor * fixes * fix el checks before el creation * syntax fix: remove extra commas * fix locale * required * moar * fixes * set admin field border to red on error * set label size to col-sm4 * test 5/7 * refactor code into csv load service * fixes * test * solve dependency issue and other minor things * refactor * fixes * fixes * test * fixes * .error -> .catch * fixes * minor fixes * fixes * add comments * fix adding new question * change version to go v7 * test using * Revert "fix dashboard results presentation (order of winners etc)" * add 2 default options to new questions * Census premium (#157) add census csv error plugin call * Census err (#159) * fix csv error on creation * show add person to census error * Save draft (#158) * test * add draft election directive * fixes * add fix * fix * fix * moar * refactor * fixes * fix * add use draft modal * fixes * fixes * fix * fix css * add erase modal * fix locale * change version to go v8 * remove test/real difference * moar * refactor onboarding call * add dep to admin-head-directive * fix onboarding * refactor * fix * fix urls thingie * refactor * fix * add to admin controller deps * refactor * fix * fixes * css fix * hide features in login and signup * fix css * remove phantomjs (#172) * fix draft election failed creation (#175) * census: Add action buttons for each voter (#174) This patch adds actions buttons for each row in the census table. Buttons added are Activate, Deactivate, Remove and Send auth codes. These buttons select the row and then call the same action as actions dropdown, all other rows are deselected so this should work similar to select only one row and click in the actions dropdown. Buttons are hidden by default and showed on hover the row. I've add translations for these actions but not for gl and ca. * Activity log (#173) * initial work on activity log * fix some compilation errors * fixing name of the directive * fixing activity log html * fix concats of admin controller * adding missing state activityLog * adding missing allowed state * fixing loading * fixing loading of the activity list * adding i18n to activity log * adding missing i18n and removing append already done in previous line * trying to add some filtering * trying to fix build * fix call to activity * fix call to activity 2 * adding more columns filtering * fix some visualization * fix typo * registering more actions * fixing comma in json * show added-to-census activity log item * make reload activity work * allow query parameters to automatically set the initial filter * fixing location search getter * parsing value in get location var * set var was missing * census: Comment for activate/deactivate actions (#176) * census: Comment for activate/deactivate actions This patch allows comments for activate and deactivate actions. A textarea is available in the activate action and the content of that will go as comment. To show the comment in the activity log I've to change a little the code because the angular template always resolve the {{obj.metadata.comment}} as undefined, I don't know why, so I've added other attr to obj called metadatacomment. * Fix deactivate comment problem * census: Autofill field param (#177) * date extra field type (#179) * Tally sheets (#181) * initial work in tally sheets * adding ballot box sidebar link * adding ballot box directive to index * adding en i18n for ballotboxes * adding missing argument to av-admin-ballot-box * allowing filtering by last update in ballot box * fixing column header * Add actions to ballot box listing * Removing select ballot box * remove unneeded comma * updating received objects from ballot boxes list * improve ballot box list styles * improve ballot box list styles 2 * adding ballot box less * improving bb less * improving ballot box list looks * improving ballot box list looks * improving ballot box list looks * starting to work in ballot box modals * make it compile * make it compile 2 * make it compile 3 * try fixing modal * trying to fix create ballot boxes modal * trying to fix create bbs modal * fix build * fix modal * fix modal * fix modal * improve modal * fix typos * fix typos * standarize mouse over on census and activity log as in ballot box tables * adding initial delete ballot box dialog * rename modal * improve deleteBallotBox * fix deleteBallotBox * initial work on writting tally sheet form * use i18next * improving layout * improving layout * improving layout * making dialog bigger * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * fixing modelline * adding number checking * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * remove timeout * remove timeout * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * working on WriteBallotBoxModal * allowing to delete a tally sheet * remove not existant file * num tally sheets filtering * num tally sheets filtering * improving activity log * improving activity log * improving activity log * improving activity log * improving activity log * improving activity log * improving activity log * improving activity log * improving activity log * fixing operator precedence * add has_ballot_boxes when creating election * Tally sheets2 (#183) * adding permissions checking, only showing what you can do with ballot boxes * fixing compilation * fixing compilation * adding has_ballot_boxes * fix index of * try deferred load for ballot box sidebar link * try deferred load for ballot box sidebar link * try deferred load for ballot box sidebar link * try deferred load for ballot box sidebar link * fix activity log ballotbox create * trying to allow non-json to calculate results, and loading by default election configresults if any * trying to allow non-json to calculate results, and loading by default election configresults if any * fix totals (#184) * allow uploading with results_ok (#185) * Add 'user-and-password' as authentication method (#180) * Add 'user-and-password' as authentication method * user-and-password authentication requires username and password fields * allow census query (#186) * fixing sms-otp (#187) --- .editorconfig | 2 +- Gruntfile.js | 131 +- app.js | 32 +- app.less | 5 + .../admin-controller/admin-controller.html | 12 +- avAdmin/admin-controller/admin-controller.js | 178 +- .../abstract-setting/abstract-setting.html | 91 + .../abstract-setting/abstract-setting.js | 126 + .../abstract-setting/abstract-setting.less | 156 + .../activity-log/activity-log.html | 236 ++ .../activity-log/activity-log.js | 157 ++ .../activity-log/activity-log.less | 26 + .../admin-field/admin-field.html | 203 ++ .../admin-field/admin-field.js | 83 + .../admin-field/admin-field.less | 9 + .../admin-fields/admin-fields.html | 19 + .../admin-fields/admin-fields.js | 26 + .../ballot-box/ballot-box.html | 175 ++ .../admin-directives/ballot-box/ballot-box.js | 442 +++ .../ballot-box/ballot-box.less | 79 + .../ballot-box/checking-ballot-box-modal.html | 61 + .../ballot-box/checking-ballot-box-modal.js | 95 + .../ballot-box/create-ballot-box-modal.html | 34 + .../ballot-box/create-ballot-box-modal.js | 33 + .../ballot-box/delete-ballot-box-modal.html | 38 + .../ballot-box/delete-ballot-box-modal.js | 54 + .../ballot-box/delete-tally-sheet-modal.html | 41 + .../ballot-box/delete-tally-sheet-modal.js | 57 + .../ballot-box/view-tally-sheet-modal.html | 167 ++ .../ballot-box/view-tally-sheet-modal.js | 46 + .../ballot-box/write-tally-sheet-modal.html | 371 +++ .../ballot-box/write-tally-sheet-modal.js | 166 ++ .../census-field/census-field.html | 3 + .../column-filters/int/column-filter-int.js | 23 +- .../column-filters/int/column-filter-int.less | 4 +- avAdmin/admin-directives/create/create.js | 551 +++- .../admin-directives/dashboard/dashboard.html | 45 +- .../admin-directives/dashboard/dashboard.js | 131 +- .../send-auth-codes-modal-confirm.html | 8 +- .../send-auth-codes-modal-confirm.js | 24 +- .../send-auth-codes-modal-confirm.less | 1 + .../dashboard/send-auth-codes-modal.html | 12 +- avAdmin/admin-directives/elauth/elauth.html | 100 +- avAdmin/admin-directives/elauth/elauth.js | 60 +- avAdmin/admin-directives/elbasic/elbasic.html | 297 +- avAdmin/admin-directives/elbasic/elbasic.js | 17 +- .../elcensus-config/elcensus-config.html | 30 +- .../elcensus-config/elcensus-config.js | 17 +- .../elcensus-config/elcensus-config.less | 10 +- .../elcensus/add-csv-modal.html | 5 + .../elcensus/add-csv-modal.js | 8 +- .../elcensus/add-person-modal.html | 5 + .../confirm-activate-people-modal.html | 8 +- .../elcensus/confirm-activate-people-modal.js | 3 +- .../confirm-deactivate-people-modal.html | 8 +- .../confirm-deactivate-people-modal.js | 3 +- .../elcensus/csv-loading-modal.html | 62 + .../elcensus/csv-loading-modal.js | 41 + .../elcensus/csv-loading-modal.less | 29 + .../admin-directives/elcensus/elcensus.html | 23 +- avAdmin/admin-directives/elcensus/elcensus.js | 164 +- .../admin-directives/elcensus/elcensus.less | 22 +- .../admin-directives/elections/elections.html | 29 +- .../admin-directives/elections/elections.js | 128 +- .../admin-directives/elections/elections.less | 38 + .../elections/erase-draft-modal.html | 25 + .../elections/erase-draft-modal.js | 14 + .../elections/use-draft-modal.html | 25 + .../elections/use-draft-modal.js | 14 + .../elquestions/elquestions.html | 2 +- .../elquestions/elquestions.js | 9 +- .../extra-field/extra-field.html | 41 +- .../extra-field/extra-field.js | 2 - avAdmin/admin-directives/import/import.js | 10 +- .../number-input/number-input.js | 38 + .../question-option-details.js | 11 +- .../admin-directives/question/question.html | 217 +- avAdmin/admin-directives/question/question.js | 12 +- .../admin-directives/question/question.less | 22 + .../success-action/success-action.html | 2 +- .../success-action/success-action.js | 5 +- .../admin-head-directive.html | 49 +- .../admin-head-directive.js | 23 +- .../admin-head-directive.less | 65 +- .../admin-login-controller.html | 7 +- .../admin-login-controller.js | 2 + avAdmin/admin-profile-service.js | 181 ++ avAdmin/admin-profile/admin-profile.html | 26 + avAdmin/admin-profile/admin-profile.js | 99 + .../admin-sidebar-directive.html | 62 +- .../admin-sidebar-directive.js | 9 +- .../admin-sidebar-directive.less | 4 + avAdmin/csv-load-service.js | 269 ++ avAdmin/draft-election.js | 119 + avAdmin/elections-api-service.js | 108 +- .../int-field-directive.js | 42 + avAdmin/must-extra-fields-service.js | 86 +- avAdmin/next-button-service.js | 33 + avAdmin/onboarding-tour-service.js | 221 ++ avAdmin/send-messages-service.js | 13 +- bower.json | 7 +- img/nVotes_logo_small.png | Bin 3247 -> 3617 bytes index.html | 47 +- locales/ca.json | 70 +- locales/en.json | 232 +- locales/es.json | 118 +- locales/gl.json | 67 +- package.json | 5 +- vendor/hopscotch-0.3.1/LICENSE | 177 ++ vendor/hopscotch-0.3.1/css/hopscotch.css | 519 ++++ vendor/hopscotch-0.3.1/img/sprite-green.png | Bin 0 -> 5405 bytes vendor/hopscotch-0.3.1/img/sprite-orange.png | Bin 0 -> 5374 bytes vendor/hopscotch-0.3.1/js/hopscotch.js | 2511 +++++++++++++++++ 113 files changed, 10121 insertions(+), 759 deletions(-) create mode 100644 avAdmin/admin-directives/abstract-setting/abstract-setting.html create mode 100644 avAdmin/admin-directives/abstract-setting/abstract-setting.js create mode 100644 avAdmin/admin-directives/abstract-setting/abstract-setting.less create mode 100644 avAdmin/admin-directives/activity-log/activity-log.html create mode 100644 avAdmin/admin-directives/activity-log/activity-log.js create mode 100644 avAdmin/admin-directives/activity-log/activity-log.less create mode 100644 avAdmin/admin-directives/admin-field/admin-field.html create mode 100644 avAdmin/admin-directives/admin-field/admin-field.js create mode 100644 avAdmin/admin-directives/admin-field/admin-field.less create mode 100644 avAdmin/admin-directives/admin-fields/admin-fields.html create mode 100644 avAdmin/admin-directives/admin-fields/admin-fields.js create mode 100644 avAdmin/admin-directives/ballot-box/ballot-box.html create mode 100644 avAdmin/admin-directives/ballot-box/ballot-box.js create mode 100644 avAdmin/admin-directives/ballot-box/ballot-box.less create mode 100644 avAdmin/admin-directives/ballot-box/checking-ballot-box-modal.html create mode 100644 avAdmin/admin-directives/ballot-box/checking-ballot-box-modal.js create mode 100644 avAdmin/admin-directives/ballot-box/create-ballot-box-modal.html create mode 100644 avAdmin/admin-directives/ballot-box/create-ballot-box-modal.js create mode 100644 avAdmin/admin-directives/ballot-box/delete-ballot-box-modal.html create mode 100644 avAdmin/admin-directives/ballot-box/delete-ballot-box-modal.js create mode 100644 avAdmin/admin-directives/ballot-box/delete-tally-sheet-modal.html create mode 100644 avAdmin/admin-directives/ballot-box/delete-tally-sheet-modal.js create mode 100644 avAdmin/admin-directives/ballot-box/view-tally-sheet-modal.html create mode 100644 avAdmin/admin-directives/ballot-box/view-tally-sheet-modal.js create mode 100644 avAdmin/admin-directives/ballot-box/write-tally-sheet-modal.html create mode 100644 avAdmin/admin-directives/ballot-box/write-tally-sheet-modal.js create mode 100644 avAdmin/admin-directives/elcensus/csv-loading-modal.html create mode 100644 avAdmin/admin-directives/elcensus/csv-loading-modal.js create mode 100644 avAdmin/admin-directives/elcensus/csv-loading-modal.less create mode 100644 avAdmin/admin-directives/elections/erase-draft-modal.html create mode 100644 avAdmin/admin-directives/elections/erase-draft-modal.js create mode 100644 avAdmin/admin-directives/elections/use-draft-modal.html create mode 100644 avAdmin/admin-directives/elections/use-draft-modal.js create mode 100644 avAdmin/admin-directives/number-input/number-input.js create mode 100644 avAdmin/admin-profile-service.js create mode 100644 avAdmin/admin-profile/admin-profile.html create mode 100644 avAdmin/admin-profile/admin-profile.js create mode 100644 avAdmin/csv-load-service.js create mode 100644 avAdmin/draft-election.js create mode 100644 avAdmin/int-field-directive/int-field-directive.js create mode 100644 avAdmin/next-button-service.js create mode 100644 avAdmin/onboarding-tour-service.js create mode 100644 vendor/hopscotch-0.3.1/LICENSE create mode 100644 vendor/hopscotch-0.3.1/css/hopscotch.css create mode 100644 vendor/hopscotch-0.3.1/img/sprite-green.png create mode 100644 vendor/hopscotch-0.3.1/img/sprite-orange.png create mode 100644 vendor/hopscotch-0.3.1/js/hopscotch.js diff --git a/.editorconfig b/.editorconfig index 0ea0cc46..d1d8a417 100755 --- a/.editorconfig +++ b/.editorconfig @@ -3,7 +3,7 @@ root = true [*] indent_style = space -indent_size = 4 +indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true diff --git a/Gruntfile.js b/Gruntfile.js index 66ce742f..8a93b8ff 100755 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -19,7 +19,7 @@ 'use strict'; var pkg = require('./package.json'); -var AV_CONFIG_VERSION = '17.04'; +var AV_CONFIG_VERSION = '103111.8'; //Using exclusion patterns slows down Grunt significantly //instead of creating a set of patterns like '**/*.js' and '!**/node_modules/**' @@ -79,6 +79,64 @@ module.exports = function (grunt) { }); }); + // custom grunt task to check avPluginsConfig.js + grunt.registerTask('check_plugins_config', function() { + var fs = require('fs'); + var done = this.async(); + grunt.log.ok('Checking avPluginsConfig.js...'); + function checkAvPluginsConfig() { + fs.readFile('avPluginsConfig.js', function(err, data) { + if (err) { + grunt.log.ok('No avPluginsConfig.js file found, creating...'); + var avPluginsConfigText = + "var AV_PLUGINS_CONFIG_VERSION = '" + AV_CONFIG_VERSION + "';\n" + + "angular.module('avPluginsConfig', [])\n" + + " .factory('PluginsConfigService', function() {\n" + + " return {};\n" + + " });\n" + + "\n" + + "angular.module('avPluginsConfig')\n" + + " .provider('PluginsConfigService', function PluginsConfigServiceProvider() {\n" + + " _.extend(this, {});\n" + + "\n" + + " this.$get = [function PluginsConfigServiceProviderFactory() {\n" + + " return new PluginsConfigServiceProvider();\n" + + " }];\n" + + " });"; + fs.writeFile("avPluginsConfig.js", + avPluginsConfigText, + function(err) { + if(err) { + grunt.log.error( + 'Error creating avPluginsConfig.js file'); + done(false); + } else { + grunt.log.ok('Created avPluginsConfig.js file, ' + + 'trying to read it again...'); + checkAvPluginsConfig(); + } + }); + } else { + var match = data.toString().match( + /AV_PLUGINS_CONFIG_VERSION = [\'\"]([\w\.]*)[\'\"];/); + if (!match) { + grunt.log.error('Invalid avPluginsConfig.js version'); + } else { + var v = match[1]; + if (v === AV_CONFIG_VERSION) { + return done(); + } else { + grunt.log.error('Invalid avPluginsConfig.js version: ' + + v); + } + } + done(false); + } + }); + } + var conf = checkAvPluginsConfig(); + }); + // Project configuration. grunt.initConfig({ variables: { @@ -106,7 +164,11 @@ module.exports = function (grunt) { main: { options: { jshintrc: '.jshintrc', - reporter: require('jshint-stylish') + reporter: require('jshint-stylish'), + ignores: [ + 'vendor/hopscotch-0.3.1/js/hopscotch.js' + ] + }, src: createFolderGlobs('*.js') } @@ -162,8 +224,15 @@ module.exports = function (grunt) { main: { files: [ {src: ['img/**'], dest: 'dist/'}, + { + expand: true, + cwd:'vendor/hopscotch-0.3.1/img/', + src: ['**'], + dest: 'dist/img/' + }, {src: ['img/**'], dest: 'dist/'}, {src: ['temp_data/**'], dest: 'dist/'}, + {src: ['bower_components/avCommon/dist/img/flags.png'], dest: 'dist/img/flags.png'}, { expand: true, cwd:'bower_components/avCommon/themes', @@ -213,15 +282,16 @@ module.exports = function (grunt) { remove: ['script[data-remove!="false"]','link[data-remove!="false"]'], append: [ {selector:'body',html:'<%= variables.admin_html_body_include %>'}, - {selector:'body',html:''}, + {selector:'body',html:''}, {selector:'body',html:''}, - {selector:'body',html:''}, - {selector:'body',html:''}, - {selector:'body',html:''}, - {selector:'body',html:''}, - {selector:'body',html:''}, + {selector:'body',html:''}, + {selector:'body',html:''}, + {selector:'body',html:''}, + {selector:'body',html:''}, + {selector:'body',html:''}, {selector:'head',html:''}, - {selector:'head',html:''} + {selector:'head',html:''}, + {selector:'head',html:''} ] }, src:'index.html', @@ -230,20 +300,29 @@ module.exports = function (grunt) { }, cssmin: { main: { - files: [{ - expand: true, - cwd:'temp/bower_components/avCommon/themes', - src: ['**/app.css'], - dest: 'dist/themes/', - ext: '.min.css', - extDot: 'first' - }] + files: [ + { + expand: true, + cwd:'temp/bower_components/avCommon/themes', + src: ['**/app.css'], + dest: 'dist/themes/', + ext: '.min.css', + extDot: 'first' + }, + { + src: ['vendor/hopscotch-0.3.1/css/hopscotch.css'], + dest: 'dist/vendor.min.css' + } + ] }, }, concat: { main: { files: { 'dist/plugins.css': ['temp/plugins/**/*.css'], + 'dist/vendor.css': [ + 'vendor/hopscotch-0.3.1/css/hopscotch.css' + ], 'temp/libcompat.js': [ 'vendor/jquery.compat/jquery-1.11.1.js', 'vendor/json3/json-v3.3.2.js', @@ -252,9 +331,10 @@ module.exports = function (grunt) { 'temp/libnocompat.js': ['<%= dom_munger.data.libnocompatjs %>'], 'temp/lib.js': ['<%= dom_munger.data.libjs %>'], 'temp/app.js': ['<%= dom_munger.data.appjs %>','<%= ngtemplates.main.dest %>','<%= ngtemplates.common.dest %>'], - 'dist/avConfig-v17.04.js': ['avConfig.js'], - 'dist/avThemes-v17.04.js': ['bower_components/avCommon/dist/avThemes-v17.04.js'], - 'dist/avPlugins-v17.04.js': [ + 'dist/avConfig-v103111.8.js': ['avConfig.js'], + 'dist/avThemes-v103111.8.js': ['bower_components/avCommon/dist/avThemes-v103111.8.js'], + 'dist/avPlugins-v103111.8.js': [ + 'avPluginsConfig.js', 'plugins/**/*.js', '!plugins/**/*-spec.js' ] @@ -289,10 +369,10 @@ module.exports = function (grunt) { beautify: true }, files: { - 'dist/app-v17.04.min.js': 'temp/app.js', - 'dist/lib-v17.04.min.js': 'temp/lib.js', - 'dist/libnocompat-v17.04.min.js': 'temp/libnocompat.js', - 'dist/libcompat-v17.04.min.js': 'temp/libcompat.js', + 'dist/app-v103111.8.min.js': 'temp/app.js', + 'dist/lib-v103111.8.min.js': 'temp/lib.js', + 'dist/libnocompat-v103111.8.min.js': 'temp/libnocompat.js', + 'dist/libcompat-v103111.8.min.js': 'temp/libcompat.js', 'dist/avWidgets.min.js': 'avWidgets.js', "dist/locales/moment/en.js": "bower_components/moment/lang/en-gb.js", @@ -325,6 +405,7 @@ module.exports = function (grunt) { '<%= dom_munger.data.libnocompatjs %>', '<%= dom_munger.data.libjs %>', 'avConfig.js', + 'avPluginsConfig.js', 'avThemes.js', 'avWidgets.js', '<%= dom_munger.data.appjs %>', @@ -377,6 +458,7 @@ module.exports = function (grunt) { 'build', [ 'check_config', + 'check_plugins_config', 'jshint', 'clean:before', 'less', @@ -422,6 +504,7 @@ module.exports = function (grunt) { files.concat(grunt.config('dom_munger.data.libjs')); files.push('bower_components/angular-mocks/angular-mocks.js'); files.push('avConfig.js'); + files.push('avPluginsConfig.js'); files.push('avThemes.js'); files.push('avWidgets.js'); files.concat(grunt.config('dom_munger.data.appjs')); diff --git a/app.js b/app.js index f087eb94..37c33809 100755 --- a/app.js +++ b/app.js @@ -30,6 +30,7 @@ angular.module( 'infinite-scroll', 'angularMoment', 'avConfig', + 'avPluginsConfig', 'jm.i18next', 'avUi', 'avRegistration', @@ -97,6 +98,11 @@ angular.module('agora-gui-admin').config( templateUrl: 'avAdmin/admin-login-controller/admin-login-controller.html', controller: "AdminLoginController" }) + .state('admin.login_email', { + url: '/login/:email', + templateUrl: 'avAdmin/admin-login-controller/admin-login-controller.html', + controller: "AdminLoginController" + }) .state('admin.signup', { url: '/signup', templateUrl: 'avAdmin/admin-signup-controller/admin-signup-controller.html', @@ -108,7 +114,7 @@ angular.module('agora-gui-admin').config( }) // admin directives using the admin controller .state('admin.new', { - url: '/new', + url: '/new/:draft', templateUrl: 'avAdmin/admin-controller/admin-controller.html', controller: 'AdminController' }) @@ -127,6 +133,11 @@ angular.module('agora-gui-admin').config( templateUrl: 'avAdmin/admin-controller/admin-controller.html', controller: 'AdminController' }) + .state('admin.adminFields', { + url: '/admin-fields/:id', + templateUrl: 'avAdmin/admin-controller/admin-controller.html', + controller: 'AdminController' + }) .state('admin.questions', { url: '/questions/:id', templateUrl: 'avAdmin/admin-controller/admin-controller.html', @@ -152,6 +163,11 @@ angular.module('agora-gui-admin').config( templateUrl: 'avAdmin/admin-controller/admin-controller.html', controller: 'AdminController' }) + .state('admin.activityLog', { + url: '/activity/:id', + templateUrl: 'avAdmin/admin-controller/admin-controller.html', + controller: 'AdminController' + }) .state('admin.tally', { url: '/tally/:id', templateUrl: 'avAdmin/admin-controller/admin-controller.html', @@ -177,6 +193,11 @@ angular.module('agora-gui-admin').config( templateUrl: 'avAdmin/admin-controller/admin-controller.html', controller: 'AdminController' }) + .state('admin.ballotBox', { + url: '/ballot-box/:id', + templateUrl: 'avAdmin/admin-controller/admin-controller.html', + controller: 'AdminController' + }) .state('admin.create', { url: '/create/:autocreate', templateUrl: 'avAdmin/admin-controller/admin-controller.html', @@ -229,9 +250,11 @@ angular.module('agora-gui-admin').config( /** * IF the cookie is there we make the autologin */ -angular.module('agora-gui-admin').run(function($cookies, $http, Authmethod) { - if ($cookies.auth) { - Authmethod.setAuth($cookies.auth, $cookies.isAdmin); +angular.module('agora-gui-admin').run(function($cookies, $http, Authmethod, ConfigService) { + var adminId = ConfigService.freeAuthId + ''; + var postfix = "_authevent_" + adminId; + if ($cookies["auth" + postfix]) { + Authmethod.setAuth($cookies["auth" + postfix], $cookies["isAdmin" + postfix], adminId); } }); @@ -258,6 +281,7 @@ angular.module('agora-gui-admin').run(function($http, $rootScope, ConfigService) function(event, toState, toParams, fromState, fromParams) { console.log("change success"); $("#angular-preloading").hide(); + $(window).trigger("angular-state-change-success", [event, toState, toParams, fromState, fromParams]); }); }); diff --git a/app.less b/app.less index 44fd9a39..f1386eda 100755 --- a/app.less +++ b/app.less @@ -20,6 +20,10 @@ @import "avAdmin/admin-sidebar-directive/admin-sidebar-directive.less"; @import "avAdmin/admin-head-directive/admin-head-directive.less"; /* admin directives */ +@import "avAdmin/admin-directives/abstract-setting/abstract-setting.less"; +@import "avAdmin/admin-directives/activity-log/activity-log.less"; +@import "avAdmin/admin-directives/admin-field/admin-field.less"; +@import "avAdmin/admin-directives/ballot-box/ballot-box.less"; @import "avAdmin/admin-directives/elections/elections.less"; @import "avAdmin/admin-directives/extra-field/extra-field.less"; @import "avAdmin/admin-directives/create/create.less"; @@ -33,6 +37,7 @@ @import "avAdmin/admin-directives/elauth/elauth.less"; @import "avAdmin/admin-directives/elquestions/elquestions.less"; @import "avAdmin/admin-directives/elcensus/elcensus.less"; +@import "avAdmin/admin-directives/elcensus/csv-loading-modal.less"; @import "avAdmin/admin-directives/elcensus-config/elcensus-config.less"; @import "avAdmin/admin-directives/success-action/success-action.less"; @import "avAdmin/admin-directives/import/import.less"; diff --git a/avAdmin/admin-controller/admin-controller.html b/avAdmin/admin-controller/admin-controller.html index fb0e0a7f..bdb56030 100644 --- a/avAdmin/admin-controller/admin-controller.html +++ b/avAdmin/admin-controller/admin-controller.html @@ -12,18 +12,12 @@ id="content" class="col-sm-9 admin-controller-content"> -
-

- - - - -

-
-
+
+
+
diff --git a/avAdmin/admin-controller/admin-controller.js b/avAdmin/admin-controller/admin-controller.js index f0726df0..a76db144 100644 --- a/avAdmin/admin-controller/admin-controller.js +++ b/avAdmin/admin-controller/admin-controller.js @@ -16,8 +16,9 @@ **/ angular.module('avAdmin').controller('AdminController', - function(Plugins, ConfigService, $scope, $i18next, $state, $stateParams, ElectionsApi, $compile) { + function(Plugins, ConfigService, $scope, $i18next, $state, $stateParams, $timeout, $modal, ElectionsApi, DraftElection, $compile, NextButtonService, $q) { var id = $stateParams.id; + $scope.electionId = id; $scope.state = $state.current.name; $scope.current = null; $scope.noplugin = true; @@ -46,42 +47,106 @@ angular.module('avAdmin').controller('AdminController', $scope.current = el; ElectionsApi.setCurrent(el); ElectionsApi.newElection = true; - return el; } + $scope.hasAdminFields = false; + var next_states = ['admin.dashboard']; + + function updateHasAdminFields() { + $scope.hasAdminFields = false; + if (_.isObject($scope.current) && + _.isObject($scope.current.census) && + _.isArray($scope.current.census.admin_fields) && + 0 < $scope.current.census.admin_fields.length) { + $scope.hasAdminFields = true; + } + } + + function updateStates() { + updateHasAdminFields(); + if (!!$scope.hasAdminFields && -1 === next_states.indexOf('admin.adminFields')) { + var index = next_states.indexOf('admin.basic') + 1; + next_states.splice(index, 0, 'admin.adminFields'); + } + } + + function loadDraft() { + var deferred = $q.defer(); + DraftElection.getDraft() + .then(function (el) { + $scope.current = el; + ElectionsApi.setCurrent(el); + ElectionsApi.newElection = true; + deferred.resolve(el); + }, + deferred.reject); + return deferred.promise; + } + if (id) { ElectionsApi.getElection(id) .then(function(el) { $scope.current = el; ElectionsApi.setCurrent(el); - if ('real' in el) { - $scope.isTest = !el.real; - } else { - $scope.isTest = true; - } + updateStates(); + NextButtonService.setStates(next_states); }); } + function goToBasic() { + updateHasAdminFields(); + $state.go("admin.basic"); + } + if ($scope.state === 'admin.new') { - // New election - newElection(); - $state.go("admin.basic"); - $scope.isTest = !$scope.current['real']; + var draft = $stateParams.draft; + if ("true" === draft) { + // Load draft + loadDraft() + .then(goToBasic); + } else { + // New election + newElection(); + goToBasic(); + } } - var states =[ 'admin.dashboard', 'admin.basic', 'admin.questions', 'admin.censusConfig', 'admin.census', 'admin.auth', 'admin.tally', 'admin.successAction', 'admin.create']; + var states =[ 'admin.dashboard', 'admin.basic', 'admin.questions', 'admin.censusConfig', 'admin.census', 'admin.auth', 'admin.tally', 'admin.successAction', 'admin.adminFields', 'admin.create', 'admin.activityLog', 'admin.ballotBox']; + + var plugins_data = {states: [] }; + Plugins.hook('add-dashboard-election-states', plugins_data); + states = states.concat(plugins_data.states); + if (states.indexOf($scope.state) >= 0) { - $scope.sidebarlinks = [ + $scope.sidebarlinks = []; + if (!!id) { + $scope.sidebarlinks = $scope.sidebarlinks.concat([ + {name: 'activityLog', icon: 'pie-chart'} + ]); + + ElectionsApi.getElection(id).then( + function(el) + { + if (el.census.has_ballot_boxes && + ElectionsApi.getCachedEditPerm(id).indexOf('list-ballot-boxes') !== -1) + { + $scope.sidebarlinks.splice(1, 0, {name: 'ballotBox', icon: 'archive'}); + } + } + ); + } + $scope.sidebarlinks = $scope.sidebarlinks.concat([ {name: 'basic', icon: 'university'}, {name: 'questions', icon: 'question-circle'}, {name: 'auth', icon: 'unlock'}, {name: 'censusConfig', icon: 'newspaper-o'}, {name: 'census', icon: 'users'}, //{name: 'successAction', icon: 'star-o'}, + {name: 'adminFields', icon: 'user'}, //{name: 'tally', icon: 'pie-chart'}, - ]; - // if showSuccessAction is true, + ]); + // if showSuccessAction is true, // show the SuccessAction tab in the admin gui if (true === ConfigService.showSuccessAction) { $scope.sidebarlinks = $scope.sidebarlinks.concat([{name: 'successAction', icon: 'star-o'}]); @@ -94,10 +159,91 @@ angular.module('avAdmin').controller('AdminController', current = newElection(); } $scope.current = current; - $scope.isTest = !$scope.current['real']; } } else { $scope.sidebarlinks = []; } + var sidebar_plugins = $scope.plugins.list.filter( + function (plug) { + return true === plug.sidebarlink && _.isString(plug.before); + }); + $scope.sidebarlinks.forEach( function (sidebarlink) { + sidebarlink.plugins = sidebar_plugins.filter(function (plug) { + return 'admin.' + sidebarlink.name === plug.before; + }); + }); + $scope.sidebarlinks.forEach( + function (sidebarlink) { + next_states = next_states.concat(_.map( + sidebarlink.plugins, + function (plug) { + return plug.link; + })); + next_states.push('admin.' + sidebarlink.name); + }); + updateStates(); + NextButtonService.setStates(next_states); + + $scope.draft = {}; + $scope.has_draft = false; + + function updateDraft(el) { + $timeout(function () { + $scope.draft = el; + $scope.has_draft = ("{}" !== JSON.stringify(el)); + }); + } + + function getUpdateDraft() { + DraftElection.getDraft(updateDraft) + .then(updateDraft); + } + getUpdateDraft(); + + $scope.loadDraft = function () { + // show a warning dialog before loading draft + $modal + .open({ + templateUrl: "avAdmin/admin-directives/elections/use-draft-modal.html", + controller: "UseDraftModal", + size: 'lg', + resolve: { + title: function () { return $scope.draft.title; } + } + }) + .result.then(function (data) { + if ('ok' === data) { + $state.go("admin.new", {"draft": true}); + } + }); + }; + + $scope.eraseDraft = function () { + // show a warning dialog before erasing draft + $modal + .open({ + templateUrl: "avAdmin/admin-directives/elections/erase-draft-modal.html", + controller: "EraseDraftModal", + size: 'lg', + resolve: { + title: function () { return $scope.draft.title; } + } + }) + .result.then(function (data) { + if ('ok' === data) { + DraftElection.eraseDraft() + .then(function () { + getUpdateDraft(); + }, + function (error) { + console.log("error erasing draft: " + error); + }); + } + }); + }; + + if (!id) { + DraftElection.updateDraft(); + } } ); diff --git a/avAdmin/admin-directives/abstract-setting/abstract-setting.html b/avAdmin/admin-directives/abstract-setting/abstract-setting.html new file mode 100644 index 00000000..f2b826fa --- /dev/null +++ b/avAdmin/admin-directives/abstract-setting/abstract-setting.html @@ -0,0 +1,91 @@ +
+
+
+ + +
+
+
+ +
+
+

+ {{ description }} +

+
+
+
+
+
+
+
+ {{ shortValue }} +
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
diff --git a/avAdmin/admin-directives/abstract-setting/abstract-setting.js b/avAdmin/admin-directives/abstract-setting/abstract-setting.js new file mode 100644 index 00000000..df0f3e7b --- /dev/null +++ b/avAdmin/admin-directives/abstract-setting/abstract-setting.js @@ -0,0 +1,126 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avAdmin') + .directive( + 'avAbstractSetting', + function( + $q, + $state, + $http, + $sce, + Authmethod, + Plugins, + ElectionsApi, + $stateParams, + $modal, + ConfigService, + SendMsg) + { + function link(scope, element, attrs, nullController, transclude) { + scope.election = ElectionsApi.currentElection; + scope.title = ''; + scope.description = ''; + scope.showHelp = false; + scope.html = ''; + scope.helpPath = ''; + scope.forLabel = ''; + scope.expanded = true; + scope.collapsable = false; + scope.shortValue = ''; + scope.hoverShow = false; + scope.mouseEnter = function () { + scope.hoverShow = true; + }; + scope.mouseLeave = function () { + scope.hoverShow = false; + }; + + function watchAttr(name) { + attrs.$observe(name, function (newValue) { + scope[name] = newValue; + }); + } + + function transcludeWidget() { + var widget = element.find('.abstract-widget'); + if (_.isObject(widget)) { + transclude(scope, function(clone) { + widget.append(clone); + }); + } + } + + scope.toggleExpand = function() { + if (!!scope.collapsable) { + scope.expanded = !scope.expanded; + } + }; + + if (_.isString(attrs.collapsable)) { + scope.collapsable = ('true' === attrs.collapsable); + } + if (_.isString(attrs.expanded)) { + scope.expanded = ('true' === attrs.expanded); + } + if (_.isString(attrs.title)) { + scope.title = attrs.title; + } + if (_.isString(attrs.description)) { + scope.description = attrs.description; + } + if (_.isString(attrs.helpPath) && !!ConfigService.settingsHelpBaseUrl) { + scope.helpPath = ConfigService.settingsHelpBaseUrl + attrs.helpPath; + } else if(!!ConfigService.settingsHelpDefaultUrl) { + scope.helpPath = ConfigService.settingsHelpDefaultUrl; + } + if (_.isString(attrs.for)) { + scope.forLabel = attrs.for; + } + if (_.isString(attrs.shortValue)) { + scope.shortValue = attrs.shortValue; + } + + watchAttr('shortValue'); + + scope.toggleHelp = function() { + scope.showHelp = !scope.showHelp; + if (!!scope.showHelp && !scope.html && !!scope.helpPath) { + $http.get(scope.helpPath) + .then( + function (data) { + if (200 === data.status && !scope.html) { + scope.html = $sce.trustAsHtml(data.data); + } + }, + function (err) { + console.log("error loading setting help url\nurl: " + scope.helpPath + "\nerror: " + err); + scope.html = $sce.trustAsHtml(ConfigService.settingsHelpUrlError); + }); + } + }; + transcludeWidget(); + } + + return { + restrict: 'AE', + transclude: true, + scope: true, + link: link, + templateUrl: 'avAdmin/admin-directives/abstract-setting/abstract-setting.html' + }; + }); \ No newline at end of file diff --git a/avAdmin/admin-directives/abstract-setting/abstract-setting.less b/avAdmin/admin-directives/abstract-setting/abstract-setting.less new file mode 100644 index 00000000..3d430497 --- /dev/null +++ b/avAdmin/admin-directives/abstract-setting/abstract-setting.less @@ -0,0 +1,156 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2017 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +[av-abstract-setting] { + .collapse-icon { + width: 24px; + } + + .expanded-glyphicon { + font-size: large; + margin: 5px 0px 0px 5px; + } + + .abstract-setting { + margin: 15px 0px; + } + + .collapsed-widget { + margin: 8px; + padding: 10px; + } + + .collapsed-input { + height: 20px; + overflow: hidden; + } + + .btn-help { + color: #2f7bbf; + background-color: #fff; + border-color: transparent; + + &:hover { + border-color: #ccc; + } + + &:active, &:focus { + outline: none !important; + } + + .glyphicon { + font-size: 0.8em; + } + } + + .title { + font-size: x-large; + } + + .gray-back { + background-color: #f0f0f0; + padding: 15px 20px 10px 5px; + border-right: 1px; + border-right-style: solid; + border-right-color: #dedede; + } + + .abstract-widget { + background-color: white; + padding: 10px; + margin: 8px; + } + + .help-plugin { + padding: 15px 5px; + border-bottom: 1px; + border-bottom-style: solid; + border-bottom-color: #dedede; + border-left: 1px; + border-left-style: solid; + border-left-color: #dedede; + border-right: 1px; + border-right-style: solid; + border-right-color: #dedede; + } + + + .help-padding { + padding: 4px 10px; + border-bottom: 1px; + border-bottom-style: solid; + border-bottom-color: #dedede; + border-left: 1px; + border-left-style: solid; + border-left-color: #dedede; + border-right: 1px; + border-right-style: solid; + border-right-color: #dedede; + } + + .abstract-setting>.rower { + background-color: #f0f0f0; + display: flex; + + &.help-plugin { + background-color: white; + + a { + color: #2f7bbf; + border-bottom: 1px dotted; + + &:hover { + color: #00BF7F; + } + + &.headerlink { + display: none; + } + } + + img { + margin: 25px; + } + + h1 { + background-color: transparent; + } + + + } + } + + .whiteback { + background-color: white !important; + padding: 0; + margin: 0; + } + + .pointer { + cursor: pointer !important; + } + + .nopointer { + cursor: default !important; + } + + .abstract-borders { + border: 1px; + border-style: solid; + border-color: #dedede; + } +} diff --git a/avAdmin/admin-directives/activity-log/activity-log.html b/avAdmin/admin-directives/activity-log/activity-log.html new file mode 100644 index 00000000..5b744a9f --- /dev/null +++ b/avAdmin/admin-directives/activity-log/activity-log.html @@ -0,0 +1,236 @@ +
+

avAdmin.activityLog.title

+ +

+ avAdmin.activityLog.intro + + avAdmin.learnMore + +

+ +
+ +
+

+ + avAdmin.activityLog.manage + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ avAdmin.activityLog.tableColumn.actionColumnHeader + + avAdmin.activityLog.tableColumn.commentColumnHeader +
+ {{obj.id}} + + + + +
+ [i18next:html](obj)avAdmin.activityLog.action.authevent.create +
+
+ [i18next:html](obj)avAdmin.activityLog.action.authevent.callback +
+
+ [i18next:html](obj)avAdmin.activityLog.action.authevent.edit +
+ + +
+ [i18next:html](obj)avAdmin.activityLog.action.authevent.start +
+
+ [i18next:html](obj)avAdmin.activityLog.action.authevent.stop +
+
+ [i18next:html](obj)avAdmin.activityLog.action.authevent.delete +
+ + +
+ [i18next:html](obj)avAdmin.activityLog.action.user.activate +
+
+ [i18next:html](obj)avAdmin.activityLog.action.user.deactivate +
+ + +
+ [i18next:html](obj)avAdmin.activityLog.action.user.successful-login +
+
+ [i18next:html](obj)avAdmin.activityLog.action.user.send-auth +
+
+ [i18next:html](obj)avAdmin.activityLog.action.user.register +
+
+ [i18next:html](obj)avAdmin.activityLog.action.user.added-to-census +
+
+ [i18next:html](obj)avAdmin.activityLog.action.user.resend-authcode +
+ + +
+ [i18next:html]({ + executer_username: obj.executer_username, + executer_id: obj.executer_id, + event_id: obj.event_id, + ballot_box_id: obj.metadata.ballot_box_id, + ballot_box_name: obj.metadata.ballot_box_name + })avAdmin.activityLog.action.ballotBox.create +
+
+ [i18next:html]({ + executer_username: obj.executer_username, + executer_id: obj.executer_id, + event_id: obj.event_id, + ballot_box_id: obj.metadata.ballot_box_id, + ballot_box_name: obj.metadata.ballot_box_name + })avAdmin.activityLog.action.ballotBox.delete +
+ + +
+ [i18next:html]({ + executer_username: obj.executer_username, + executer_id: obj.executer_id, + event_id: obj.event_id, + ballot_box_id: obj.metadata.ballot_box_id, + ballot_box_name: obj.metadata.ballot_box_name, + tally_sheet_id: obj.metadata.tally_sheet_id, + })avAdmin.activityLog.action.tallySheet.create +
+
+ [i18next:html]({ + executer_username: obj.executer_username, + executer_id: obj.executer_id, + event_id: obj.event_id, + ballot_box_id: obj.metadata.ballot_box_id, + ballot_box_name: obj.metadata.ballot_box_name, + tally_sheet_id: obj.metadata.tally_sheet_id, + action_id: obj.id + })avAdmin.activityLog.action.tallySheet.delete +
+
+
+ {{obj.metadatacomment}} +
+
-
+
+ + loading +
+ +
+ + +
+ +
+ +
+ +
diff --git a/avAdmin/admin-directives/activity-log/activity-log.js b/avAdmin/admin-directives/activity-log/activity-log.js new file mode 100644 index 00000000..aef555ba --- /dev/null +++ b/avAdmin/admin-directives/activity-log/activity-log.js @@ -0,0 +1,157 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2018 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avAdmin') + .directive( + 'avAdminActivityLog', + function( + Authmethod, + ConfigService, + NextButtonService, + $timeout) + { + // we use it as something similar to a controller here + function link(scope, element, attrs) { + scope.electionId = attrs.electionId; + scope.reloadingActivity = false; + scope.loading = false; + scope.activity = []; + scope.nomore = false; + scope.error = null; + scope.page = 1; + scope.msg = null; + scope.resizeSensor = null; + scope.helpurl = ConfigService.helpUrl; + scope.filterStr = ""; + scope.filterTimeout = null; + scope.filterOptions = {}; + + scope.goNext = NextButtonService.goNext; + + /** + * Load more activity in infinite scrolling mode + */ + function loadMoreActivity(reload) { + if (scope.loading || scope.nomore) { + if (scope.reloadingActivity) { + scope.reloadingActivity = false; + } + return; + } + scope.loading = true; + + Authmethod.getActivity( + scope.electionId, + scope.page, + null, + scope.filterOptions, + scope.filterStr) + .success( + function(data) + { + scope.page += 1; + if (scope.reloadingActivity) + { + scope.reloadingActivity = false; + } + _.each(data.activity, function (obj) { + // I'm doing this because if we try to get this from the + // template, doesn't work: {{obj.metadata.comment}} is + // always undefined + obj.metadatacomment = obj.metadata.comment; + scope.activity.push(obj); + }); + + if (data.end_index === data.total_count) { + scope.nomore = true; + } + scope.loading = false; + } + ) + .error( + function(data) { + scope.error = data; + scope.loading = false; + + if (scope.reloadingActivity) { + scope.reloadingActivity = false; + } + } + ); + } + + function reloadActivity() { + scope.nomore = false; + scope.page = 1; + scope.reloadingActivity = true; + scope.activity.splice(0, scope.activity.length); + + loadMoreActivity(); + } + + // debounced reloading + function reloadActivityDebounce() { + $timeout.cancel(scope.filterTimeout); + scope.filterTimeout = $timeout(function() { + scope.reloadActivity(); + }, 500); + } + + // debounced filter options + scope.$watch("filterOptions", function(newOpts, oldOpts) { + if (_.isEqual(newOpts, oldOpts)) { + return; + } + reloadActivityDebounce(); + }, true); + + // debounced filter + scope.$watch("filterStr", function(newStr, oldStr) { + if (newStr === oldStr) { + return; + } + reloadActivityDebounce(); + }); + + // overflow-x needs to resize the height + var dataElement = angular.element(".data-table"); + /* jshint ignore:start */ + scope.resizeSensor = new ResizeSensor(dataElement, function() { + if (dataElement.width() > $(element).width()) { + $(element).width(dataElement.width()); + $(element).parent().css('overflow-x', 'auto'); + } + }); + /* jshint ignore:end */ + scope.$on("$destroy", function() { delete scope.resizeSensor; }); + + angular.extend(scope, { + loadMoreActivity: loadMoreActivity, + reloadActivity: reloadActivity + }); + + scope.reloadActivity(); + } + + return { + restrict: 'AE', + scope: { + }, + link: link, + templateUrl: 'avAdmin/admin-directives/activity-log/activity-log.html' + }; + }); diff --git a/avAdmin/admin-directives/activity-log/activity-log.less b/avAdmin/admin-directives/activity-log/activity-log.less new file mode 100644 index 00000000..88c87919 --- /dev/null +++ b/avAdmin/admin-directives/activity-log/activity-log.less @@ -0,0 +1,26 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +[av-admin-activity-log] { + .data-table tr:hover { + background-color: #f4f4f4; + } + + .data-table tbody tr td a { + text-decoration: underline; + } +} diff --git a/avAdmin/admin-directives/admin-field/admin-field.html b/avAdmin/admin-directives/admin-field/admin-field.html new file mode 100644 index 00000000..79e4768f --- /dev/null +++ b/avAdmin/admin-directives/admin-field/admin-field.html @@ -0,0 +1,203 @@ + +
+
+ + +
+

+

+ +
+ + [i18next]({value: field.value})avAdmin.adminFields.textError + + + avAdmin.adminFields.requiredError + +
+
+ +
+

+

+ +
+ + [i18next]({value: field.value})avAdmin.adminFields.textError + + + avAdmin.adminFields.requiredError + +
+
+
+

+

+ + +
+ + [i18next]({value: field.value})avAdmin.adminFields.emailError + + + avAdmin.adminFields.requiredError + +
+
+ +
+

+

+
+ + + +
+
+ + + + +
+
+

+ [i18next]({value: field.value})avAdmin.adminFields.numberError +

+

+ [i18next]({max: field.max, value: field.value})avAdmin.adminFields.maxError +

+

+ [i18next]({min: field.min, value: field.value})avAdmin.adminFields.minError +

+

+ avAdmin.adminFields.requiredError +

+
+
+
+
diff --git a/avAdmin/admin-directives/admin-field/admin-field.js b/avAdmin/admin-directives/admin-field/admin-field.js new file mode 100644 index 00000000..7c7de173 --- /dev/null +++ b/avAdmin/admin-directives/admin-field/admin-field.js @@ -0,0 +1,83 @@ + + +angular.module('avAdmin') + .directive( + 'avAdminField', + function( + $i18next, + ElectionsApi) + { + function link(scope, element, attrs) { + scope.editable = function () { + if (!_.isUndefined(attrs.editable)) { + return "true" === attrs.editable; + } + var election = ElectionsApi.currentElection; + var editable = !election.id || election.status === "registered"; + return editable; + }; + + scope.incInt = function (inc, event) { + var val = parseInt(scope.field.value); + var newValue = val + inc; + if("int" === scope.field.type && + _.isNumber(val) && + (!scope.field.max || newValue <= scope.field.max) && + (!scope.field.min || newValue >= scope.field.min)) + { + scope.field.value = newValue.toString(); + } + + if (!!event) { + event.preventDefault(); + } + }; + + scope.validateRequired = function (value) { + return !(_.isUndefined(value) || (_.isString(value) && 0 === value.length)); + }; + + scope.validateMax = function (value) { + var parsed = parseInt(value); + return !_.isNumber(scope.field.max) || _.isNaN(parsed) || parsed <= scope.field.max; + }; + + scope.validateMin = function (value) { + var parsed = parseInt(value); + return !_.isNumber(scope.field.min) || _.isNaN(parsed) || parsed >= scope.field.min; + }; + + scope.validateNumber = function (value) { + return !_.isNaN(parseInt(value)); + }; + + scope.validateText = function (value) { + return true; + }; + + scope.validateEmail = function (value) { + var re = /^[^\s@]+@[^\s@.]+\.[^\s@.]+$/; + return re.test(value); + }; + + scope.has_description = !_.isUndefined(scope.field.description) && + _.isString(scope.field.description) && + 0 < scope.field.description.length; + + scope.getPlaceholder = function () { + if (!_.isUndefined(scope.field.placeholder) && + _.isString(scope.field.placeholder)) { + return "[placeholder]" + scope.field.placeholder; + } + return ""; + }; + } + + return { + restrict: 'AE', + scope: true, + link: link, + templateUrl: 'avAdmin/admin-directives/admin-field/admin-field.html' + }; + } +); \ No newline at end of file diff --git a/avAdmin/admin-directives/admin-field/admin-field.less b/avAdmin/admin-directives/admin-field/admin-field.less new file mode 100644 index 00000000..22a5f7a4 --- /dev/null +++ b/avAdmin/admin-directives/admin-field/admin-field.less @@ -0,0 +1,9 @@ +[av-admin-field] { + label.control-label { + font-weight: normal; + } + + .ng-invalid, .ng-invalid-required { + border-color: #b70303; + } +} diff --git a/avAdmin/admin-directives/admin-fields/admin-fields.html b/avAdmin/admin-directives/admin-fields/admin-fields.html new file mode 100644 index 00000000..04f3ae03 --- /dev/null +++ b/avAdmin/admin-directives/admin-fields/admin-fields.html @@ -0,0 +1,19 @@ +
+
+
+

avAdmin.adminFields.title

+
+
+ +

avAdmin.adminFields.intro

+ + +
+
+ + +
diff --git a/avAdmin/admin-directives/admin-fields/admin-fields.js b/avAdmin/admin-directives/admin-fields/admin-fields.js new file mode 100644 index 00000000..d145d15b --- /dev/null +++ b/avAdmin/admin-directives/admin-fields/admin-fields.js @@ -0,0 +1,26 @@ +/** + * Requires the user to edit some admin fields + */ +angular.module('agora-gui-admin') + .directive( + 'avAdminFields', + function( + $i18next, + NextButtonService, + ElectionsApi) + { + function link(scope, element, attrs) + { + scope.goNext = NextButtonService.goNext; + scope.election = ElectionsApi.currentElection; + } + + return { + restrict: 'AEC', + scope: {}, + link: link, + templateUrl: 'avAdmin/admin-directives/admin-fields/admin-fields.html' + }; + } + ); + diff --git a/avAdmin/admin-directives/ballot-box/ballot-box.html b/avAdmin/admin-directives/ballot-box/ballot-box.html new file mode 100644 index 00000000..a88fe48b --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/ballot-box.html @@ -0,0 +1,175 @@ +
+

avAdmin.ballotBox.title

+ +

+ avAdmin.ballotBox.intro + + avAdmin.learnMore + +

+ +
+ +
+

+ + avAdmin.ballotBox.manage + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ avAdmin.ballotBox.tableColumn.nameColumnHeader + +
+
+
+
+
+
+ avAdmin.ballotBox.tableColumn.usernameColumnHeader + + avAdmin.ballotBox.tableColumn.actionsColumnHeader +
+ {{obj.id}} + + {{obj.name}} + + + + + - + + + + {{obj.num_tally_sheets}} + + + 0 + + + + {{obj.creator_username}} + + + - + + + + + + + + +
+ + loading +
+ +
+ + +
+ +
+ +
+ +
diff --git a/avAdmin/admin-directives/ballot-box/ballot-box.js b/avAdmin/admin-directives/ballot-box/ballot-box.js new file mode 100644 index 00000000..f32c0bcb --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/ballot-box.js @@ -0,0 +1,442 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2018 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avAdmin') + .directive( + 'avAdminBallotBox', + function( + Authmethod, + ConfigService, + NextButtonService, + $timeout, + $i18next, + $modal, + $location, + ElectionsApi + ) + { + // we use it as something similar to a controller here + function link(scope, element, attrs) { + scope.electionId = attrs.electionId; + scope.reloading = false; + scope.loading = false; + scope.object_list = []; + scope.nomore = false; + scope.error = null; + scope.page = 1; + scope.msg = null; + scope.resizeSensor = null; + scope.helpurl = ConfigService.helpUrl; + scope.filterStr = ""; + scope.filterTimeout = null; + scope.filterOptions = {}; + + scope.goNext = NextButtonService.goNext; + + /** + * Load more objects in infinite scrolling mode + */ + function loadMore(reload) { + if (scope.loading || scope.nomore) { + if (scope.reloading) { + scope.reloading = false; + } + return; + } + scope.loading = true; + + Authmethod.getBallotBoxes( + scope.electionId, + scope.page, + null, + scope.filterOptions, + scope.filterStr) + .success( + function(data) + { + scope.page += 1; + if (scope.reloading) + { + scope.reloading = false; + } + + _.each(data.object_list, function (obj) { + scope.object_list.push(obj); + }); + + if (data.end_index === data.total_count) { + scope.nomore = true; + } + scope.loading = false; + } + ) + .error( + function(data) { + scope.error = data; + scope.loading = false; + + if (scope.reloading) { + scope.reloading = false; + } + } + ); + } + + function reload() { + scope.nomore = false; + scope.page = 1; + scope.reloading = true; + scope.object_list.splice(0, scope.object_list.length); + + loadMore(); + } + + // debounced reloading + function reloadDebounce() { + $timeout.cancel(scope.filterTimeout); + scope.filterTimeout = $timeout(function() { + scope.reload(); + }, 500); + } + + // debounced filter options + scope.$watch("filterOptions", function(newOpts, oldOpts) { + if (_.isEqual(newOpts, oldOpts)) { + return; + } + reloadDebounce(); + }, true); + + // debounced filter + scope.$watch("filterStr", function(newStr, oldStr) { + if (newStr === oldStr) { + return; + } + reloadDebounce(); + }); + + // overflow-x needs to resize the height + var dataElement = angular.element(".data-table"); + /* jshint ignore:start */ + scope.resizeSensor = new ResizeSensor(dataElement, function() { + if (dataElement.width() > $(element).width()) { + $(element).width(dataElement.width()); + $(element).parent().css('overflow-x', 'auto'); + } + }); + /* jshint ignore:end */ + scope.$on("$destroy", function() { delete scope.resizeSensor; }); + + scope.canCreateBallotBox = ( + ElectionsApi.getCachedEditPerm(scope.electionId).indexOf('add-ballot-boxes') !== -1 + ); + + scope.createBallotBox = function() + { + $modal.open({ + templateUrl: "avAdmin/admin-directives/ballot-box/create-ballot-box-modal.html", + controller: "CreateBallotBoxModal", + size: 'lg', + resolve: {} + }) + .result.then(function(textarea) { + $modal.open({ + templateUrl: "avAdmin/admin-directives/ballot-box/checking-ballot-box-modal.html", + controller: "CheckingBallotBoxModal", + size: 'lg', + resolve: { + electionId: function () { return scope.electionId; }, + textarea: function () { return textarea; }, + errorFunc: function () + { + function errorFunction(data) + { + if (_.isBoolean(data)) { + scope.error = data; + } + return scope.error; + } + return errorFunction; + } + } + }) + .result.then( + scope.reload, + function (error) { + scope.reload(); + } + ); + }); + }; + + // list of row commands + scope.row_commands = [ + { + text: $i18next("avAdmin.ballotBox.viewTallySheetAction"), + iconClass: 'fa fa-file', + actionFunc: function(ballotBox) + { + Authmethod.getTallySheet( + scope.electionId, + ballotBox.id, + null + ) + .success( + function(tallySheet) + { + $modal.open({ + templateUrl: "avAdmin/admin-directives/ballot-box/view-tally-sheet-modal.html", + controller: "ViewTallySheetModal", + windowClass: "view-tally-sheet-modal", + resolve: { + tallySheet: function () { return tallySheet; }, + allowEdit: function () { return true; }, + ballotBox: function () { return ballotBox; }, + electionId: function () { return scope.electionId; }, + } + }) + .result.then( + function (action) { + if (action === "edit-tally-sheet") { + scope.row_commands[1].actionFunc(ballotBox); + } + }, + function (error) { + scope.reload(); + } + ); + } + ); + }, + enableFunc: function(ballotBox) { + return ( + ballotBox.num_tally_sheets > 0 && + ElectionsApi.getCachedEditPerm(scope.electionId).indexOf('list-tally-sheets') !== -1 + ); + } + }, + { + text: $i18next("avAdmin.ballotBox.writeTallySheetAction"), + iconClass: 'fa fa-edit', + actionFunc: function(ballotBox) + { + $modal.open({ + templateUrl: "avAdmin/admin-directives/ballot-box/write-tally-sheet-modal.html", + controller: "WriteTallySheetModal", + windowClass: "write-tally-sheet-modal", + resolve: { + ballotBox: function () { return ballotBox; } + } + }) + .result.then( + scope.reload, + function (error) { + scope.reload(); + } + ); + }, + enableFunc: function(ballotBox) { + return ( + ['stopped', 'tally_ok', 'results_ok'].indexOf(ElectionsApi.currentElection.status) !== -1 && ( + ( + ballotBox.num_tally_sheets > 0 && + ElectionsApi.getCachedEditPerm(scope.electionId).indexOf('override-tally-sheets') !== -1 + ) || ( + ballotBox.num_tally_sheets === 0 && + ElectionsApi.getCachedEditPerm(scope.electionId).indexOf('add-tally-sheets') !== -1 + ) + ) + ); + } + }, + { + text: $i18next("avAdmin.ballotBox.deleteTallySheetAction"), + iconClass: 'fa fa-minus-square', + actionFunc: function(ballotBox) + { + Authmethod.getTallySheet( + scope.electionId, + ballotBox.id, + null + ) + .success( + function(tallySheet) + { + $modal.open({ + templateUrl: "avAdmin/admin-directives/ballot-box/delete-tally-sheet-modal.html", + controller: "DeleteTallySheetModal", + resolve: { + ballotBox: function () { return ballotBox; }, + tallySheetId: function () { return tallySheet.id; }, + electionId: function () { return scope.electionId; }, + } + }) + .result.then( + scope.reload, + function (error) { + scope.reload(); + } + ); + }); + }, + enableFunc: function(ballotBox) { + return ( + ['stopped', 'tally_ok'].indexOf(ElectionsApi.currentElection.status) !== -1 && + ballotBox.num_tally_sheets > 0 && + ElectionsApi.getCachedEditPerm(scope.electionId).indexOf('delete-tally-sheets') !== -1 + ); + } + }, + { + text: $i18next("avAdmin.ballotBox.deleteBallotBoxAction"), + iconClass: 'fa fa-times', + actionFunc: function(ballotBox) + { + $modal.open({ + templateUrl: "avAdmin/admin-directives/ballot-box/delete-ballot-box-modal.html", + controller: "DeleteBallotBoxModal", + resolve: { + ballotBox: function () { return ballotBox; }, + electionId: function () { return scope.electionId; }, + } + }) + .result.then( + scope.reload, + function (error) { + scope.reload(); + } + ); + }, + enableFunc: function(ballotBox) { + return ( + ElectionsApi.getCachedEditPerm(scope.electionId).indexOf('delete-ballot-boxes') !== -1 + ); + } + } + ]; + + angular.extend(scope, { + loadMore: loadMore, + reload: reload + }); + + if ($location.search().view_ballot_box_name) + { + scope.filterStr = $location.search().view_ballot_box_name; + scope.reload(); + } else if ($location.search().view_tally_sheet_id && $location.search().ballot_box_id) + { + Authmethod.getBallotBoxes( + scope.electionId, + 1, + null, + {ballotbox__id__equals: $location.search().ballot_box_id}, + "" + ) + .success( + function(data) + { + if (data.total_count !== 1) { + return; + } + var ballotBox = data.object_list[0]; + + Authmethod.getTallySheet( + scope.electionId, + ballotBox.id, + $location.search().view_tally_sheet_id + ) + .success( + function(tallySheet) + { + $modal.open({ + templateUrl: "avAdmin/admin-directives/ballot-box/view-tally-sheet-modal.html", + controller: "ViewTallySheetModal", + windowClass: "view-tally-sheet-modal", + resolve: { + tallySheet: function () { return tallySheet; }, + allowEdit: function () { return true; }, + ballotBox: function () { return ballotBox; }, + electionId: function () { return scope.electionId; }, + } + }) + .result.then( + function (action) { + if (action === "edit-tally-sheet") { + scope.row_commands[1].actionFunc(ballotBox); + } + }, + function (error) { + scope.reload(); + } + ); + } + ); + } + ); + + } else if ($location.search().view_tally_sheet_from_action_id && $location.search().ballot_box_id && $location.search().ballot_box_name) + { + var action_id = $location.search().view_tally_sheet_from_action_id; + var ballot_box_id = $location.search().ballot_box_id; + var ballot_box_name = $location.search().ballot_box_name; + Authmethod.getActivity( + scope.electionId, + 1, + null, + { + activity__id__equals: action_id + } + ) + .success( + function(data) + { + if (data.total_count !== 1) { + return; + } + var action = data.activity[0]; + + $modal.open({ + templateUrl: "avAdmin/admin-directives/ballot-box/view-tally-sheet-modal.html", + controller: "ViewTallySheetModal", + windowClass: "view-tally-sheet-modal", + resolve: { + tallySheet: function () { return action.metadata; }, + allowEdit: function () { return false; }, + ballotBox: function () { + return { + id: ballot_box_id, + name: ballot_box_name + }; + }, + electionId: function () { return scope.electionId; }, + } + }); + } + ); + } + + scope.reload(); + } + + return { + restrict: 'AE', + scope: {}, + link: link, + templateUrl: 'avAdmin/admin-directives/ballot-box/ballot-box.html' + }; + }); diff --git a/avAdmin/admin-directives/ballot-box/ballot-box.less b/avAdmin/admin-directives/ballot-box/ballot-box.less new file mode 100644 index 00000000..f78755db --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/ballot-box.less @@ -0,0 +1,79 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +[av-admin-ballot-box] { + .data-table tr .actions { + span { + margin: 10px; + opacity: 0; + font-size: large; + } + + .disabled { display: none; } + } + + .data-table tr:hover { + background-color: #f4f4f4; + } + + .data-table tr:hover .actions span { + opacity: 1; + } +} + +.write-tally-sheet-modal, .view-tally-sheet-modal { + .container-fluid, .col-xs-6 { + padding: 0; + } + + .row { + margin-left: 0; + margin-right: 0; + } + + .value-label { + padding-top: 7px; + } + + .label-obs { + width: 18.88888%; + float: left; + } + + .data-obs { + width: 72%; + float: left; + padding-left: 26px; + } + + .important { + padding-bottom: 15px; + border-bottom: 1px solid #ddd; + margin-bottom: 15px; + } + + .question-title { + margin: 10px 0; + text-decoration: underline; + } +} + +@media (min-width: 960px) { + .write-tally-sheet-modal .modal-dialog, .view-tally-sheet-modal .modal-dialog { + width: 900px; + } +} diff --git a/avAdmin/admin-directives/ballot-box/checking-ballot-box-modal.html b/avAdmin/admin-directives/ballot-box/checking-ballot-box-modal.html new file mode 100644 index 00000000..865b3726 --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/checking-ballot-box-modal.html @@ -0,0 +1,61 @@ + + + diff --git a/avAdmin/admin-directives/ballot-box/checking-ballot-box-modal.js b/avAdmin/admin-directives/ballot-box/checking-ballot-box-modal.js new file mode 100644 index 00000000..8548a766 --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/checking-ballot-box-modal.js @@ -0,0 +1,95 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +// Checks if the ballot boxes are available (do not exist) and if so, create +// them +angular.module('avAdmin') + .controller( + 'CheckingBallotBoxModal', + function( + $scope, + $modalInstance, + electionId, + textarea, + ConfigService, + Authmethod + ) { + $scope.electionId = electionId; + $scope.state = "checking-existing"; + $scope.boxNames = textarea.split("\n"); + $scope.errorCheckingExisting = ""; + $scope.existingBoxes = []; + $scope.createdBoxes = []; + $scope.errorCreatingBoxes = []; + + function createBallotBoxes() + { + $scope.state = "creating"; + _.each( + $scope.boxNames, + function(name) + { + Authmethod.createBallotBox($scope.electionId, name) + .success(function (data) { + $scope.createdBoxes.push(name); + if ($scope.createdBoxes.length === $scope.boxNames.length) { + $scope.state = "success"; + } + }) + .error(function (error) { + $scope.errorCreatingBoxes.push(error); + }); + } + ); + } + + Authmethod.getBallotBoxes( + $scope.electionId, + 1, + null, + {ballotbox__name__in: $scope.boxNames.join("|")}, + null + ) + .success( + function(data) + { + if (data.total_count !== 0) + { + $scope.state = "has-existing"; + $scope.existingBoxes = data.object_list; + } else { + createBallotBoxes(); + } + } + ) + .error( + function(existing) { + $scope.state = "error-checking-existing"; + $scope.errorCheckingExisting = existing; + } + ); + + $scope.helpurl = ConfigService.helpUrl; + $scope.ok = function () { + $modalInstance.close($scope.ballotboxes_input); + }; + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + } + ); diff --git a/avAdmin/admin-directives/ballot-box/create-ballot-box-modal.html b/avAdmin/admin-directives/ballot-box/create-ballot-box-modal.html new file mode 100644 index 00000000..5fb3a222 --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/create-ballot-box-modal.html @@ -0,0 +1,34 @@ + + + diff --git a/avAdmin/admin-directives/ballot-box/create-ballot-box-modal.js b/avAdmin/admin-directives/ballot-box/create-ballot-box-modal.js new file mode 100644 index 00000000..5cf293d1 --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/create-ballot-box-modal.js @@ -0,0 +1,33 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avAdmin') + .controller( + 'CreateBallotBoxModal', + function($scope, $modalInstance, ConfigService) + { + $scope.ballotBoxes = {input: ""}; + $scope.helpurl = ConfigService.helpUrl; + $scope.ok = function () { + $modalInstance.close($scope.ballotBoxes.input); + }; + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + } + ); diff --git a/avAdmin/admin-directives/ballot-box/delete-ballot-box-modal.html b/avAdmin/admin-directives/ballot-box/delete-ballot-box-modal.html new file mode 100644 index 00000000..cb101066 --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/delete-ballot-box-modal.html @@ -0,0 +1,38 @@ + + + diff --git a/avAdmin/admin-directives/ballot-box/delete-ballot-box-modal.js b/avAdmin/admin-directives/ballot-box/delete-ballot-box-modal.js new file mode 100644 index 00000000..2d5fdb67 --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/delete-ballot-box-modal.js @@ -0,0 +1,54 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avAdmin') + .controller( + 'DeleteBallotBoxModal', + function( + $scope, + $modalInstance, + electionId, + ballotBox, + Authmethod, + ConfigService + ) { + $scope.electionId = electionId; + $scope.ballotBox = ballotBox; + $scope.deleteText = {text: ""}; + $scope.helpurl = ConfigService.helpUrl; + $scope.ok = function () + { + Authmethod.deleteBallotBox($scope.electionId, ballotBox.id) + .success( + function (data) + { + $modalInstance.close(); + } + ) + .error( + function (error) { + $scope.errorDeleteBox = error; + } + ); + }; + + $scope.cancel = function () + { + $modalInstance.dismiss('cancel'); + }; + } + ); diff --git a/avAdmin/admin-directives/ballot-box/delete-tally-sheet-modal.html b/avAdmin/admin-directives/ballot-box/delete-tally-sheet-modal.html new file mode 100644 index 00000000..5a8041d9 --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/delete-tally-sheet-modal.html @@ -0,0 +1,41 @@ + + + diff --git a/avAdmin/admin-directives/ballot-box/delete-tally-sheet-modal.js b/avAdmin/admin-directives/ballot-box/delete-tally-sheet-modal.js new file mode 100644 index 00000000..4c30c95e --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/delete-tally-sheet-modal.js @@ -0,0 +1,57 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avAdmin') + .controller( + 'DeleteTallySheetModal', + function( + $scope, + $modalInstance, + electionId, + ballotBox, + tallySheetId, + Authmethod, + ConfigService + ) { + $scope.electionId = electionId; + $scope.ballotBox = ballotBox; + $scope.tallySheetId = tallySheetId; + $scope.deleteText = {text: ""}; + $scope.helpurl = ConfigService.helpUrl; + $scope.errorDeleteTallySheet = ""; + $scope.ok = function () + { + Authmethod.deleteTallySheet($scope.electionId, ballotBox.id, $scope.tallySheetId) + .success( + function (data) + { + $modalInstance.close(); + } + ) + .error( + function (error) { + $scope.errorDeleteTallySheet = error; + } + ); + }; + + $scope.cancel = function () + { + $modalInstance.dismiss('cancel'); + }; + } + ); diff --git a/avAdmin/admin-directives/ballot-box/view-tally-sheet-modal.html b/avAdmin/admin-directives/ballot-box/view-tally-sheet-modal.html new file mode 100644 index 00000000..e05e8104 --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/view-tally-sheet-modal.html @@ -0,0 +1,167 @@ + + + + + + diff --git a/avAdmin/admin-directives/ballot-box/view-tally-sheet-modal.js b/avAdmin/admin-directives/ballot-box/view-tally-sheet-modal.js new file mode 100644 index 00000000..b9380a81 --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/view-tally-sheet-modal.js @@ -0,0 +1,46 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +// Form to register a ballot box tally sheet +angular.module('avAdmin') + .controller( + 'ViewTallySheetModal', + function( + $scope, + $modalInstance, + ElectionsApi, + allowEdit, + ballotBox, + tallySheet + ) { + + $scope.tallySheet = tallySheet.data; + $scope.tallySheetId = tallySheet.id; + $scope.ballotBox = ballotBox; + $scope.allowEdit = allowEdit; + + $scope.edit = function () + { + $modalInstance.close("edit-tally-sheet"); + }; + + $scope.cancel = function () + { + $modalInstance.dismiss('cancel'); + }; + } + ); diff --git a/avAdmin/admin-directives/ballot-box/write-tally-sheet-modal.html b/avAdmin/admin-directives/ballot-box/write-tally-sheet-modal.html new file mode 100644 index 00000000..2b60301f --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/write-tally-sheet-modal.html @@ -0,0 +1,371 @@ + + + diff --git a/avAdmin/admin-directives/ballot-box/write-tally-sheet-modal.js b/avAdmin/admin-directives/ballot-box/write-tally-sheet-modal.js new file mode 100644 index 00000000..4ff85140 --- /dev/null +++ b/avAdmin/admin-directives/ballot-box/write-tally-sheet-modal.js @@ -0,0 +1,166 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +// Form to register a ballot box tally sheet +angular.module('avAdmin') + .controller( + 'WriteTallySheetModal', + function( + $scope, + $modalInstance, + ElectionsApi, + ballotBox, + Authmethod + ) { + $scope.step = 0; + $scope.sending = false; + $scope.mismatchTotalCount = false; + + $scope.goToStep = function(step) { + if ($scope.step === 0 && step === 1) { + $scope.sending = false; + } + $scope.step = step; + }; + + $scope.tallySheet = { + id: ElectionsApi.currentElection.id, + title: ElectionsApi.currentElection.title, + + registeredVotes: 0, + observations: "", + num_votes: 0, + + questions: _.map( + ElectionsApi.currentElection.questions, + function (question) + { + return { + title: question.title, + blank_votes: 0, + null_votes: 0, + tally_type: question.tally_type, + answers: _.map( + question.answers, + function (answer) + { + return { + id: answer.id, + text: answer.text, + num_votes: 0 + }; + } + ) + }; + } + ) + }; + $scope.ballotBox = ballotBox; + $scope.deleteText = {text: ""}; + $scope.numbersError = false; + + + // used inside checkNumbers() to validate a tally sheet number + function checkNumber(i) + { + if (!angular.isNumber(i) || i < 0 || (i ^ 0) !== i) + { + throw "Invalid"; + } + } + + // throws an exception if expr is false + function assert(expr) + { + if (!expr) { + throw "Invalid"; + } + } + + // Checks if all numbers are valid (>=0) and add up + $scope.checkNumbers = function() + { + $scope.numbersError = false; + try { + checkNumber($scope.tallySheet.num_votes); + $scope.mismatchTotalCount = ( + $scope.tallySheet.num_votes === $scope.tallySheet.registeredVotes + ); + _.each( + $scope.tallySheet.questions, + function(question) { + checkNumber(question.blank_votes); + checkNumber(question.null_votes); + assert( + $scope.tallySheet.num_votes === ( + question.blank_votes + + question.null_votes + + _.reduce( + question.answers, + function (sum, answer) + { + checkNumber(answer.num_votes); + return sum + answer.num_votes; + }, + 0 + ) + ) + ); + } + ); + } catch(e) { + $scope.numbersError = true; + } + }; + + $scope.sendTally = function () + { + $scope.sending = true; + var sheet = angular.copy($scope.tallySheet); + delete sheet["registeredVotes"]; + delete sheet["id"]; + + Authmethod.postTallySheet( + ElectionsApi.currentElection.id, + ballotBox.id, + $scope.tallySheet + ) + .success( + function(data) + { + $scope.step = 2; + } + ) + .error( + function(data) { + $scope.error = data; + $scope.sending = false; + } + ); + }; + + $scope.close = function () + { + $modalInstance.close(); + }; + + $scope.cancel = function () + { + $modalInstance.dismiss('cancel'); + }; + } + ); diff --git a/avAdmin/admin-directives/census-field/census-field.html b/avAdmin/admin-directives/census-field/census-field.html index 67c7d27a..d0f38035 100644 --- a/avAdmin/admin-directives/census-field/census-field.html +++ b/avAdmin/admin-directives/census-field/census-field.html @@ -20,4 +20,7 @@
{{ c.metadata[field.name] }}
+
+ **** +
diff --git a/avAdmin/admin-directives/column-filters/int/column-filter-int.js b/avAdmin/admin-directives/column-filters/int/column-filter-int.js index 36fc7d2a..9b7d5fa5 100644 --- a/avAdmin/admin-directives/column-filters/int/column-filter-int.js +++ b/avAdmin/admin-directives/column-filters/int/column-filter-int.js @@ -16,15 +16,30 @@ **/ angular.module('avAdmin') - .directive('avColumnFilterInt', function() { + .directive('avColumnFilterInt', function($location) { function link(scope, element, attrs) { scope.status = { isOpen: false }; + + // Allows query parameters to automatically set the initial filter + function getLocationVar(postfix) { + var val = $location.search()[attrs.filterPrefix + "__" + postfix]; + try { + val = parseInt(val, 10); + } catch(err) { + val = undefined; + } + if (!!val) { + setkey(scope.filterOptionsVar, attrs.filterPrefix + "__" + postfix, val); + } + return (!!val) ? val : ''; + } + scope.filter = { - sort: '', - min: '', - max: '' + sort: getLocationVar('sort'), + min: getLocationVar('gt'), + max: getLocationVar('lt') }; scope.filterPrefix = attrs.filterPrefix; scope.filterI18n = attrs.filterI18n; diff --git a/avAdmin/admin-directives/column-filters/int/column-filter-int.less b/avAdmin/admin-directives/column-filters/int/column-filter-int.less index 2ff6cfc4..3ae68a59 100644 --- a/avAdmin/admin-directives/column-filters/int/column-filter-int.less +++ b/avAdmin/admin-directives/column-filters/int/column-filter-int.less @@ -46,7 +46,7 @@ // dropdown menu > .dropdown-menu { - min-width: 285px; + min-width: 380px; // add some spacing to the sort label @@ -86,4 +86,4 @@ } } } -} \ No newline at end of file +} diff --git a/avAdmin/admin-directives/create/create.js b/avAdmin/admin-directives/create/create.js index dccc2522..83e4fc27 100644 --- a/avAdmin/admin-directives/create/create.js +++ b/avAdmin/admin-directives/create/create.js @@ -22,6 +22,7 @@ angular.module('avAdmin') $q, Plugins, Authmethod, + DraftElection, ElectionsApi, $state, $stateParams, @@ -30,7 +31,9 @@ angular.module('avAdmin') $modal, ConfigService, ElectionLimits, - CheckerService) + CheckerService, + CsvLoad, + MustExtraFieldsService) { // we use it as something similar to a controller here function link(scope, element, attrs) @@ -55,6 +58,10 @@ angular.module('avAdmin') function logError(text) { scope.log += "

" + text + "

"; } + function validateEmail(email) { + var re = /^[^\s@]+@[^\s@.]+\.[^\s@.]+$/; + return re.test(email); + } /* * Checks elections for errors @@ -113,7 +120,7 @@ angular.module('avAdmin') if (census.auth_method !== 'email') { return true; } - + return census.config.msg.length > 0; }, appendOnErrorLambda: function (census) { @@ -131,7 +138,7 @@ angular.module('avAdmin') if (census.auth_method !== 'email') { return true; } - + return census.config.msg.length <= 5000; }, appendOnErrorLambda: function (census) { @@ -149,7 +156,7 @@ angular.module('avAdmin') if (census.auth_method !== 'email') { return true; } - + return census.config.subject.length > 0; }, appendOnErrorLambda: function (census) { @@ -167,7 +174,7 @@ angular.module('avAdmin') if (census.auth_method !== 'email') { return true; } - + return census.config.subject.length <= 1024; }, appendOnErrorLambda: function (census) { @@ -185,7 +192,7 @@ angular.module('avAdmin') if (census.auth_method !== 'sms') { return true; } - + return census.config.msg.length > 0; }, appendOnErrorLambda: function (census) { @@ -203,7 +210,7 @@ angular.module('avAdmin') if (census.auth_method !== 'sms') { return true; } - + return census.config.msg.length <= 200; }, appendOnErrorLambda: function (census) { @@ -326,11 +333,480 @@ angular.module('avAdmin') ] } ] - } + }, + { + check: "object-key-chain", + key: "census", + prefix: "census-", + append: {}, + checks: [ + { + check: "is-array", + key: "admin_fields", + postfix: "-admin-fields" + }, + { + check: "array-length", + key: "admin_fields", + min: 0, + max: ElectionLimits.maxNumQuestions, + postfix: "-admin-fields" + }, + { + check: "lambda", + key: "admin_fields", + appendOnErrorLambda: function (admin_fields) { + var adminNames = []; + if (_.isArray(admin_fields)) { + _.each( + admin_fields, + function (field) { + if ('int' === field.type && + !_.isNumber(field.value)) { + var field_name = field.name; + if (_.isString(field.label) && + 0 < field.label.length) { + field_name = field.label; + } + adminNames.push(field_name); + } + }); + } + return {"admin_names": adminNames.join(", ")}; + }, + validator: function (admin_fields) { + if (_.isArray(admin_fields)) { + return _.every( + admin_fields, + function (field) { + return ('int' !== field.type || + _.isNumber(field.value)); + }); + } + return true; + }, + postfix: "-admin-fields-int-type-value" + }, + { + check: "lambda", + key: "admin_fields", + appendOnErrorLambda: function (admin_fields) { + var adminNames = []; + if (_.isArray(admin_fields)) { + _.each( + admin_fields, + function (field) { + if ('email' === field.type && + _.isString(field.value) && + !validateEmail(field.value)) { + var field_name = field.name; + if (_.isString(field.label) && + 0 < field.label.length) { + field_name = field.label; + } + adminNames.push(field_name); + } + }); + } + return {"admin_names": adminNames.join(", ")}; + }, + validator: function (admin_fields) { + if (_.isArray(admin_fields)) { + return _.every( + admin_fields, + function (field) { + if ('email' === field.type && + !_.isUndefined(field.value) && + _.isString(field.value)) { + return validateEmail(field.value); + } + return true; + }); + } + return true; + }, + postfix: "-admin-fields-email-type-value" + }, + { + check: "lambda", + key: "admin_fields", + appendOnErrorLambda: function (admin_fields) { + var adminNames = []; + if (_.isArray(admin_fields)) { + _.each( + admin_fields, + function (field) { + if ('text' === field.type && + !_.isString(field.value)) { + var field_name = field.name; + if (_.isString(field.label) && + 0 < field.label.length) { + field_name = field.label; + } + adminNames.push(field_name); + } + }); + } + return {"admin_names": adminNames.join(", ")}; + }, + validator: function (admin_fields) { + if (_.isArray(admin_fields)) { + return _.every( + admin_fields, + function (field) { + return ('text' !== field.type || + _.isString(field.value)); + }); + } + return true; + }, + postfix: "-admin-fields-string-type-value" + }, + { + check: "lambda", + key: "admin_fields", + appendOnErrorLambda: function (admin_fields) { + var adminNames = []; + if (_.isArray(admin_fields)) { + _.each( + admin_fields, + function (field) { + if (_.isUndefined(field.value) || + (('text' === field.type || + 'email' === field.type) && + _.isString(field.value) && + 0 === field.value.length)) { + var field_name = field.name; + if (_.isString(field.label) && + 0 < field.label.length) { + field_name = field.label; + } + adminNames.push(field_name); + } + }); + } + return {"admin_names": adminNames.join(", ")}; + }, + validator: function (admin_fields) { + if (_.isArray(admin_fields)) { + return _.every( + admin_fields, + function (field) { + if (true === field.required) { + if (_.isUndefined(field.value) || + (('text' === field.type || + 'email' === field.type) && + _.isString(field.value) && + 0 === field.value.length)) { + return false; + } + } + return true; + }); + } + return true; + }, + postfix: "-admin-fields-required-value" + }, + { + check: "lambda", + key: "admin_fields", + append: {key: "max", value: ElectionLimits.maxLongStringLength}, + appendOnErrorLambda: function (admin_fields) { + var adminNames = []; + if (_.isArray(admin_fields)) { + _.each( + admin_fields, + function (field) { + if ('text' === field.type && + _.isString(field.value) && + (field.value.length > ElectionLimits.maxLongStringLength)) { + var field_name = field.name; + if (_.isString(field.label) && + 0 < field.label.length) { + field_name = field.label; + } + adminNames.push(field_name); + } + }); + } + return {"admin_names": adminNames.join(", ")}; + }, + validator: function (admin_fields) { + if (_.isArray(admin_fields)) { + return _.every( + admin_fields, + function (field) { + if ('text' === field.type && + _.isString(field.value)) { + return (field.value.length <= ElectionLimits.maxLongStringLength); + } + return true; + }); + } + return true; + }, + postfix: "-admin-fields-string-value-array-length" + }, + { + check: "lambda", + key: "admin_fields", + append: {key: "max", value: ElectionLimits.maxLongStringLength}, + appendOnErrorLambda: function (admin_fields) { + var adminNames = []; + if (_.isArray(admin_fields)) { + _.each( + admin_fields, + function (field) { + if ('email' === field.type && + _.isString(field.value) && + (field.value.length > ElectionLimits.maxLongStringLength)) { + var field_name = field.name; + if (_.isString(field.label) && + 0 < field.label.length) { + field_name = field.label; + } + adminNames.push(field_name); + } + }); + } + return {"admin_names": adminNames.join(", ")}; + }, + validator: function (admin_fields) { + if (_.isArray(admin_fields)) { + return _.every( + admin_fields, + function (field) { + if ('email' === field.type && + _.isString(field.value)) { + return (field.value.length <= ElectionLimits.maxLongStringLength); + } + return true; + }); + } + return true; + }, + postfix: "-admin-fields-email-value-array-length" + }, + { + check: "lambda", + key: "admin_fields", + appendOnErrorLambda: function (admin_fields) { + var adminNames = []; + if (_.isArray(admin_fields)) { + _.each( + admin_fields, + function (field) { + if ('int' === field.type && + !_.isUndefined(field.value) && + _.isNumber(field.value) && + !_.isUndefined(field.min) && + _.isNumber(field.min) && + field.min > field.value) { + var field_name = field.name; + if (_.isString(field.label) && + 0 < field.label.length) { + field_name = field.label; + } + adminNames.push(field_name); + } + }); + } + return {"admin_names": adminNames.join(", ")}; + }, + validator: function (admin_fields) { + if (_.isArray(admin_fields)) { + return _.every( + admin_fields, + function (field) { + if ('int' === field.type && + !_.isUndefined(field.value) && + _.isNumber(field.value) && + !_.isUndefined(field.min) && + _.isNumber(field.min)) { + return (field.min <= field.value); + } + return true; + }); + } + return true; + }, + postfix: "-admin-fields-int-min-value" + }, + { + check: "lambda", + key: "admin_fields", + appendOnErrorLambda: function (admin_fields) { + var adminNames = []; + if (_.isArray(admin_fields)) { + _.each( + admin_fields, + function (field) { + if ('int' === field.type && + !_.isUndefined(field.value) && + _.isNumber(field.value) && + !_.isUndefined(field.max) && + _.isNumber(field.max) && + field.max < field.value) { + var field_name = field.name; + if (_.isString(field.label) && + 0 < field.label.length) { + field_name = field.label; + } + adminNames.push(field_name); + } + }); + } + return {"admin_names": adminNames.join(", ")}; + }, + validator: function (admin_fields) { + if (_.isArray(admin_fields)) { + return _.every( + admin_fields, + function (field) { + if ('int' === field.type && + !_.isUndefined(field.value) && + _.isNumber(field.value) && + !_.isUndefined(field.max) && + _.isNumber(field.max)) { + return (field.max >= field.value); + } + return true; + }); + } + return true; + }, + postfix: "-admin-fields-int-max-value" + }, + { + check: "array-key-group-chain", + key: "admin_fields", + prefix: "admin-fields-", + append: {key: "fname", value: "$value.name"}, + checks: [ + { + check: "is-string-if-defined", + key: "placeholder", + postfix: "-placeholder" + }, + { + check: "array-length-if-defined", + key: "placeholder", + min: 0, + max: ElectionLimits.maxLongStringLength, + postfix: "-placeholder" + }, + { + check: "is-string", + key: "label", + postfix: "-label" + }, + { + check: "array-length", + key: "label", + min: 0, + max: ElectionLimits.maxLongStringLength, + postfix: "-label" + }, + { + check: "is-string-if-defined", + key: "description", + postfix: "-description" + }, + { + check: "array-length-if-defined", + key: "description", + min: 0, + max: ElectionLimits.maxLongStringLength, + postfix: "-description" + }, + { + check: "is-string", + key: "name", + postfix: "-name" + }, + { + check: "array-length", + key: "name", + min: 0, + max: ElectionLimits.maxLongStringLength, + postfix: "-name" + }, + { + check: "is-string", + key: "type", + postfix: "-type" + }, + { + check: "array-length", + key: "type", + min: 0, + max: ElectionLimits.maxLongStringLength, + postfix: "-type" + }, + { + check: "lambda", + key: "min", + validator: function (min) { + if (!_.isUndefined(min) && !_.isNumber(min)) { + return false; + } + return true; + }, + postfix: "-min-number" + }, + { + check: "lambda", + key: "max", + validator: function (max) { + if (!_.isUndefined(max) && !_.isNumber(max)) { + return false; + } + return true; + }, + postfix: "-max-number" + }, + { + check: "lambda", + key: "step", + validator: function (step) { + if (!_.isUndefined(step) && !_.isNumber(step)) { + return false; + } + return true; + }, + postfix: "-step-number" + }, + { + check: "lambda", + key: "required", + validator: function (required) { + if (!_.isUndefined(required) && !_.isBoolean(required)) { + return false; + } + return true; + }, + postfix: "-required-boolean" + }, + { + check: "lambda", + key: "private", + validator: function (_private) { + if (!_.isUndefined(_private) && !_.isBoolean(_private)) { + return false; + } + return true; + }, + postfix: "-private-boolean" + } + ] + }, + ]}, ] } ]; + Plugins.hook('election-create-add-checks', {'checks': checks, 'elections': scope.elections}); scope.errors = []; CheckerService({ checks: checks, @@ -361,13 +837,19 @@ angular.module('avAdmin') var d = { auth_method: el.census.auth_method, + has_ballot_boxes: el.census.has_ballot_boxes, census: el.census.census, auth_method_config: el.census.config, extra_fields: [], - real: el.real, - num_successful_logins_allowed: el.num_successful_logins_allowed + admin_fields: [], + num_successful_logins_allowed: el.num_successful_logins_allowed, + allow_public_census_query: el.allow_public_census_query }; + d.admin_fields = _.filter(el.census.admin_fields, function(af) { + return true; + }); + d.extra_fields = _.filter(el.census.extra_fields, function(ef) { var must = ef.must; delete ef.disabled; @@ -404,13 +886,29 @@ angular.module('avAdmin') function addCensus(el) { console.log("adding census for election " + el.title); var deferred = $q.defer(); + // Adding the census logInfo($i18next('avAdmin.create.census', {title: el.title, id: el.id})); - var voters = _.map(el.census.voters, function (i) { return i.metadata; }); - Authmethod.addCensus(el.id, voters, 'disabled') - .success(function(data) { + + var data = { + election: el, + error: function (errorMsg) { + scope.errors.push({ + data: {message: errorMsg}, + key: "election-census-createel-unknown" + }); + }, + disableOk: false, + cancel: function (error) { + Plugins.hook('census-csv-load-error', error); + }, + close: function () {} + }; + CsvLoad.uploadUponElCreation(data) + .then(function(data) { deferred.resolve(el); - }).error(deferred.reject); + }).catch(deferred.reject); + return deferred.promise; } @@ -470,6 +968,7 @@ angular.module('avAdmin') .then(function(el) { console.log("waiting for election " + el.title); waitForCreated(el.id, function () { + DraftElection.eraseDraft(); addElection(i+1); }); }) @@ -500,6 +999,18 @@ angular.module('avAdmin') function (data) { scope.elections = angular.fromJson(data.electionJson); + + scope.errors = []; + CheckerService({ + checks: checks, + data: scope.elections, + onError: function (errorKey, errorData) { + scope.errors.push({ + data: errorData, + key: errorKey + }); + } + }); } ); }; @@ -531,6 +1042,18 @@ angular.module('avAdmin') }); } + function checkMustExtra() { + var index = 0; + for (; index < scope.elections.length; index++) { + MustExtraFieldsService(scope.elections[index]); + } + } + checkMustExtra(); + + scope.$watch("elections", function (newVal, oldVal) { + scope.$evalAsync(checkMustExtra); + }, true); + angular.extend(scope, { createElections: createElections, }); diff --git a/avAdmin/admin-directives/dashboard/dashboard.html b/avAdmin/admin-directives/dashboard/dashboard.html index 7cc9be43..43a725bd 100644 --- a/avAdmin/admin-directives/dashboard/dashboard.html +++ b/avAdmin/admin-directives/dashboard/dashboard.html @@ -11,7 +11,7 @@

{{ status }}
- - - - - -

{{ q.totals.valid_votes|number }}
avAdmin.dashboard.optionvotes - ({{ percentVotes(q.totals.valid_votes, q) }}) + ({{ percentVotes(q.totals.valid_votes, q, "over-all") }})
{{ q.totals.blank_votes }}
avAdmin.dashboard.blankvotes - ({{ percentVotes(q.totals.blank_votes, q) }}) + ({{ percentVotes(q.totals.blank_votes, q, "over-all") }})
{{ q.totals.null_votes }}
avAdmin.dashboard.nullvotes - ({{ percentVotes(q.totals.null_votes, q) }}) + ({{ percentVotes(q.totals.null_votes, q, "over-all") }})
@@ -313,7 +282,7 @@

{{ q.title }}

{{ $index + 1 }} {{ an.total_count|number }} - {{ percentVotes(an.total_count, q, "over-valid-votes") }} + {{ percentVotes(an.total_count, q) }} {{ an.winner_position + 1}} -- diff --git a/avAdmin/admin-directives/dashboard/dashboard.js b/avAdmin/admin-directives/dashboard/dashboard.js index 58b0b312..dc1d7b45 100644 --- a/avAdmin/admin-directives/dashboard/dashboard.js +++ b/avAdmin/admin-directives/dashboard/dashboard.js @@ -17,15 +17,16 @@ angular.module('avAdmin') .directive( - 'avAdminDashboard', + 'avAdminDashboard', function( - $state, - Authmethod, - Plugins, - ElectionsApi, - $stateParams, - $modal, - PercentVotesService, + $q, + $state, + Authmethod, + Plugins, + ElectionsApi, + $stateParams, + $modal, + PercentVotesService, ConfigService, SendMsg) { @@ -57,18 +58,7 @@ angular.module('avAdmin') 'avAdmin.dashboard.publish' ]; - scope.calculateResultsJson = [ - [ - "agora_results.pipes.results.do_tallies", - {"ignore_invalid_votes": true} - ], - [ - "agora_results.pipes.sort.sort_non_iterative", - { - "question_indexes": [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] - } - ] - ]; + scope.calculateResultsJson = ""; var commands = [ @@ -144,14 +134,14 @@ angular.module('avAdmin') path: 'calculate-results', method: 'POST', confirmController: "ConfirmCalculateResultsModal", - payload: angular.toJson(scope.calculateResultsJson, true), + payload: scope.calculateResultsJson, confirmTemplateUrl: "avAdmin/admin-directives/dashboard/confirm-calculate-results-modal.html", doAction: function (data) { // calculate results command var command = commands[5]; command.payload = data; - scope.calculateResultsJson = angular.fromJson(data); + scope.calculateResultsJson = data; var ignorecache = true; ElectionsApi.getElection(id, ignorecache) .then(function(el) { @@ -199,6 +189,14 @@ angular.module('avAdmin') .then(function(el) { scope.loading = false; scope.election = el; + + if (!!el.resultsConfig && el.resultsConfig.length > 0) { + commands[5].payload = scope.calculateResultsJson = el.resultsConfig; + } else { + commands[5].payload = scope.calculateResultsJson = angular.toJson(ConfigService.calculateResultsDefault, true); + + } + scope.intally = el.status === 'doing_tally'; if (scope.intally) { scope.index = statuses.indexOf('stopped') + 1; @@ -232,6 +230,7 @@ angular.module('avAdmin') scope.waiting = false; scope.loading = false; scope.prevStatus = null; + Plugins.hook('election-modified', {old: scope.election, el: el, calculateResults: calculateResults}); scope.election = el; scope.intally = el.status === 'doing_tally'; @@ -247,6 +246,12 @@ angular.module('avAdmin') if (el.status === 'results_ok') { ElectionsApi.results(el); + if (!!ConfigService.always_publish) { + scope.loading = true; + scope.prevStatus = scope.election.status; + scope.waiting = true; + setTimeout(waitElectionChange, 1000); + } } } } @@ -258,25 +263,57 @@ angular.module('avAdmin') return; } var command = commands[index]; - if (!angular.isDefined(command.confirmController)) { - doAction(index); + + // This hook allows plugins to interrupt this function. This interruption + // usually happens because the plugin does some processing and decides to + // show another previous dialog at this step, for example. + var pluginData = { + election: scope.election, + command: command, + deferred: false + }; + + if (!Plugins.hook( + 'dashboard-before-do-action', + pluginData)) + { return; } - var payload = {}; - if(angular.isDefined(command.payload)) { - payload = command.payload; - } - $modal.open({ - templateUrl: command.confirmTemplateUrl, - controller: command.confirmController, - size: 'lg', - resolve: { - payload: function () { return payload; } + function doActionConfirmBulk() { + if (!angular.isDefined(command.confirmController)) { + doAction(index); + return; } - }).result.then(function (data) { - doAction(index, data); - }); + var payload = {}; + if(angular.isDefined(command.payload)) { + payload = command.payload; + } + + $modal.open({ + templateUrl: command.confirmTemplateUrl, + controller: command.confirmController, + size: 'lg', + resolve: { + payload: function () { return payload; } + } + }).result.then(function (data) { + doAction(index, data); + }); + } + + if (!pluginData.deferred) { + doActionConfirmBulk(); + } else { + pluginData.deferred.promise + .then(function (futureData) { + doActionConfirmBulk(); + }) + .catch(function (failureData) { + }); + } + + } function doAction(index, data) { @@ -353,25 +390,6 @@ angular.module('avAdmin') $state.go("admin.basic"); } - function createRealElection() { - var el = ElectionsApi.templateEl(); - _.extend(el, angular.copy(scope.election)); - if (el.census.extra_fields && el.census.extra_fields.length > 0) { - for (var i = 0; i < el.census.extra_fields.length; i++) { - var field = el.census.extra_fields[i]; - if(field.slug) { - delete field['slug']; - } - } - } - scope.current = el; - el.id = null; - el.real = true; - ElectionsApi.setCurrent(el); - ElectionsApi.newElection = true; - $state.go("admin.create", {"autocreate": true}); - } - function changeSocial() { if(ConfigService.share_social.allow_edit) { $modal.open({ @@ -392,7 +410,6 @@ angular.module('avAdmin') doActionConfirm: doActionConfirm, sendAuthCodes: sendAuthCodes, duplicateElection: duplicateElection, - createRealElection: createRealElection, changeSocial: changeSocial }); } diff --git a/avAdmin/admin-directives/dashboard/send-auth-codes-modal-confirm.html b/avAdmin/admin-directives/dashboard/send-auth-codes-modal-confirm.html index ab4ebc95..de58c301 100644 --- a/avAdmin/admin-directives/dashboard/send-auth-codes-modal-confirm.html +++ b/avAdmin/admin-directives/dashboard/send-auth-codes-modal-confirm.html @@ -51,7 +51,7 @@

-
+
@@ -78,7 +78,7 @@

+ ng-bind="parseMessage(election.census.config.subject)">

@@ -93,7 +93,7 @@

+ ng-bind="parseMessage(election.census.config.msg)">
diff --git a/avAdmin/admin-directives/dashboard/send-auth-codes-modal-confirm.js b/avAdmin/admin-directives/dashboard/send-auth-codes-modal-confirm.js index a8a32596..ae96c131 100644 --- a/avAdmin/admin-directives/dashboard/send-auth-codes-modal-confirm.js +++ b/avAdmin/admin-directives/dashboard/send-auth-codes-modal-confirm.js @@ -120,22 +120,36 @@ angular.module('avAdmin') /** * Render the example message with template substitution */ - $scope.exampleMsg = function() + $scope.parseMessage = function(msg) { + function replacer(str, keys) { + var out = str; + Object.keys(keys).map( + function (key) { + var value = keys[key]; + out = out.replace(key, value); + }); + return out; + } + var identity = "/aabb@gmail.com"; - if("sms" === election.census.auth_method) { + if("sms" === election.census.auth_method || "sms-otp" === election.census.auth_method) { identity = "/+34666666666"; } - var msg = election.census.config.msg; var url = "https://" + $location.host() + "/election/" + election.id + "/public/login" + identity; var url2 = url + "/AABB1234"; msg = msg.replace("__URL__", url); msg = msg.replace("__URL2__", url2); msg = msg.replace("__CODE__", "AABB1234"); + var keys = { + "__URL__": url, + "__URL2__": url2, + "__CODE__": "AABB1234" + }; for (var i = 0; i < SendMsg.slug_list.length; i++) { - msg = msg.replace("__" + SendMsg.slug_list[i] + "__", "AABB1234"); + keys["__" + SendMsg.slug_list[i] + "__"] = "AABB1234"; } - return msg; + return replacer(msg, keys); }; /** diff --git a/avAdmin/admin-directives/dashboard/send-auth-codes-modal-confirm.less b/avAdmin/admin-directives/dashboard/send-auth-codes-modal-confirm.less index 026f2163..c989e7ba 100644 --- a/avAdmin/admin-directives/dashboard/send-auth-codes-modal-confirm.less +++ b/avAdmin/admin-directives/dashboard/send-auth-codes-modal-confirm.less @@ -17,4 +17,5 @@ [modal-window] .send-auth-codes-modal .preview-body { white-space: pre-wrap; + word-wrap: break-word; } \ No newline at end of file diff --git a/avAdmin/admin-directives/dashboard/send-auth-codes-modal.html b/avAdmin/admin-directives/dashboard/send-auth-codes-modal.html index 21082bee..7c523a97 100644 --- a/avAdmin/admin-directives/dashboard/send-auth-codes-modal.html +++ b/avAdmin/admin-directives/dashboard/send-auth-codes-modal.html @@ -22,15 +22,15 @@

+ + + + + diff --git a/avAdmin/admin-directives/elcensus/csv-loading-modal.js b/avAdmin/admin-directives/elcensus/csv-loading-modal.js new file mode 100644 index 00000000..91f7269f --- /dev/null +++ b/avAdmin/admin-directives/elcensus/csv-loading-modal.js @@ -0,0 +1,41 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2017 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avAdmin') + .controller('CsvLoadingModal', + function($scope, $modalInstance, ConfigService, CsvLoad, election, textarea, errorFunc) { + $scope.election = election; + $scope.textarea = textarea; + $scope.helpurl = ConfigService.helpUrl; + $scope.error = errorFunc; + $scope.disableOk = false; + + $scope.cancel = function (error) { + $modalInstance.dismiss(_.isUndefined(error)? 'cancel': error); + }; + + $scope.close = function () { + $modalInstance.close('ok'); + }; + + CsvLoad.processCsv($scope); + + $scope.ok = function () { + $scope.disableOk = true; + CsvLoad.uploadCSV(); + }; + }); diff --git a/avAdmin/admin-directives/elcensus/csv-loading-modal.less b/avAdmin/admin-directives/elcensus/csv-loading-modal.less new file mode 100644 index 00000000..320d1981 --- /dev/null +++ b/avAdmin/admin-directives/elcensus/csv-loading-modal.less @@ -0,0 +1,29 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2017 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + + +.csv-loading-modal { + .progress { + background-color: darken(@av-primary, 4%); + } + .progress-bar { + background-color: @av-primary-contrast; + } + .zero { + color: @av-primary-contrast; + } +} diff --git a/avAdmin/admin-directives/elcensus/elcensus.html b/avAdmin/admin-directives/elcensus/elcensus.html index 6053c7ce..ccd9f813 100644 --- a/avAdmin/admin-directives/elcensus/elcensus.html +++ b/avAdmin/admin-directives/elcensus/elcensus.html @@ -101,8 +101,9 @@

avAdmin.sidebar.census

- {{ field.name| htmlToText }} + {{ field.name | htmlToText | limitTo:15 }} + avAdmin.census.actionsDropdown @@ -139,6 +140,15 @@

avAdmin.sidebar.census

+ + + + + + + @@ -171,17 +181,24 @@

avAdmin.sidebar.census

+
+ + + {{ election.id }} - avAdmin.elections.test - avAdmin.elections.real diff --git a/avAdmin/admin-directives/elections/elections.js b/avAdmin/admin-directives/elections/elections.js index 014df142..c29e2279 100644 --- a/avAdmin/admin-directives/elections/elections.js +++ b/avAdmin/admin-directives/elections/elections.js @@ -16,64 +16,88 @@ **/ angular.module('avAdmin') - .directive('avAdminElections', ['Authmethod', 'ElectionsApi', '$state', function(Authmethod, ElectionsApi, $state) { - // we use it as something similar to a controller here - function link(scope, element, attrs) { - scope.page = 1; - scope.loading = false; - scope.nomore = false; - scope.elections = []; + .directive( + 'avAdminElections', + function(Authmethod, ElectionsApi, DraftElection, AdminProfile, OnboardingTourService, $state, Plugins, $modal, $timeout, $window) + { + // we use it as something similar to a controller here + function link(scope, element, attrs) { + scope.page = 1; + scope.loading = false; + scope.nomore = false; + scope.elections = []; - function loadMoreElections() { - if (scope.loading || scope.nomore) { - return; + function maybeStartOnboarding() { + // launch the onboarding tour if the profile has been correctly + // filled up and the election list is zero + if ($window.electionsTotalCount !== undefined && + $window.electionsTotalCount === 0) + { + OnboardingTourService(); + } } - scope.loading = true; - function getAllElections(list) { - list.forEach(function (perm) { - ElectionsApi.getElection(perm.object_id) - .then(function(d) { - scope.elections.push(d); - scope.loading -= 1; - }) - .catch(function(d) { - // election doesn't exists in agora-elections - console.log("Not in agora elections: " + perm.object_id); - scope.loading -= 1; - }); - }); - } + function loadMoreElections() { + if (scope.loading || scope.nomore) { + return; + } + scope.loading = true; + + function getAllElections(list) { + list.forEach(function (perm) { + ElectionsApi.getElection(perm.object_id) + .then(function(d) { + scope.elections.push(d); + scope.loading -= 1; + }) + .catch(function(d) { + // election doesn't exists in agora-elections + console.log("Not in agora elections: " + perm.object_id); + scope.loading -= 1; + }); + }); + } + + Authmethod.electionsIds(scope.page) + .success(function(data) { + scope.page += 1; + + $window.electionsTotalCount = data.total_count; + AdminProfile.openProfileModal(true) + .then(maybeStartOnboarding,maybeStartOnboarding); - Authmethod.electionsIds(scope.page) - .success(function(data) { - scope.page += 1; + if (data.end_index === data.total_count) { + scope.nomore = true; + } - if (data.end_index === data.total_count) { - scope.nomore = true; - } + // here we've the elections id, then we need to ask to + // ElectionsApi for each election and load it. + scope.loading = data.perms.length; + getAllElections(data.perms); + }) + .error(function(data) { + scope.loading = false; + scope.error = data; + }); + } + + scope.exhtml = []; + Plugins.hook( + 'admin-elections-list-extra-html', + { + 'exhtml': scope.exhtml + } + ); - // here we've the elections id, then we need to ask to - // ElectionsApi for each election and load it. - scope.loading = data.perms.length; - getAllElections(data.perms); - }) - .error(function(data) { - scope.loading = false; - scope.error = data; - }); + angular.extend(scope, { + loadMoreElections: loadMoreElections, + }); } - angular.extend(scope, { - loadMoreElections: loadMoreElections, - }); + return { + restrict: 'AE', + link: link, + templateUrl: 'avAdmin/admin-directives/elections/elections.html' + }; } - - return { - restrict: 'AE', - scope: { - }, - link: link, - templateUrl: 'avAdmin/admin-directives/elections/elections.html' - }; - }]); + ); diff --git a/avAdmin/admin-directives/elections/elections.less b/avAdmin/admin-directives/elections/elections.less index 3dbe2f43..3fbeb0c7 100644 --- a/avAdmin/admin-directives/elections/elections.less +++ b/avAdmin/admin-directives/elections/elections.less @@ -16,9 +16,47 @@ **/ [av-admin-elections] { + .align-close { + text-align: center; + vertical-align: middle; + } + + .close-center { + float: unset !important; + } + + .label-draft { + background-color: #3784d6; + } + + .warn-notification { + background-color: @btn-warning-bg; + } + + .head-notification { + background-color: @btn-warning-bg; + color: white; + padding: 10px 5px 2px 5px; + border-radius: 5px; + margin-top: 10px; + + a { + color: @av-primary; + text-decoration: underline; + + &:hover { + text-decoration: none; + } + } + } + .electionlist { border: none; + .label-primary { + background-color: darken(@gray-lighter, 30%); + } + tbody { > tr { diff --git a/avAdmin/admin-directives/elections/erase-draft-modal.html b/avAdmin/admin-directives/elections/erase-draft-modal.html new file mode 100644 index 00000000..91936032 --- /dev/null +++ b/avAdmin/admin-directives/elections/erase-draft-modal.html @@ -0,0 +1,25 @@ + + + + + diff --git a/avAdmin/admin-directives/elections/erase-draft-modal.js b/avAdmin/admin-directives/elections/erase-draft-modal.js new file mode 100644 index 00000000..d420b5db --- /dev/null +++ b/avAdmin/admin-directives/elections/erase-draft-modal.js @@ -0,0 +1,14 @@ +angular.module('avAdmin') + .controller('EraseDraftModal', + function($scope, $modalInstance, title) + { + $scope.title = title; + + $scope.ok = function () { + $modalInstance.close('ok'); + }; + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + }); diff --git a/avAdmin/admin-directives/elections/use-draft-modal.html b/avAdmin/admin-directives/elections/use-draft-modal.html new file mode 100644 index 00000000..63f704b3 --- /dev/null +++ b/avAdmin/admin-directives/elections/use-draft-modal.html @@ -0,0 +1,25 @@ + + + + + diff --git a/avAdmin/admin-directives/elections/use-draft-modal.js b/avAdmin/admin-directives/elections/use-draft-modal.js new file mode 100644 index 00000000..1d99f802 --- /dev/null +++ b/avAdmin/admin-directives/elections/use-draft-modal.js @@ -0,0 +1,14 @@ +angular.module('avAdmin') + .controller('UseDraftModal', + function($scope, $modalInstance, title) + { + $scope.title = title; + + $scope.ok = function () { + $modalInstance.close('ok'); + }; + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + }); diff --git a/avAdmin/admin-directives/elquestions/elquestions.html b/avAdmin/admin-directives/elquestions/elquestions.html index 62f10747..20ac04d8 100644 --- a/avAdmin/admin-directives/elquestions/elquestions.html +++ b/avAdmin/admin-directives/elquestions/elquestions.html @@ -16,5 +16,5 @@
- +
diff --git a/avAdmin/admin-directives/elquestions/elquestions.js b/avAdmin/admin-directives/elquestions/elquestions.js index ad3c9e1c..743d3cd6 100644 --- a/avAdmin/admin-directives/elquestions/elquestions.js +++ b/avAdmin/admin-directives/elquestions/elquestions.js @@ -18,22 +18,20 @@ angular.module('avAdmin') .directive( 'avAdminElquestions', - function($i18next, $state, ElectionsApi, ElectionLimits) + function($i18next, $state, ElectionsApi, ElectionLimits,NextButtonService, ConfigService) { // we use it as something similar to a controller here function link(scope, element, attrs) { scope.election = ElectionsApi.currentElection; scope.electionLimits = ElectionLimits; - scope.vsystems = ['plurality-at-large', 'borda-nauru', 'borda', 'pairwise-beta']; + scope.vsystems = ConfigService.shownAdminQuestionVotingSystems; scope.lshuffleoptions = ['dont-shuffle','shuffle-all', 'shuffle-some']; scope.electionEditable = function() { return !scope.election.id || scope.election.status === "registered"; }; - function save() { - $state.go("admin.auth"); - } + scope.goNext = NextButtonService.goNext; function newQuestion() { var el = ElectionsApi.currentElection; @@ -142,7 +140,6 @@ angular.module('avAdmin') } angular.extend(scope, { - saveQuestions: save, newQuestion: newQuestion, delQuestion: delQuestion, expandQuestion: expandQuestion, diff --git a/avAdmin/admin-directives/extra-field/extra-field.html b/avAdmin/admin-directives/extra-field/extra-field.html index 9a3c7b58..1f409d35 100644 --- a/avAdmin/admin-directives/extra-field/extra-field.html +++ b/avAdmin/admin-directives/extra-field/extra-field.html @@ -100,6 +100,9 @@ + @@ -116,6 +119,11 @@ ng-i18next="avAdmin.census.fieldRegExLabel">
+
+
+
+
+
+
+ + +
+
+ +
+ +
+
+
+ @@ -359,9 +397,10 @@
+
{{field.name}}
- \ No newline at end of file + diff --git a/avAdmin/admin-directives/extra-field/extra-field.js b/avAdmin/admin-directives/extra-field/extra-field.js index ab931ba0..465bee02 100644 --- a/avAdmin/admin-directives/extra-field/extra-field.js +++ b/avAdmin/admin-directives/extra-field/extra-field.js @@ -18,8 +18,6 @@ angular.module('avAdmin') .directive('avExtraField', function() { function link(scope, element, attrs) { - scope.field.disabled = true; - scope.toggleEdit = function() { if (scope.extra_fields.editing === scope.field) { scope.extra_fields.editing = null; diff --git a/avAdmin/admin-directives/import/import.js b/avAdmin/admin-directives/import/import.js index ab228929..0b4314c3 100644 --- a/avAdmin/admin-directives/import/import.js +++ b/avAdmin/admin-directives/import/import.js @@ -44,11 +44,11 @@ angular.module('avAdmin') console.log("retrieveFile complete"); scope.loading = false; var els = ImportService(results.data); - // only works for one election, the first - ElectionsApi.currentElections = els; - ElectionsApi.setCurrent(els[0]); - ElectionsApi.newElection = true; - $state.go("admin.create"); + // only works for one election, the first + ElectionsApi.currentElections = els; + ElectionsApi.setCurrent(els[0]); + ElectionsApi.newElection = true; + $state.go("admin.create"); }, }); } diff --git a/avAdmin/admin-directives/number-input/number-input.js b/avAdmin/admin-directives/number-input/number-input.js new file mode 100644 index 00000000..734d81ee --- /dev/null +++ b/avAdmin/admin-directives/number-input/number-input.js @@ -0,0 +1,38 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2017 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avAdmin') + .directive( + 'avNumberInput', + [ + function () { + function link (scope, element, attrs) { + scope.$watch(attrs.ngModel, function (newValue, oldValue) { + if (_.isString(newValue) && !isNaN(parseInt(newValue))) { + scope.$evalAsync(function () { + eval('scope.' + attrs.ngModel + ' = ' + parseInt(newValue) + ';'); // jshint ignore:line + }); + } + }); + } + + return { + restrict: 'AEC', + scope: false, + link: link + }; + }]); diff --git a/avAdmin/admin-directives/question-option-details/question-option-details.js b/avAdmin/admin-directives/question-option-details/question-option-details.js index 02c4d7ba..1fc49a5d 100644 --- a/avAdmin/admin-directives/question-option-details/question-option-details.js +++ b/avAdmin/admin-directives/question-option-details/question-option-details.js @@ -19,11 +19,12 @@ angular.module('avAdmin') .directive('avQuestionOptionDetails', function($state, ElectionsApi) { function link(scope, element, attrs) { scope.editting = false; - if (scope.answer.urls.length !== 2) { - scope.answer.urls = [ - {title: "URL", url: ""}, - {title: "Image URL", url: ""} - ]; + var urlTitles = _.pluck(scope.answer.urls, 'title'); + if (-1 === _.indexOf(urlTitles, 'URL')) { + scope.answer.urls.push({title: "URL", url: ""}); + } + if (-1 === _.indexOf(urlTitles, 'Image URL')) { + scope.answer.urls.push({title: "Image URL", url: ""}); } scope.answerBeingEdited = function() { diff --git a/avAdmin/admin-directives/question/question.html b/avAdmin/admin-directives/question/question.html index 25fb16a6..41e41614 100644 --- a/avAdmin/admin-directives/question/question.html +++ b/avAdmin/admin-directives/question/question.html @@ -23,56 +23,67 @@
-
- -
- -
+
+
-
- -
- -
+
+
-
- -
-
- -
+
+
+
-
- -
-

avAdmin.questions.winners.placeholder

+
+
+
-
-
- -
+

avAdmin.questions.min.placeholder

- +
+
-
-
-
- -
+

avAdmin.questions.max.placeholder

@@ -204,10 +231,15 @@ +
+
-
-
- -
+

avAdmin.questions.layout.placeholder

@@ -269,13 +305,18 @@
-
-
- -
+
-
-
- -
+
-
-
- -
+

avAdmin.shuffling_policy.list_muted

@@ -323,13 +375,18 @@ ng-disabled="!electionEditable()" ng-i18next="[placeholder]avAdmin.shuffling_policy.list_placeholder" ng-model="internal.shuffling_cat_list"/> -
-
- -
+
-
-
- -
+

avAdmin.questions.candidates.placeholder @@ -361,7 +421,7 @@

-
+
-
diff --git a/avAdmin/admin-directives/question/question.js b/avAdmin/admin-directives/question/question.js index 014dc4ce..625b8fe4 100644 --- a/avAdmin/admin-directives/question/question.js +++ b/avAdmin/admin-directives/question/question.js @@ -16,16 +16,10 @@ **/ angular.module('avAdmin') - .directive('avAdminQuestion', function() { + .directive('avAdminQuestion', function(ConfigService) { // we use it as something similar to a controller here function link(scope, element, attrs) { - scope.layouts = [ - "circles", - "accordion", - "simultaneous-questions" - /*"conditional-accordion", - "pcandidates-election"*/ - ]; + scope.layouts = ConfigService.shownAdminQuestionLayouts; scope.edittingIndex = -1; function fillCatList() { @@ -108,7 +102,7 @@ angular.module('avAdmin') scope.$watch("internal.shuffling_cat_list", function (newValue, oldValue) { if ('shuffle-some' === scope.internal.shuffle_opts_policy) { - scope.q.extra_options.shuffle_category_list = + scope.q.extra_options.shuffle_category_list = _.map (newValue.split(','), function (x) { return x.trim(); }); diff --git a/avAdmin/admin-directives/question/question.less b/avAdmin/admin-directives/question/question.less index 6a52223f..6039a8ea 100644 --- a/avAdmin/admin-directives/question/question.less +++ b/avAdmin/admin-directives/question/question.less @@ -16,6 +16,28 @@ **/ [av-admin-question] { + [av-abstract-setting] { + .abstract-setting>.rower, .btn-help { + background-color: #f0f0f0 !important; + } + + .btn-help:hover { + background-color: #fff !important; + } + + .help-padding, .abstract-borders { + border-color: white !important; + } + + .gray-back { + border-right-color: white !important; + } + + .bottom-margin { + margin-bottom: 15px + } + } + .question { padding: 0; background-color: @av-bg; diff --git a/avAdmin/admin-directives/success-action/success-action.html b/avAdmin/admin-directives/success-action/success-action.html index 0c2b8a44..2a680145 100644 --- a/avAdmin/admin-directives/success-action/success-action.html +++ b/avAdmin/admin-directives/success-action/success-action.html @@ -77,7 +77,7 @@

avAdmin.sidebar.successAction

+ +
+ + \ No newline at end of file diff --git a/avAdmin/admin-profile/admin-profile.js b/avAdmin/admin-profile/admin-profile.js new file mode 100644 index 00000000..d315fae2 --- /dev/null +++ b/avAdmin/admin-profile/admin-profile.js @@ -0,0 +1,99 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2017 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avAdmin') + .controller('AdminProfileController', + function( + $scope, + $window, + $modalInstance, + ConfigService, + $sce, + Authmethod, + fields_def, + user_fields + ) { + // only end the tour if it has started + if (!!$window.hopscotch.getState()) { + $window.hopscotch.endTour(); + } + + var field; + for (var i = 0; i < fields_def.length; i++) { + field = fields_def[i]; + // adapt fields to have a label, to conform with admin-field directive + if (_.isUndefined(field.label)) { + field.label = field.name; + } + // give an initial value to fields + if (_.isUndefined(user_fields[field.name])) { + if (-1 !== ["text", "password", "regex", "email", "tlf", "textarea", + "dni"].indexOf(field.type)) { + field.value = ""; + } else if ("int" === field.type) { + field.value = 0; + } else if ("bool" === field.type) { + field.value = false; + } + } else { + // copy the value from the profile + field.value = angular.copy(user_fields[field.name]); + } + } + + $scope.fields_def = fields_def; + $scope.user_fields = user_fields; + $scope.showWorking = false; + + // true if some value has been changed and needs to be saved + function values_changed() { + var ret = false; + var field; + for (var i = 0; i < fields_def.length; i++) { + field = fields_def[i]; + if (field.value !== user_fields[field.name]) { + if ( false === ret) { + ret = {}; + } + ret[field.name] = field.value; + } + } + return ret; + } + + $scope.ok = function () { + var changed = values_changed(); + if (false === changed) { + $modalInstance.close(); + } else { + $scope.showWorking = true; + Authmethod.updateUserExtra(changed) + .success(function (d) { + $modalInstance.close(changed); + }) + .error(function (e) { + $modalInstance.close(changed); + }); + } + }; + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + + $scope.html = $sce.trustAsHtml(ConfigService.profileHtml); + }); diff --git a/avAdmin/admin-sidebar-directive/admin-sidebar-directive.html b/avAdmin/admin-sidebar-directive/admin-sidebar-directive.html index 0b96d752..dea98b1c 100644 --- a/avAdmin/admin-sidebar-directive/admin-sidebar-directive.html +++ b/avAdmin/admin-sidebar-directive/admin-sidebar-directive.html @@ -3,26 +3,64 @@ Elections + + + + + + + + + + + + + + -
  • +
  • diff --git a/avAdmin/admin-sidebar-directive/admin-sidebar-directive.js b/avAdmin/admin-sidebar-directive/admin-sidebar-directive.js index c725f509..581875fd 100644 --- a/avAdmin/admin-sidebar-directive/admin-sidebar-directive.js +++ b/avAdmin/admin-sidebar-directive/admin-sidebar-directive.js @@ -31,14 +31,17 @@ * You should have received a copy of the GNU Affero General Public License * along with agora-gui-admin. If not, see . **/ - + angular.module('avAdmin') - .directive('avAdminSidebar', ['$cookies', function($cookies) { + .directive('avAdminSidebar', ['$cookies', 'Authmethod', 'DraftElection', function($cookies, Authmethod, DraftElection) { // we use it as something similar to a controller here function link(scope, element, attrs) { - var admin = $cookies.user; + var autheventid = Authmethod.getAuthevent(); + var postfix = "_authevent_" + autheventid; + var admin = $cookies["user" + postfix]; scope.admin = admin; scope.active = attrs.active; + scope.isEditingDraft = DraftElection.isEditingDraft; } return { diff --git a/avAdmin/admin-sidebar-directive/admin-sidebar-directive.less b/avAdmin/admin-sidebar-directive/admin-sidebar-directive.less index 165a0027..3cabb3a4 100644 --- a/avAdmin/admin-sidebar-directive/admin-sidebar-directive.less +++ b/avAdmin/admin-sidebar-directive/admin-sidebar-directive.less @@ -1,4 +1,8 @@ [av-admin-sidebar] { + .capitalize-draft { + text-transform: capitalize; + } + .sidebar { background-color: lighten(@av-primary, 5%); color: @av-primary; diff --git a/avAdmin/csv-load-service.js b/avAdmin/csv-load-service.js new file mode 100644 index 00000000..8d75f7c5 --- /dev/null +++ b/avAdmin/csv-load-service.js @@ -0,0 +1,269 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avAdmin') + .factory( + 'CsvLoad', + function ( + $q, + $timeout, + ConfigService, + Plugins, + Authmethod) + { + var csvLoadService = {}; + + function calculateExportList(textarea) { + var el = csvLoadService.scope.election; + var cs; + if (!el.id) { + cs = el.census.voters; + } else { + cs = []; + } + + var fields = el.census.extra_fields; + + var lines = textarea.split("\n"); + lines.forEach(function(l) { + var lf = l.split(";"); + var nv = {}; + fields.forEach(function(f, i) { nv[f.name] = lf[i].trim(); }); + if (nv.tlf) { + nv.tlf = nv.tlf.replace(" ", ""); + } + if (nv.email) { + nv.email = nv.email.replace(" ", ""); + } + cs.push({selected: false, vote: false, username: "", metadata: nv}); + }); + + if (!!el.id) { + var csExport = _.map(cs, function (i) { return i.metadata; }); + return csExport; + } + return []; + } + + /** + * After introducing the CSV text in a previous modal, this method will + * parse it. If the election hasn't been created yet, the added census + * will be included in the local election variable. If the election exists, + * additional steps will be performed to prepare the uploading census + * modal. + */ + csvLoadService.processCsv = function (scope) { + csvLoadService.scope = scope; + if (!!scope.election.id) { + csvLoadService.scope.batchSize = ConfigService.censusImportBatch; + // 0 to 100% (when finished) + csvLoadService.scope.percent = 0; + csvLoadService.scope.disableOk = false; + + csvLoadService.scope.exportList = calculateExportList(csvLoadService.scope.textarea); + csvLoadService.scope.exportListIndex = 0; + + var pluginData = { + html: [], + scope: {}, + okhtml: [], + processBatchPlugin: false, + startClickedPlugin: false, + election: csvLoadService.scope.election, + exportList: csvLoadService.scope.exportList + }; + Plugins.hook('census-csv-loading-modal', pluginData); + csvLoadService.scope.exhtml = pluginData.html; + csvLoadService.scope.okhtml = pluginData.okhtml; + csvLoadService.scope = _.extend(csvLoadService.scope, pluginData.scope); + csvLoadService.scope.startClickedPlugin = pluginData.startClickedPlugin; + csvLoadService.scope.processBatchPlugin = pluginData.processBatchPlugin; + } else { + var el = csvLoadService.scope.election; + var exportList = calculateExportList(csvLoadService.scope.textarea); + el.census.voters.push.apply(exportList); + } + }; + + function calcPercent (index) { + return index*100.0/csvLoadService.scope.exportList.length; + } + + function censusCall(id, csExport, opt) { + var deferred = $q.defer(); + try { + // this hook can avoid the addCensus call + if (Plugins.hook('add-to-census-pre', csExport)) { + Authmethod.addCensus(id, csExport, opt) + .success(function(r) { + Plugins.hook('add-to-census-success', {data: csExport, response: r}); + deferred.resolve(); + }) + .error(function(error) { + csvLoadService.scope.error(error.error_codename); + Plugins.hook('add-to-census-error', {data: csExport, response: error}); + deferred.reject(error); + }); + } + } catch (error) { + deferred.reject(error); + } + return deferred.promise; + } + + function processBatch() { + var deferred = $q.defer(); + try { + var ret = { + 'percent': csvLoadService.scope.percent, + 'exportListIndex': csvLoadService.scope.exportListIndex, + 'calcPercent': calcPercent + }; + if (csvLoadService.scope.exportList.length > csvLoadService.scope.exportListIndex) { + var batch = []; + if (0 === csvLoadService.scope.batchSize || + (csvLoadService.scope.exportList.length - csvLoadService.scope.exportListIndex) <= csvLoadService.scope.batchSize) { + batch = csvLoadService.scope.exportList.slice(csvLoadService.scope.exportListIndex); + } else { + batch = csvLoadService.scope.exportList.slice(csvLoadService.scope.exportListIndex, csvLoadService.scope.exportListIndex + csvLoadService.scope.batchSize); + } + censusCall(csvLoadService.scope.election.id, batch, 'disabled') + .then(function () { + ret.exportListIndex = csvLoadService.scope.exportListIndex + batch.length; + ret.percent = calcPercent(ret.exportListIndex); + deferred.resolve(ret); + }) + .catch(deferred.reject); + } else { + deferred.resolve(ret); + } + } catch (error) { + deferred.reject(error); + } + return deferred.promise; + } + + function processBatchCaller() { + var deferred = $q.defer(); + processBatch() + .then(function (processed) { + if (_.isFunction(csvLoadService.scope.processBatchPlugin)) { + csvLoadService.scope.processBatchPlugin(processed) + .then(function (ret) { + $timeout(function () { + csvLoadService.scope.percent = ret.percent; + csvLoadService.scope.exportListIndex = ret.exportListIndex; + + if (csvLoadService.scope.percent < 100) { + processBatchCaller() + .then(deferred.resolve) + .catch(deferred.reject); + //setTimeout(processBatchCaller, 0); + } else if (_.isFunction(csvLoadService.scope.close)) { + deferred.resolve(); + } + }); + }); + } else { + $timeout(function () { + csvLoadService.scope.percent = processed.percent; + csvLoadService.scope.exportListIndex = processed.exportListIndex; + + if (csvLoadService.scope.percent < 100) { + processBatchCaller() + .then(deferred.resolve) + .catch(deferred.reject); + //setTimeout(processBatchCaller, 0); + } else if (_.isFunction(csvLoadService.scope.close)) { + deferred.resolve(); + } + }); + } + }) + .catch(deferred.reject); + return deferred.promise; + } + + /** + * This function will upload the census to an election that already exists + */ + csvLoadService.uploadCSV = function () { + var deferred = $q.defer(); + if (_.isFunction(csvLoadService.scope.startClickedPlugin)) { + csvLoadService.scope.startClickedPlugin(); + } + processBatchCaller() + .then(function(data) { + if (_.isFunction(csvLoadService.scope.close)) { + csvLoadService.scope.close(); + } + deferred.resolve(); + }).catch(function (error) { + if (_.isFunction(csvLoadService.scope.cancel)) { + csvLoadService.scope.cancel(error); + } + deferred.reject(); + }); + return deferred.promise; + }; + + /** + * This function will be called just after creating the election, and it + * will upload the census to the election. + */ + csvLoadService.uploadUponElCreation = function (scope) { + var deferred = $q.defer(); + if (0 === scope.election.census.voters.length) { + deferred.resolve(); + } else { + csvLoadService.scope = scope; + + csvLoadService.scope.batchSize = ConfigService.censusImportBatch; + // 0 to 100% (when finished) + csvLoadService.scope.percent = 0; + csvLoadService.scope.disableOk = false; + + csvLoadService.scope.exportList = + _.pluck(csvLoadService.scope.election.census.voters, 'metadata'); + csvLoadService.scope.exportListIndex = 0; + + var pluginData = { + html: [], + scope: {}, + okhtml: [], + processBatchPlugin: false, + startClickedPlugin: false, + election: csvLoadService.scope.election, + exportList: csvLoadService.scope.exportList + }; + Plugins.hook('census-csv-loading-modal', pluginData); + csvLoadService.scope.exhtml = pluginData.html; + csvLoadService.scope.okhtml = pluginData.okhtml; + csvLoadService.scope = _.extend(csvLoadService.scope, pluginData.scope); + csvLoadService.scope.startClickedPlugin = pluginData.startClickedPlugin; + csvLoadService.scope.processBatchPlugin = pluginData.processBatchPlugin; + + csvLoadService.uploadCSV() + .then(deferred.resolve) + .catch(deferred.reject); + } + return deferred.promise; + }; + + + return csvLoadService; + }); diff --git a/avAdmin/draft-election.js b/avAdmin/draft-election.js new file mode 100644 index 00000000..56a9207f --- /dev/null +++ b/avAdmin/draft-election.js @@ -0,0 +1,119 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2017 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avAdmin') + .factory( + 'DraftElection', + function( + $q, + Plugins, + Authmethod, + ConfigService, + ElectionsApi, + $i18next, + $http, + $timeout, + $modal, + $state, + $stateParams, + $cookies, + $rootScope) + { + var election; + var draft_election = {}; + var promise; + draft_election.getDraft = function (update_func) { + var deferred = $q.defer(); + if (_.isFunction(update_func)) { + if (!_.isUndefined(election) && + "{}" !== JSON.stringify(election)) + { + if (election.id) { + delete election.id; + } + update_func(election); + } + } + + Authmethod.getUserDraft() + .success(function (data) { + election = data; + deferred.resolve(election); + }) + .error(function (error) { + console.log("error downloading draft: " + error); + deferred.reject(error); + }); + return deferred.promise; + }; + + draft_election.isEditingDraft = function () { + var state = $state.current.name; + var id = $stateParams.id; + if (id) { + return false; + } + if (!("admin.basic" === state || + "admin.questions" === state || + "admin.auth" === state || + "admin.censusConfig" === state || + "admin.census" === state || + "admin.adminFields" === state || + "admin.create" === state)) { + return false; + } + return true; + }; + + draft_election.updateDraft = function () { + if (!draft_election.isEditingDraft()) { + election = undefined; + if (!_.isUndefined(promise)) { + $timeout.cancel(promise); + } + return; + } + + if (ElectionsApi.currentElections.length === 0 && !!ElectionsApi.currentElection) { + election = angular.copy(ElectionsApi.currentElection); + } else { + election = angular.copy(ElectionsApi.currentElections[0]); + } + + Authmethod.uploadUserDraft(election) + .success(function (data) { + }) + .error(function (error) { + console.log("error uploading draft: " + error); + }); + promise = $timeout(draft_election.updateDraft, 60000); + }; + + draft_election.eraseDraft = function () { + var deferred = $q.defer(); + election = undefined; + if (!_.isUndefined(promise)) { + $timeout.cancel(promise); + } + Authmethod.uploadUserDraft({}) + .success(deferred.resolve) + .error(deferred.reject); + return deferred.promise; + }; + + return draft_election; + }); diff --git a/avAdmin/elections-api-service.js b/avAdmin/elections-api-service.js index 03aeea6c..bcd08328 100644 --- a/avAdmin/elections-api-service.js +++ b/avAdmin/elections-api-service.js @@ -23,6 +23,7 @@ angular.module('avAdmin') Plugins, Authmethod, ConfigService, + AdminProfile, $i18next, $http, $cookies, @@ -36,13 +37,15 @@ angular.module('avAdmin') * as a cookie. */ function getSavedElectionKey() { - if (!$cookies.savedElectionKey) { + var autheventid = Authmethod.getAuthevent(); + var postfix = "_authevent_" + autheventid; + if (!$cookies["savedElectionKey" + postfix]) { /* jshint ignore:start */ - $cookies.savedElectionKey = "savedElectionKey_" + sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0)); + $cookies["savedElectionKey" + postfix] = "savedElectionKey_" + sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0)); /* jshint ignore:end */ } - return $cookies.savedElectionKey; + return $cookies["savedElectionKey" + postfix]; } /** @@ -92,10 +95,12 @@ angular.module('avAdmin') electionsapi.currentElection = el; electionsapi.newElection = !el.id; + Plugins.hook('elections-api-set-current', {election: el , rootScope: $rootScope}); + $rootScope.currentElection = el; if (!$rootScope.watchingElection) { $rootScope.$watch('currentElection', function(newv, oldv) { - Plugins.hook('election-modified', {'old': oldv, 'el': newv}); + Plugins.hook('election-modified', {old: oldv, el: newv, rootScope: $rootScope}); if (!$rootScope.currentElection.id) { localSaveElection($rootScope.currentElection); } @@ -151,7 +156,9 @@ angular.module('avAdmin') // updating census el.census.auth_method = data.events.auth_method; + el.census.has_ballot_boxes = data.events.has_ballot_boxes; el.census.extra_fields = data.events.extra_fields; + el.census.admin_fields = data.events.admin_fields; el.census.census = data.events.census; if(!!data.events.num_successful_logins_allowed || 0 === data.events.num_successful_logins_allowed) { el.num_successful_logins_allowed = data.events.num_successful_logins_allowed; @@ -235,13 +242,17 @@ angular.module('avAdmin') return conf; }; + electionsapi.getCachedEditPerm = function(id) { + return electionsapi.permcache[id]; + }; + electionsapi.getEditPerm = function(id) { var deferred = $q.defer(); var cached = electionsapi.permcache[id]; if (!cached) { Authmethod.getPerm( - "edit|create|create-notreal|register|update|update-share|view|delete|send-auth|send-auth-all|view-results|view-stats|view-voters|view-census|start|stop|tally|calculate-results|publish-results|census-add|census-delete|census-activation", + "edit|create|register|update|update-share|view|delete|send-auth|send-auth-all|view-results|view-stats|view-voters|view-census|start|stop|tally|calculate-results|publish-results|census-add|census-delete|census-activation|add-ballot-boxes|list-ballot-boxes|delete-ballot-boxes|add-tally-sheets|override-tally-sheets|list-tally-sheets|delete-tally-sheets", "AuthEvent", id ) @@ -322,7 +333,6 @@ angular.module('avAdmin') }; electionsapi.templateEl = function() { - function getShareTextDefault() { var ret = angular.copy(ConfigService.share_social.default); if(!!ret) { @@ -334,54 +344,56 @@ angular.module('avAdmin') return ret; } - var el = { - title: $i18next('avAdmin.sidebar.newel'), - description: "", - start_date: "2015-01-27T16:00:00.001", - end_date: "2015-01-27T16:00:00.001", - authorities: ConfigService.authorities, - director: ConfigService.director, - presentation: { - theme: 'default', - share_text: getShareTextDefault(), - urls: [], - theme_css: '' - }, - layout: 'simple', - real: false, - num_successful_logins_allowed: 0, - census: { - voters: [], - auth_method: 'email', - census:'close', - extra_fields: [ ], - config: { - "msg": $i18next('avAdmin.auth.emaildef'), - "subject": $i18next('avAdmin.auth.emailsubdef', - {name: ConfigService.organization.orgName}), - "authentication-action": { - "mode": "vote", - "mode-config": { - "url": "" - } - }, - "registration-action": { - "mode": "vote", - "mode-config": null - } - } - }, - questions: [], - extra_data: {} - }; + function evalElectionTemplate() { + /* jshint ignore:start */ + return eval("(function(){ return" + ConfigService.electionTemplate +";})()"); + /* jshint ignore:end */ + } + var el = (angular.isString(ConfigService.electionTemplate)? evalElectionTemplate(): ConfigService.electionTemplate); + Plugins.hook('elections-api-template-el', {'el': el}); return el; }; electionsapi.templateQ = function(title) { var q = { "answer_total_votes_percentage": "over-total-valid-votes", - "answers": [], - "description": "", + "answers": [ + { + "category": "", + "details": "This is an option with a simple example description.", + "id": 0, + "sort_order": 0, + "text": "Example option 1", + "urls": [ + { + "title": "URL", + "url": "" + }, + { + "title": "Image URL", + "url": "" + } + ] + }, + { + "category": "", + "details": "An option can contain a description. You can add simple html like bold or links to websites. You can also set an image url below, but be sure it's HTTPS or else it won't load.\n\n

    You need to use two br element for new paragraphs.", + "id": 1, + "sort_order": 1, + "text": "Example option 2", + "urls": [ + { + "title": "URL", + "url": "https://nvotes.com" + }, + { + "title": "Image URL", + "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/The_Fabs.JPG/220px-The_Fabs.JPG" + } + ] + } + ], + "description": "This is the description of this question. You can have multiple questions. You can add simple html like bold or links to websites.\n\n

    You need to use two br element for new paragraphs.", "layout": "accordion", "max": 1, "min": 1, diff --git a/avAdmin/int-field-directive/int-field-directive.js b/avAdmin/int-field-directive/int-field-directive.js new file mode 100644 index 00000000..7c38767c --- /dev/null +++ b/avAdmin/int-field-directive/int-field-directive.js @@ -0,0 +1,42 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2017 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avAdmin') + .directive('avIntField', function() { + function link(scope, element, attrs) { + scope.$watch( + 'intData', + function (newVal, oldVal) { + var parsed = parseInt(newVal); + if (_.isNaN(parsed)) { + parsed = newVal; + } + if (parsed !== newVal) { + scope.intData = parsed; + } + }); + + } // link + + return { + scope: { + intData: '=' + }, + restrict: 'AEC', + link: link + }; + }); \ No newline at end of file diff --git a/avAdmin/must-extra-fields-service.js b/avAdmin/must-extra-fields-service.js index 444afcb0..ad5561bd 100644 --- a/avAdmin/must-extra-fields-service.js +++ b/avAdmin/must-extra-fields-service.js @@ -20,23 +20,23 @@ angular.module('avAdmin') return function (el) { var ef = el.census.extra_fields; - var name = 'email'; - var must = {}; + var names = ['email']; + var must = null; if (el.census.auth_method === 'email') { - name = 'email'; - must = { + names = ['email']; + must = [{ "must": true, "name": "email", - "type": "text", + "type": "email", "required": true, "min": 2, "max": 200, "required_on_authentication": true - }; - } else if (el.census.auth_method === 'sms') { - name = 'tlf'; - must = { + }]; + } else if (el.census.auth_method === 'sms' || el.census.auth_method === 'sms-otp') { + names = ['tlf']; + must = [{ "must": true, "name": "tlf", "type": "tlf", @@ -44,10 +44,10 @@ angular.module('avAdmin') "min": 2, "max": 200, "required_on_authentication": true - }; + }]; } else if (el.census.auth_method === 'dnie') { - name = 'dni'; - must = { + names = ['dni']; + must = [{ "must": true, "name": "dni", "type": "text", @@ -55,21 +55,77 @@ angular.module('avAdmin') "min": 2, "max": 200, "required_on_authentication": true - }; + }]; + } else if (el.census.auth_method === 'user-and-password') { + names = ['username', 'password']; + must = [{ + "must": true, + "name": "username", + "type": "text", + "required": true, + "min": 3, + "max": 200, + "required_on_authentication": true + }, + { + "must": true, + "name": "password", + "type": "password", + "required": true, + "min": 3, + "max": 200, + "required_on_authentication": true + }]; + } else if (el.census.auth_method === 'email-and-password') { + names = ['email', 'password']; + must = [{ + "must": true, + "name": "email", + "type": "email", + "required": true, + "min": 2, + "max": 200, + "required_on_authentication": true + }, + { + "must": true, + "name": "password", + "type": "password", + "required": true, + "min": 3, + "max": 200, + "required_on_authentication": true + }]; + } + + // the authmethod doesn't have a required field so we do nothing here + if (must === null) { + return; } var found = false; ef.forEach(function(e) { - if (e.name === name) { + if (_.find(names, function(n) { return e.name === n; })) { found = true; e.must = true; + if ('email' === e.name) { + e.type = 'email'; + } else if ('tlf' === e.name) { + e.type = 'tlf'; + } else if ('dni' === e.name) { + e.type = 'text'; + } else if ('username' === e.name) { + e.type = 'text'; + } else if ('password' === e.name) { + e.type = 'password'; + } } else { e.must = false; } }); if (!found) { - ef.push(must); + _.each(must, function(m) { ef.push(m); }); } }; }); diff --git a/avAdmin/next-button-service.js b/avAdmin/next-button-service.js new file mode 100644 index 00000000..944b06b7 --- /dev/null +++ b/avAdmin/next-button-service.js @@ -0,0 +1,33 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2017 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avAdmin') + .factory('NextButtonService', function($state) { + var nextButtonService = {states: []}; + + nextButtonService.setStates = function (states) { + nextButtonService.states = states; + }; + + nextButtonService.goNext = function (params) { + var present_index = nextButtonService.states.indexOf($state.current.name); + var next_state = nextButtonService.states[(present_index + 1) % nextButtonService.states.length]; + $state.go(next_state, params); + }; + + return nextButtonService; + }); \ No newline at end of file diff --git a/avAdmin/onboarding-tour-service.js b/avAdmin/onboarding-tour-service.js new file mode 100644 index 00000000..8e6bb2e8 --- /dev/null +++ b/avAdmin/onboarding-tour-service.js @@ -0,0 +1,221 @@ +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avAdmin') + .factory( + 'OnboardingTourService', + function($i18next, $window) + { + return function (el) + { + var hopscotch = $window.hopscotch; + var autolaunchTour = { + state: null, + tour: null + }; + + function stateCallback(jQueryEvent, angularEvent, toState, toParams, fromState, fromParams) + { + console.log("stateCallBack, state=" + toState.name); + if (!autolaunchTour.tour || !toState || !toState.name || toState.name !== autolaunchTour.state) + { + console.log("stateCallBack, ignoring"); + return; + } + $($window).off("angular-state-change-success", stateCallback); + console.log("stateCallBack, launching tour"); + var nextTour = autolaunchTour.tour; + autolaunchTour.tour = autolaunchTour.state = null; + setTimeout( + function () { hopscotch.startTour(nextTour); }, + 300); + } + $($window).on("angular-state-change-success", stateCallback); + + function closeTour() + { + $(".onboarding-focus").removeClass("onboarding-focus"); + $("#onboarding-overlay").fadeOut({complete: function() { $(this).remove(); }}); + } + + function onStartTour() + { + console.log("onStart"); + $("#onboarding-css").remove(); + $('').appendTo("body"); + $("#onboarding-overlay").remove(); + $('
    ').hide().appendTo("body").fadeIn(); + } + + var helpTour = { + id: "help-hopscotch", + steps: [ + { + title: $i18next("avAdmin.onboarding.help_tour.step0_help_title"), + content: $i18next("avAdmin.onboarding.help_tour.step0_help_content"), + target: "#navbar-collapse-1 .help-dropdown a", + highlightTarget: "#navbar-collapse-1 .help-dropdown a", + placement: "bottom" + }, + { + title: $i18next("avAdmin.onboarding.help_tour.step1_chat_title"), + content: $i18next("avAdmin.onboarding.help_tour.step1_chat_content"), + target: ".zsiq_cnt", + highlightTarget: ".zsiq_cnt", + placement: "top", + xOffset: -50, + arrowOffset: "center" + } + ], + onStart: onStartTour, + onStop: closeTour, + onEnd: closeTour, + onClose: closeTour + }; + + var dashboardTour = { + id: "dashboard-hopscotch", + steps: [ + { + title: $i18next("avAdmin.onboarding.dashboard_tour.step2_status_title"), + content: $i18next("avAdmin.onboarding.dashboard_tour.step2_status_content"), + target: ".statusbar.row.text-center", + highlightTarget: ".statusbar.row.text-center", + placement: "bottom" + }, + { + title: $i18next("avAdmin.onboarding.dashboard_tour.step3_start_title"), + content: $i18next("avAdmin.onboarding.dashboard_tour.step3_start_content"), + target: "button.actionbtn.btn.election-status-action-2", + highlightTarget: "button.actionbtn.btn.election-status-action-2", + placement: "top", + nextOnTargetClick: true, + showNextButton: false + } + ], + onStart: onStartTour, + onStop: function() + { + closeTour(); + setTimeout(function () { hopscotch.startTour(helpTour); }, 300); + }, + onEnd: closeTour, + onClose: function() + { + closeTour(); + setTimeout(function () { hopscotch.startTour(helpTour); }, 300); + } + }; + + var tour = { + id: "hello-hopscotch", + steps: [ + { + title: $i18next("avAdmin.onboarding.hello_tour.step0_create_title"), + content: $i18next("avAdmin.onboarding.hello_tour.step0_create_content"), + target: "a[ui-sref='admin.new()']", + highlightTarget: "a[ui-sref='admin.new()']", + placement: "right", + width: 650, + nextOnTargetClick: true, + showNextButton: false + }, + { + title: $i18next("avAdmin.onboarding.hello_tour.step1_change_title"), + content: $i18next("avAdmin.onboarding.hello_tour.step1_change_content"), + target: "[title='avAdmin.basic.title.label']", + highlightTarget: "[title='avAdmin.basic.title.label']", + placement: "bottom", + delay: 300 + }, + { + title: $i18next("avAdmin.onboarding.hello_tour.step2_sidebar_title"), + content: $i18next("avAdmin.onboarding.hello_tour.step2_sidebar_content"), + target: "ul[ng-if='current']", + highlightTarget: "ul[ng-if='current']", + placement: "right" + }, + { + title: $i18next("avAdmin.onboarding.hello_tour.step3_review_title"), + content: $i18next("avAdmin.onboarding.hello_tour.step3_review_content"), + target: "a[href='/admin/create/']", + highlightTarget: "a[href='/admin/create/']", + placement: "right", + nextOnTargetClick: true, + showNextButton: false + }, + { + title: $i18next("avAdmin.onboarding.hello_tour.step4_create2_title"), + content: $i18next("avAdmin.onboarding.hello_tour.step4_create2_content"), + target: "button[ng-click='createElections()']", + highlightTarget: "button[ng-click='createElections()']", + placement: "top", + nextOnTargetClick: true, + showNextButton: false, + delay: 300 + } + ], + onStart: onStartTour, + onStop: closeTour, + onEnd: function() + { + closeTour(); + autolaunchTour.state = "admin.dashboard"; + autolaunchTour.tour = dashboardTour; + }, + onClose: function() + { + closeTour(); + setTimeout(function () { hopscotch.startTour(helpTour); }, 300); + } + }; + + // Start the tour! + hopscotch.listen( + "show", + function() + { + console.log("hopscotch::show to highlight focus"); + $(".onboarding-focus").removeClass("onboarding-focus"); + $(hopscotch.getCurrTour().steps[hopscotch.getCurrStepNum()].highlightTarget).addClass("onboarding-focus"); + } + ); + hopscotch.startTour(tour); + }; + } + ); diff --git a/avAdmin/send-messages-service.js b/avAdmin/send-messages-service.js index 3be724af..9e47de51 100644 --- a/avAdmin/send-messages-service.js +++ b/avAdmin/send-messages-service.js @@ -113,7 +113,7 @@ angular.module('avAdmin') return false; } - if('sms' === service.election.census.auth_method) { + if('sms' === service.election.census.auth_method || 'sms-otp' === service.election.census.auth_method) { var email_field = getExtraField('email'); if(email_field && 'email' === email_field.type) { return true; @@ -132,6 +132,17 @@ angular.module('avAdmin') * is */ service.sendAuthCodesModal = function() { + var pluginData = { + continue: true, + el: service.election, + ids: service.user_ids, + extra: service.extra + }; + Plugins.hook('send-auth-codes-modal', pluginData); + if (!pluginData.continue) { + return; + } + service.selectable_auth_method = service.authMethodIsSelectable(); service.selected_auth_method = angular.copy(service.election.census.auth_method); // If skip dialog flag is activated, then we jump directly to the diff --git a/bower.json b/bower.json index 2ac08a3d..03fcdfc2 100755 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "agoraGuiAdmin", - "version" : "17.04", + "version" : "103111.8", "main": "index.html", "ignore": [ "**/.*", @@ -8,7 +8,10 @@ "bower_components" ], "dependencies": { - "avCommon": "https://github.com/agoravoting/agora-gui-common.git#next", + "avCommon": "https://github.com/agoravoting/agora-gui-common.git#go-next", "angular-local-storage": "~0.2.3" + }, + "resolutions": { + "intl-tel-input": "^12.1.0" } } diff --git a/img/nVotes_logo_small.png b/img/nVotes_logo_small.png index 75a24fd1edc9790a74747d36f154fe2c1875d8b2..7b704a8ac91afb1a33539ea4cf152c735e1ac53c 100644 GIT binary patch literal 3617 zcmV++4&L#JP)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{01b&rL_t(o!{u0MY+l!KJ!kIw z-uHdnS8)+VTHGnwa;%Mul+aczixy?4DdGez8aF}Vx;2UxO^cvyniMwNrf5^3sOum> zn+B+z#*OPHwiQ_$?aKn0W^r;JH5BK2{ zw1S$NaFWS>T!deR>aeH!Washy$M7p;@=0vsYqSTd3nVIp{A_YKVf{@l+_1qFhqWMg z0?3EO#o3sSf6|xq<5?t`E0j$DH;N(wzhd^u0C6ear&fOfg-75nH)yJXN6g2qD-Rc^ z2&9HUDioJIYtIa|9a+HtLvxC@)#o}0WvVza|I%zCbmQkK_vH~@T+q~Y#v=IP|Ro@$m z40d-Pomzfo-j*#1VWeld0h6U-QpM8HFydK^&TDM=24`MlIh%GTCI?I|LdX{DBm%zD z!?dUV(1rQ_A+)#00Dv2IKKwNRj{xY2diL)Cz%~3W64d~60Syr{Z=wCL_AMKW|ES~a zp^ocLENuu@=DJTC@(&g0kH@{o&!77F(bYb(vhLx>jN#t`4EWaniw};!`rVa+S+eb+ zDmQ@7L0CiJ4nWfvtC@333`6x@U-ji^|I8abJF0DvTeB-2S|5D2R3iP4gHJ9>I70I96oS7Ye>nUKFD zKqbg90S{11GLhhV2sV>)k1L7Ol9GRZexxUM4ei!8YzvscHx2n^P}TvkP|Se@$vl#G zlKd2nnHmcgzhklsD;h{{hmt~;5X@GK|AP8I@O=Nrv^=V?T>!!qSw~_cMMBAIi%w7U zch4<7Jbg`xC&tu~C{fT$D_DPQysv9wrEDoIthiT1o&cDv*8V5PCjNVDtZ(UMShl@! zXTUXoNy;8TXRYED`1V;p=KZs8@jS#HQ;R{6DHK*3@~)r+{LF-`wz09k*l1eV&kUvx zD|i-QP{;}eCROeCEcAy8J)r6zQr~YGJKi(_K+0+ww>n0?LGob$6RP%QReaw@@r-Z% z=b`pEh+#sqge)VlAsh_MriJv~@$qpV0Lr)2XE}kyuLGhS8q*ZLNRH5|J$Qevp>0}7i`$@v(lvEW3H=zMY0CKuAp%@0l0>G zrWO{4GJ@`D0<{FH44h8^k9-8Y=r8rFsHjU4kp}=40O<3rJ=EE8;+o!5QN5=~jQk#m zT~G|Dwa+fZ?Q=aX2Zk<}HvpW=+p^_QN+|D51Nn21)etf#l$8@W z<$zzO$YvoPldQ7$(A9N(vGa!WA>09IKvkP!vys2=ZaXr(B%bY6X(c{|PmwGov1h4j zQfu|lloxsVx`y1|9>ciT4AuPrW(j1%$3|dFV5!?kd-EJ3TU+O+BZE!8Z(mT= z34(VxL_JLcXtOWev%|CIAHhXML+jyIYUmc(813jeDLw z0Yxu>AiynUHAy)D;Hg9um<803wxV+D?u}PKx?&gXXy=?4{g&tZUtRFMXRf|f#e{_p zEzh{6X2rV%x^s()MU#@l0W-49r?>UcrHZPV!i z>W3;k1GL7M?hyzgiPZH39Sb|)iQfspUoD2+r)NK`+L7`$fSm-_I&jtkc;}XeD%uUC zNdzz^;!jq6rm=K2f9q)mgfjpLYA!6Y09^EZJK#DzqhJZe)JrhT@>jes!qr z$Q%F!L*XKj_W*DKw8dW;0gx$h{+JfgRSSq3AhZl12nNNm0RZS}IWSyNyZ8GdCV}Km(CjAl-hiOrNlAad zs`jDxKzk+{jlSExFmMU&?Xhdh#ks{ozR`<0JJp+{*-gg5W{|s5lHAQhz*|7QV*>6r z5)}%}SZGuC$+WTSkhGQxa50M1Xe@eD!K8|XPywZi-uhny6)t9y0#FB_t_}nNMHis3 zsvWB4N^i9tCl8z|-@g0nYTRm&TZK{z>RM7h4Wkc|&QvgD+A4#^FU{Yz{v`*u!TL{~ zij>tf{=%s9B>=?++*MRm-xAD9P9nLBWQwXDwJ{u7uBa|g+%W?4m%&bQtebc<%%ZSs zsK!-t64s90d^G?-v#)$;YO%p$*pGXE9rwMCo2E523z$OxQs(MBbL2w+ALVS?^lEY_ zKV3!F1ZXP6m})5R7r}0|a(7}95^oC-09392fx@(rST7=V%umcwYN60Mh^q5P*SQ`i6cGMVppw;r8}&riOtu&s+RVD@bTVo~1lgenIBW(JJ& z8$z=eihxyn)<$Q}-w?#EgHhj$y$;nj0wGWy4u+EVpuT>k3TA1RQ$yfUsOAtl;QRVo zcU$c0?{qh5#zGmrOzc6SgjPCt!Sgy4+M)`furZvR{<+f9(%?!=R9?I9USk4Jia1Y+ zaUOBtgs-YTWr6^WLiv4)e5Ir&zi9aeQHBCg1fd|P*1>JGd7VF>stIR7DW}L@DnkmL zg0-hQF}tb~I)5hmUPZ0@LnBxNu+ljER)v?88?S9_@?zdtLIS`DGT@pTQhpm`4HXl< z>I;!Mdt}KEJoGd-$E)i04XQN}5UBumm)AB7c-ET~&#@D;)5BvOCuVwE4-HrD*!vQN zcS5;AME^+EREA2@OWulk{sbkefj~IXG46IF^1D>lsOpfl_7yvZU-pw2My!kXgu0nv zwJY|~vg*A*rZ+bN0Dd5t=9)kw$r^=H6&<$~h=G=-iPXA%2SDr*N{&Jmit|3S1+A^| zkCUUm-|!NRID|ey%AF4RO(A|W7>iR&_rG3E(@)T-#W-JtuC>$0Cr z@Bw5KECzIqLNEBf|LojU{JH+F!&jwl*V_lj%4;9|k(j_b5ci6aCjw&XHIUkm#Ys0` zfJy*qR<+5u`fnz?u(BG&9RNQ9{iCD3=Wp2LvJ0v*MOXt;V9{dL9-kQOUQKIjN_x?V z6Li)=kq%(gs_vif9sB9@biem;K#pJRiiYyj-*=JF2~a_%iZG2}F3B7U5>zpxs;~R% z{p0B9#0&jxO+zc~HIWh=%Lt{7fDD5OQy3&9VNtKz)1wRH?_+S#PEYrHWBn=T)AADD zBjHh)Dl};%*O1I5nG9muTH9n*pBeG}mxkUxG`alVV=Yw!9|r!*mq!(;;K z?Je=AOY|oDqDx2?t3rU<$P#-!+Dow>ztV) zGKf+HUP9!4HH;Y_n)xN7rc24JJ?&7;=Z*o#taRo^luyQ%PXTVWqRJz)qkl z@GIYQcUg7@0KbrGPWpk2T+No43NT< zeRf9T<5oM{u%7JNFwaV77g}AL7OYscDKT*_-81Ez$yDQCENU+RPPJDs$O& zYn*BEby=R?(p05mo^aIeIa&A!3 zF9Q}ZJ;GB4PNN)Xs*L^g_|eBl zXet46r|;!{*|0C&gP-rG-Og-RxE~l(kvmKITn->-ukk?16?U<(O4utPU z@_Yc)l8(y>BZ2`00)JNY+yga69oDE0lq;PR!+qP8bxGd+nSz5Qw);M;txFfZUnrxKTvc{d|yX*7C+yQ z)xGhL6n+$ee57WO_H<+r+i4du4g$9TXMuM?xBH&E%=g@7z#W8%L zovPd&83W*LLH53Uc<)%!&ft6Q2Y_d#o#`m+fb%L+HR_M2_D~Ns0Ea6wWLviLE#NBPEJm=sc687Sbshx4kEy#i zz?(s3yg&Y#!Na{(pcI=5fNNS)J&T454O;1rT*5dD{$Hh}q^DQD?mA@>SE za!qzA1YZG65!qcF*zUwrzpv1Do^r9wjnR|~rn zWJuFOVqV;7JFBYbY~cN9>QyE2`;OFlh6?){pj)=H097Z(bDAnY02mz`sl9RECJg;$ zLQj7kHU6%~Lkvm;Os8(N0Ju+-?UtSKRpin47a#8(e#kuq{A>a@Ks8ZbLu2|n4m#9; z(lkvVe#@Fs`48a21_6-Z6!5>XnQ}#RU7e>s49pSuZ84wQ(VliF4TKl>_`!XMp&f)3 zevO(<=mjPl1YjnO54b|Lp{r1V&7o`|I^YF0v@+Qs5q%W6E;c?V$iG^)^SWhcMnvRw z;ByhbQQ$$}b6ZPZ-|GNYcLsmJEuiu+6UA^128)69qPlzTYn>xjI`e%im1zNJx1E|H z5m*ag*_rPN+6i*Hin#%%)H$HW zN*dkY9Q1O}PX0*JcA9`|C(qAgycB&a1T4CN@K)F8_$)IhPd*oJ)<#*8aYX+SbT?s^ z_&q_s1#(-pT3PCQ?pFXhSBLeCKj31e9Ptae&j|cA@C>1+%pi<1O08T_zJYMO!_Hn) z^l~&Uw>Ks#8pisFnh>4==;+$W(2+e1<#TkTolgX+vk22cLG`#+f_JU7gFjiBHJxHq zh2TYCWemjimYw}$2<`wz5xsXf-}`FP&QkIkZL9&aV|QOb&H!B3o#DiQ8(Yv8;QE*n z69@;M%0E`^uzMKtTr4|t1EPPhta zr#1r0mFf$JV%SyX)!2R&^vY}3g&`&xrMm#_-64iraocHcGTYNV zV%eE5S}EuL_H^dPXnoYr_q|mtxVuY*TpEl&WpaN|S+HcyCc{9O0W6CxnTbTpM2c8* zM?q^3S~wIfiWy8itexGBZHPL_+r+cF@I*JrSOA;-L%*n6>5H6I%@cZNjS`mKv)K1s zI#*{ID)iP=_Ap`n0%82#@zPU!Uu*4(w_D_T8jTW&$dswgCt>Y4_sP8%;ajko+7sA=~c7(WAUshVo6NNs2;8|H3Qml9R#0=;L>oO}S_#fJWx z&YP?mMSq^`a(-kQj0R{2NYeLmUyat^wCv1o!eWw$|LW%Ryp*&v{|>w?A~Qi(pzP0^7+gL^NiRiF}vfim@0M3cZj6}|pLo%Y2WhX5y@d@QI0)4qrh zxdaK&lw~`|5dB42nx0459AW<-;NGI=-v0UvM}n!`Bu{uNTOt`uV67PXWPlz4ehM1T zw#a_$W8Vh;f$+jki|~I$V28jiRCl7>37P;o5Wx1*NK37`S!?d7GCNunMb4DGsSjF5cXn0~=;sk#4>Y5Ui11&5 z%3(pD^F8;X{~)sL%%g-=v7yLd$;;jT2dxoN?eXted#kC{mNrHITvcus(WSsNgtvj` zMD>vX<8aB#jaXr&JQc&bbwXv%BH(Ml3XlZo??pL*=n;@1L{1g+xw8<4N diff --git a/index.html b/index.html index ae62c782..3e045bba 100755 --- a/index.html +++ b/index.html @@ -6,33 +6,38 @@ + - + - + - - + + + - + + + + - + - + @@ -44,20 +49,38 @@ + + + + + + + + + + + + + + + + + + @@ -89,13 +112,19 @@ + + + + + + @@ -106,7 +135,7 @@ - + @@ -119,7 +148,7 @@ For security reasons, your browser is unsupported. Please use a newer web browser (if you are using Internet Explorer, use version 9 or newer).
  • -
    +
    diff --git a/locales/ca.json b/locales/ca.json index 4d14e66b..3c633352 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -29,10 +29,55 @@ "votings": {"plurality-at-large": "Plurality at large", "borda-nauru": "Nauru's Borda Count or Borda Dowdall (1/n)", "borda": "Borda Count (traditional)", "pairwise-beta": "Pairwise (beta-distribution)", "desborda": "DesBorda"} }, "avAdmin": { + "profile": { + "title": "Perfil", + "save": "Guardar cambios" + }, + "onboarding": { + "help_tour": { + "step0_help_title": "Ayuda", + "step0_help_content": "Si necesitas más ayuda, haz clic aquí para obtenerla. ¡También puedes iniciar de nuevo el tour desde aquí!", + + "step1_chat_title": "Chatea con nosotros", + "step1_chat_content": "Si tienes alguna pregunta,, estaremos encantados de responderla rápidamente a través del chat." + }, + "dashboard_tour": { + "step0_test_title": "Votación de prueba", + "step0_test_content": "Has creado una votación de prueba. Las elecciones de prueba tienen un tamaño de censo limitado y una votación real sólo se puede crear a partir de una votación de prueba recontada con éxito.", + + "step1_real_title": "Votación real", + "step1_real_content": "Una vez que se realize el recuento de ésta votación, podras crear una votación real haciendo clic aquí.", + + "step2_status_title": "Estado de la votación", + "step2_status_content": "Esta sección muestra el estado de la votación y en ella siempre aparece un botón para pasar la votación al siguiente estado.", + + "step3_start_title": "Comienza la votación", + "step3_start_content": "Haz clic en este botón apra comenzar la votación y enviar el email de autenticación a todo el censo." + }, + "hello_tour": { + "step0_create_title": "Crea la votación", + "step0_create_content": "Comienza a crear una votación de prueba haciendo clic en el botón Nueva votación.

    O visualiza nuestro video de introducción:
    ", + + "step1_change_title": "Cambiar el título", + "step1_change_content": "Ahora puedes editar todos los detalles de la elección, por ejemplo podrías editar el título de la votación haciendo clic aquí.", + + "step2_sidebar_title": "Barra lateral", + "step2_sidebar_content": "Podrías personalizar la votación haciendo clic en éstas secciones.", + + "step3_review_title": "Revisa la votación antes de crearla", + "step3_review_content": "Una vez hayas terminado configurando la votación, haz clic en el botón Crear Votación para revisar y crear la votación.", + + "step4_create2_title": "Crear la votación", + "step4_create2_content": "Haz clic en éste botón para crear la votación.

    Ten en cuenta que la votación no se creará hasta que hagas clic en éste botón, y por razones de seguridad no se podrá editar posteriormente." + } + }, "show_points_policy": { "label": "Mostra punts", "text": "Mostra els punts que la papereta dóna a cada opció seleccionada" }, + "setting": { + "helpTooltip": "Clickea para expandir/colapsar texto de ayuda sobre esta configuración" + }, "shuffling_policy": { "categories_label": "Ramdomize categories", "categories_label_text": "Check to randomize", @@ -44,11 +89,6 @@ "shuffle-all": "Shuffle all options. Each voter will see the options for this question in a different order in the voting booth. If there are different categories, they will also be randomized.", "shuffle-some": "Shuffle the options of a selected list of categories." }, - "notes": { - "noteTag": "Nota", - "thisisATestElection": "Esta es una votación de prueba. Por motivos de seguridad, sólo pueden crearse votaciones reales a partir de votaciones de prueba exitosas.", - "thisisATestElectionPopover": "Esta es una votación de prueba. Por motivos de seguridad, sólo pueden crearse votaciones reales a partir de votaciones de prueba exitosas. Para ello, debes completar el proceso de la votación hasta el paso de publicar resultados" - }, "results": { "downloadData": "Descargar datos", "downloadBallotsCsv": "Descargar votos como hoja de cálculo (formato CSV)", @@ -66,6 +106,14 @@ "logout": "salir" }, "learnMore": "Learn more..", + "adminFields": { + "title": "Campos admin", + "intro": "Campos admin", + "maxError": "__value__ es mayor de __max__", + "minError": "__value__ es menor de __min__", + "textError": "error en texto __value__", + "numberError": "__value__ ha de ser un número" + }, "sidebar": { "elections": "Votaciones", "newel": "Nueva votació", @@ -83,7 +131,8 @@ "myAccount": "Mi cuenta", "billinfo": "Información de pago", "billhistory": "Historial de pagos", - "buy": "Comprar créditos" + "buy": "Comprar créditos", + "adminFields": "Admin fields" }, "elections": { "filter": "Filtrar...", @@ -144,6 +193,9 @@ "createHeader": "Create election", "createBody": "To create the election, its configuration is sent to the election authorities and they create election public encryption keys. After that, the election is no longer editable. Before doing this, please check that the configuration is correct (for example taking a look at the demo voting booth). If you need to change the election after this, you will have to create a new one.", "confirmCreateButton": "Confirm and CREATE election", + "createRealHeader": "Crear votación real", + "createRealBody": "Esta es una votación de prueba. Por motivos de seguridad, sólo pueden crearse votaciones reales a partir de votaciones de prueba exitosas. Para ello, debes completar el proceso de la votación hasta el paso de publicar resultados.", + "confirmCreateRealButton": "Confirmar y CREAR votación real", "calculateResultsHeader": "Calcular resultados", "calculateResultsBody": "El recuento se ha realizado y el próximo paso es el cálculo de resultados. Aunque el recuento sólo se permite realizar una vez, el cálculo de resultados se puede realizar tantas veces como sea necesario antes de publicarlos.", "calculateResultsButton": "Confirmar y CALCULAR resultados", @@ -249,6 +301,12 @@ "census": {"open": "Registro abierto", "close": "Registro cerrado"}, "fields": {"label": "Campos extra", "placeholder": "Campos extra del votante"}, "censusadd": "Voters added to the census", + + "modals": { + "csvLoadingHeader": "Uploading CSV to census", + "csvLoadingBody": "Please click on the start upload button to add the users from the CSV to the census. Please wait until the process is finished and don't close this window.", + "csvLoadingStartButton": "Start upload NOW" + }, "actionsDropdown": "Actions", "exportCensusAction": "Export all census to CSV", "removeCensusAction": "Remove __num__ selected people", diff --git a/locales/en.json b/locales/en.json index fc3eda78..7e115ba2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -29,10 +29,167 @@ "votings": {"plurality-at-large": "Plurality at large", "borda-nauru": "Nauru's Borda Count or Borda Dowdall (1/n)", "borda": "Borda Count (traditional)", "pairwise-beta": "Pairwise (beta-distribution)", "desborda": "DesBorda"} }, "avAdmin": { + "profile": { + "title": "Profile", + "save": "Save changes" + }, + "ballotBox": { + "title": "Ballot Boxes", + "intro": "List of ballot boxes and their tally sheets.", + "manage": "Search and filter looking for ballot boxes", + "filter": "Search here..", + "tableColumn": { + "idColumnHeader": "Id", + "nameColumnHeader": "Title", + "actionsColumnHeader": "Actions", + "lastUpdateColumnHeader": "Last Update", + "versionColumnHeader": "Number of versions", + "usernameColumnHeader": "Username" + }, + "modals": { + "createBallotBox": { + "header": "Create ballot boxes", + "body": "Add one ballot box name per line. It won't create any if any of them already exist.", + "holder": "Ballot box name 1\nBallot box name 2..", + "button": "Confirm and CREATE ballot boxes" + }, + "deleteBallotBox": { + "header": "Confirm to DELETE ballot box", + "body": "Please to confirm that you want to delete the ballot box __name__ and all the tally sheets related to it, write 'DELETE' below:", + "holder": "Write DELETE here to confirm..", + "ok": "Confirm and DELETE ballot box" + }, + "deleteTallySheet": { + "header": "Confirm to DELETE tally sheet", + "body": "Please to confirm that you want to delete the tally sheet with id __id__ for ballot box __name__, write 'DELETE' below:", + "holder": "Write DELETE here to confirm..", + "ok": "Confirm and DELETE tally sheet" + }, + "checkingBallotBox": { + "header": "Creating ballot boxes", + "checkingExisting": "Checking if any ballot box requested already exist..", + "hasExisting": "No ballot box was created, because the following already exist: ", + "errorCheckingExisting": "There was an error checking existing ballot boxes:", + "creatingBoxes": "Creating the ballot boxes: ", + "errorCreatingBoxes": "Error while creating some ballot boxes: ", + "success": "All the ballot boxes were created successfully.", + "button": "Close" + }, + "writeTallySheet": { + "step0header": "Step 1 of 3: Register tally sheet", + "step1header": "Step 2 of 3: Review and send the tally sheet", + "step2header": "Step 3 of 3: Successfully added tally sheet", + "reviewStep": "Please review carefully the tally sheet before sending it", + "ballotBoxLabel": "Ballot box name:", + "registeredVotesLabel": "Number of voters registered:", + "voteCountLabel": "Number of votes:", + "electionIdLabel": "Election id:", + "electionTitleLabel": "Election title:", + "observationsLabel": "Observaciones:", + "continue": "Continue and review", + "blankVotesLabel": "Blank votes:", + "nullVotesLabel": "Null votes:", + "mismatchedNumbersError": "The numbers don't add up or are invalid, plese review them", + "mismatchTotalCount": "The registered number of votes and the entered number of votes differ", + "send": "Confirm and send tally sheet", + "backAndEdit": "Go back and edit", + "success": "Congratulations, you successfully added the tally sheet!", + "okClose": "Close" + }, + "viewTallySheet": { + "header": "View tally sheet", + "edit": "Edit and change tally sheet", + "tallySheetIdLabel": "Tally sheet id:" + } + }, + "viewTallySheetAction": "View tally sheet", + "writeTallySheetAction": "Write tally sheet..", + "deleteTallySheetAction": "Delete tally sheet..", + "deleteBallotBoxAction": "Delete ballot box.." + }, + "activityLog": { + "title": "Activity Log", + "intro": "List of actions executed in the election.", + "manage": "Search and filter the activity log", + "filter": "Search here..", + "tableColumn": { + "idColumnHeader": "Id", + "executerIdColumnHeader": "Executer Id", + "creationDateColumnHeader": "Created at", + "receiverIdColumnHeader": "Receiver Id", + "actionColumnHeader": "Description", + "commentColumnHeader": "Comment" + }, + "action": { + "user": { + "activate": "__executer_username__ (__executer_id__) activated user __receiver_username__ (__receiver_id__)", + "deactivate": "__executer_username__ (__executer_id__) deactivated user __receiver_username__ (__receiver_id__)", + "successful-login": "__executer_username__ (__executer_id__) logged in successfully", + "send-auth": "__executer_username__ (__executer_id__) sent authentication codes to __receiver_username__ (__receiver_id__)", + "resend-authcode": "There was a request to re-send authentication codes to __receiver_username__ (__receiver_id__)", + "added-to-census": "__executer_username__ (__executer_id__) added to census to __receiver_username__ (__receiver_id__)" + }, + "authevent": { + "create": "__executer_username__ (__executer_id__) created election __event_id__", + "edit": "__executer_username__ (__executer_id__) edited election __event_id__", + "start": "__executer_username__ (__executer_id__) started election __event_id__", + "stop": "__executer_username__ (__executer_id__) stopped election __event_id__" + }, + "ballotBox": { + "create": "__executer_username__ (__executer_id__) created ballot box __ballot_box_name__ in election __event_id__", + "delete": "__executer_username__ (__executer_id__) deleted ballot box __ballot_box_name__ in election __event_id__" + }, + "tallySheet": { + "create": "__executer_username__ (__executer_id__) uploaded a tally sheet for ballot box __ballot_box_name__ in election __event_id__", + "delete": "__executer_username__ (__executer_id__) deleted a tally sheet for ballot box __ballot_box_name__ in election __event_id__" + } + } + }, + "onboarding": { + "help_tour": { + "step0_help_title": "Help", + "step0_help_content": "If you need any further help, you can click here to get it. You can launch again the tour there too!", + + "step1_chat_title": "Chat with us", + "step1_chat_content": "If you have any question, we'll be glad to answer you quickly through the chat." + }, + "dashboard_tour": { + "step0_test_title": "Test election", + "step0_test_content": "You have created a test election. Test elections have limited census size and a real election can only be created out of a successfully tallied test election.", + + "step1_real_title": "Real election", + "step1_real_content": "Once this election is tallied, you will be able to create a real election clicking here.", + + "step2_status_title": "Election status", + "step2_status_content": "This section shows you the state of the election and a button to move election to the next status.", + + "step3_start_title": "Start the election", + "step3_start_content": "Click this button to start the election and send an authentication email to all census." + }, + "hello_tour": { + "step0_create_title": "Create an election", + "step0_create_content": "Start creating a test election clicking in the New Election button.

    Or watch the intro video:
    ", + + "step1_change_title": "Change the title", + "step1_change_content": "Now you can edit all the details of the election, for example you could edit the election's title clicking title here.", + + "step2_sidebar_title": "Sidebar", + "step2_sidebar_content": "You can personalize the election clicking through these sections.", + + "step3_review_title": "Review election to create it", + "step3_review_content": "Once you are done configuring the election, click the Create Election section to review and then create the election.", + + "step4_create2_title": "Create the election", + "step4_create2_content": "Click this button to create the election.

    Note that the election won't be created until you do so, and for security reasons it won't be editable afterwards." + } + }, "show_points_policy": { "label": "Show points", "text": "Show the points that the ballot is giving to each selected option" }, + "setting": { + "helpTooltip": "Click to expand/collapse help text about this setting" + }, "shuffling_policy": { "categories_label": "Ramdomize categories", "categories_label_text": "Check to randomize", @@ -44,11 +201,6 @@ "shuffle-all": "Shuffle all options. Each voter will see the options for this question in a different order in the voting booth. If there are different categories, they will also be randomized.", "shuffle-some": "Shuffle the options of a selected list of categories." }, - "notes": { - "noteTag": "Note", - "thisisATestElection": "This is a test election. For security reasons, real elections can only be created out of successful test elections.", - "thisisATestElectionPopover": "This is a test election. For security reasons, real elections can only be created out of successful test elections. You must complete the election process up to the results publish step." - }, "results": { "downloadData": "Download data", "downloadBallotsCsv": "Download Ballots Spreadsheet (CSV format)", @@ -66,12 +218,23 @@ "logout": "logout" }, "learnMore": "Learn more..", + "adminFields": { + "title": "Admin fields", + "intro": "Admin fields", + "maxError": "__value__ is greater than __max__", + "minError": "__value__ is lower than __min__", + "textError": "error on text __value__", + "emailError": "error on email __value__", + "numberError": "__value__ must be a number", + "requiredError": "Missing value on required field" + }, "sidebar": { "elections": "Elections", "newel": "New election", "import": "Import", "currentel": "Current election", "dashboard": "Dashboard", + "activityLog": "Activity Log", "basic": "Basic details", "questions": "Questions", "censusConfig": "Census Configuration", @@ -83,14 +246,25 @@ "myAccount": "My account", "billinfo": "Billing information", "billhistory": "Billing history", - "buy": "Buy credits" + "ballotBox": "Ballot Boxes", + "buy": "Buy credits", + "adminFields": "Admin fields" }, "elections": { "filter": "Filter...", "test": "test", + "draft": "draft", "real": "real", "status": "Status", - "participation": "Participation" + "participation": "Participation", + "useDraftModal": { + "title": "Do you want to load the election draft?", + "body": "This action will load the election draft with title '__title__'. This action will load this draft and subsequent modifications of its configuration will rewrite the saved draft. Do you wish to proceed?" + }, + "eraseDraftModal": { + "title": "Do you want to erase the election draft?", + "body": "This action will erase the election draft with title '__title__'. This action cannot be reverted. Do you wish to proceed?" + } }, "action": { "create": "create", @@ -145,6 +319,9 @@ "createHeader": "Create election", "createBody": "To create the election, its configuration is sent to the election authorities and they create election encryption public keys. After that, the election is no longer editable. Before doing this, please check that the configuration is correct (for example taking a look at the demo voting booth). If you need to change the election after this, you will have to create a new one.", "confirmCreateButton": "Confirm and CREATE election", + "createRealHeader": "Create real election", + "createRealBody": "This is a test election. For security reasons, real elections can only be created out of successful test elections. You must complete the election process up to the results publish step.", + "confirmCreateRealButton": "Confirm and CREATE real election", "calculateResultsHeader": "Calculate results", "calculateResultsBody": "The election has been tallied and the next step is to calculate the results. Although the election can only be tallied once, the election results can be calculated as many times as you need before publishing them.", "calculateResultsButton": "Confirm and CALCULATE results", @@ -295,6 +472,10 @@ }, "modals": { + "comment": "Comment", + "csvLoadingHeader": "Uploading CSV to census", + "csvLoadingBody": "Please click on the start upload button to add the users from the CSV to the census. Please wait until the process is finished and don't close this window.", + "csvLoadingStartButton": "Start upload NOW", "addCsvHeader": "Add CSV to census", "addCsvBody": "Here you can add many people at once. The format is CSV, one person per line. Use semicolon to split fields, in order. Note that no data validation is done, so it's up to you to insert valid data.", "addCsvTextareaPlaceholder": "one voter per line, keep the field order", @@ -331,6 +512,11 @@ "deactivatedCensusSuccessfully": "Deactivated selected people in census successfully", "sentCodesSuccessfully": "Sent authentication codes successfully", + "activateOneAction": "Activate", + "deactivateOneAction": "Deactivate", + "removeCensusOneAction": "Remove", + "sendAuthCodesOneAction": "Send auth codes", + "voted": "voted", "notVoted": "not voted", "votedColumnHeader": "Voted?", @@ -351,6 +537,7 @@ "codeOption": "code", "textOption": "text", "tlfOption": "tel", + "dateOption": "date", "intOption": "int", "boolOption": "bool", "captchaOption": "captcha", @@ -366,6 +553,7 @@ "fieldHelpLabel": "Help text", "fieldHelpPlaceholder": "Example: Insert both your name and surname", "fieldRequiredLabel": "Required", + "fieldAutofillLabel": "Auto fill", "fieldUniqueLabel": "Unique", "fieldRequiredAuthLabel": "Checked against census in authentication", "fieldMatchCensusOnRegistrationLabel": "Checked against census in registration (only for pre-registration)", @@ -389,7 +577,7 @@ "auth": { "intro": "Authentication is the means used to ensure that an elector is who he sais he is. Here you need to find a balance between security, usability and cost", "auth": "Auth method", - "auths": {"email": "Email links", "sms": "SMS", "dnie": "spanish eDNI"}, + "auths": {"email": "Email links", "sms": "SMS", "dnie": "spanish eDNI", "user-and-password": "User and password"}, "sms": "SMS message", "smstemp": "Template used in the SMS to send to the user. Use __CODE__ to insert the SMS code and __URL__ for the authentication url, or __URL2__ to insert a link that includes the code", "smsdef": "This is your vote code: __CODE__", @@ -463,7 +651,33 @@ "election-question-int-size-min-max": "Election '__eltitle__', question '__qtitle__': the maximum number of candidates that a voter can choose is __value__, but must be bigger or equal than the minimum which is __min__", "election-question-int-size-max-max": "Election '__eltitle__', question '__qtitle__': the maximum number of candidates that a voter can choose is __value__, but cannot be larger than the number of candidates which is __max__", "election-question-int-size-min-num-winners": "Election '__eltitle__', question '__qtitle__': there must be at least one winner, but it is __value__ instead", - "election-question-int-size-max-num-winners": "Election '__eltitle__', question '__qtitle__': the number of winners is __value__, but cannot be bigger than the number of answers which is __max__" + "election-question-int-size-max-num-winners": "Election '__eltitle__', question '__qtitle__': the number of winners is __value__, but cannot be bigger than the number of answers which is __max__", + "election-census-createel-unknown": "Election, an unknown error happened while adding census after creating the election: __message__", + "election-census-is-array-admin-fields": "Election '__eltitle__': admin_fields must be an array", + "election-census-array-length-max-admin-fields": "Election '__eltitle__': admin_fields array length is __len__, exceeding the maximum of __max__", + "election-census-lambda-admin-fields-int-type-value": "Election '__eltitle__': the following admin fields are of integer type but their values are not integers: __admin_names__.", + "election-census-lambda-admin-fields-email-type-value": "Election '__eltitle__': the following admin fields have invalid email values: __admin_names__.", + "election-census-lambda-admin-fields-int-min-value": "Election '__eltitle__': the following admin fields of integer type have a value that is less than their minimum allowed value : __admin_names__.", + "election-census-lambda-admin-fields-int-max-value": "Election '__eltitle__': there is at least one field in admin fields with int type where the value is more than the maximum allowed value", + "election-census-lambda-admin-fields-string-type-value": "Election '__eltitle__': the following admin fields are of string type but their values are not string: __admin_names__.", + "election-census-lambda-admin-fields-required-value": "Election '__eltitle__': these admin fields are required but they are empty: __admin_names__.", + "election-census-lambda-admin-fields-string-value-array-length": "Election '__eltitle__': the following admin fields of string type have a string character length that has exceeded the allowed maximum of __max__ characters: __admin_names__.", + "election-census-lambda-admin-fields-email-value-array-length": "Election '__eltitle__': the following admin fields of email type have a string character length that has exceeded the allowed maximum of __max__ characters: __admin_names__.", + "election-census-admin-fields-is-string-if-defined-placeholder": "Election '__eltitle__': admin field __fname__ placeholder must be a string", + "election-census-admin-fields-array-length-if-defined-max-placeholder": "Election '__eltitle__': admin field __fname__ placeholder length is __len__ characters, exceeding the maximum of __max__", + "election-census-admin-fields-is-string-label": "Election '__eltitle__': admin field __fname__ label must be a string", + "election-census-admin-fields-array-length-max-label": "Election '__eltitle__': admin field __fname__ label length is __len__ characters, exceeding the maximum of __max__", + "election-census-admin-fields-is-string-if-defined-description": "Election '__eltitle__': admin field __fname__ description must be a string", + "election-census-admin-fields-array-length-if-defined-max-description": "Election '__eltitle__': admin field __fname__ description length is __len__ characters, exceeding the maximum of __max__", + "election-census-admin-fields-is-string-name": "Election '__eltitle__': admin field __fname__ name must be a string", + "election-census-admin-fields-array-length-max-name": "Election '__eltitle__': admin field __fname__ name length is __len__ characters, exceeding the maximum of __max__", + "election-census-admin-fields-is-string-type": "Election '__eltitle__': admin field __fname__ type must be a string", + "election-census-admin-fields-array-length-max-type": "Election '__eltitle__': admin field __fname__ type length is __len__ characters, exceeding the maximum of __max__", + "election-census-admin-fields-lambda-min-number": "Election '__eltitle__': admin field __fname__ min must be a number", + "election-census-admin-fields-lambda-max-number": "Election '__eltitle__': admin field __fname__ max must be a number", + "election-census-admin-fields-lambda-step-number": "Election '__eltitle__': admin field __fname__ step must be a number", + "election-census-admin-fields-lambda-required-boolean": "Election '__eltitle__': admin field __fname__ required must be a boolean", + "election-census-admin-fields-lambda-private-boolean": "Election '__eltitle__': admin field __fname__ private must be a boolean" } }, diff --git a/locales/es.json b/locales/es.json index 6394e9e9..90a43573 100644 --- a/locales/es.json +++ b/locales/es.json @@ -29,10 +29,55 @@ "votings": {"plurality-at-large": "Voto en bloque o Escrutinio Mayoritario Plurinominal", "borda-nauru": "Borda de Nauru o Borda Dowdall (1/n)", "borda": "Borda Count (tradicional)", "pairwise-beta": "Comparación de pares (distribución beta)", "desborda": "DesBorda"} }, "avAdmin": { + "profile": { + "title": "Perfil", + "save": "Guardar cambios" + }, + "onboarding": { + "help_tour": { + "step0_help_title": "Ayuda", + "step0_help_content": "Si necesitas más ayuda, haz clic aquí para obtenerla. ¡También puedes iniciar de nuevo el tour desde aquí!", + + "step1_chat_title": "Chatea con nosotros", + "step1_chat_content": "Si tienes alguna pregunta,, estaremos encantados de responderla rápidamente a través del chat." + }, + "dashboard_tour": { + "step0_test_title": "Votación de prueba", + "step0_test_content": "Has creado una votación de prueba. Las elecciones de prueba tienen un tamaño de censo limitado y una votación real sólo se puede crear a partir de una votación de prueba recontada con éxito.", + + "step1_real_title": "Votación real", + "step1_real_content": "Una vez que se realize el recuento de ésta votación, podras crear una votación real haciendo clic aquí.", + + "step2_status_title": "Estado de la votación", + "step2_status_content": "Esta sección muestra el estado de la votación y en ella siempre aparece un botón para pasar la votación al siguiente estado.", + + "step3_start_title": "Comienza la votación", + "step3_start_content": "Haz clic en este botón apra comenzar la votación y enviar el email de autenticación a todo el censo." + }, + "hello_tour": { + "step0_create_title": "Crea la votación", + "step0_create_content": "Comienza a crear una votación de prueba haciendo clic en el botón Nueva votación.

    O visualiza nuestro video de introducción:
    ", + + "step1_change_title": "Cambiar el título", + "step1_change_content": "Ahora puedes editar todos los detalles de la elección, por ejemplo podrías editar el título de la votación haciendo clic aquí.", + + "step2_sidebar_title": "Barra lateral", + "step2_sidebar_content": "Podrías personalizar la votación haciendo clic en éstas secciones.", + + "step3_review_title": "Revisa la votación antes de crearla", + "step3_review_content": "Una vez hayas terminado configurando la votación, haz clic en el botón Crear Votación para revisar y crear la votación.", + + "step4_create2_title": "Crear la votación", + "step4_create2_content": "Haz clic en éste botón para crear la votación.

    Ten en cuenta que la votación no se creará hasta que hagas clic en éste botón, y por razones de seguridad no se podrá editar posteriormente." + } + }, "show_points_policy": { "label": "Mostrar puntos", "text": "Mostrar los puntos que la papeleta da a cada opción seleccionada" }, + "setting": { + "helpTooltip": "Clickea para expandir/colapsar texto de ayuda sobre esta configuración" + }, "shuffling_policy": { "categories_label": "Ramdomize categories", "categories_label_text": "Check to randomize", @@ -44,11 +89,6 @@ "shuffle-all": "Shuffle all options. Each voter will see the options for this question in a different order in the voting booth. If there are different categories, they will also be randomized.", "shuffle-some": "Shuffle the options of a selected list of categories." }, - "notes": { - "noteTag": "Nota", - "thisisATestElection": "Esta es una votación de prueba. Por motivos de seguridad, sólo pueden crearse votaciones reales a partir de votaciones de prueba exitosas.", - "thisisATestElectionPopover": "Esta es una votación de prueba. Por motivos de seguridad, sólo pueden crearse votaciones reales a partir de votaciones de prueba exitosas. Para ello, debes completar el proceso de la votación hasta el paso de publicar resultados" - }, "results": { "downloadData": "Descargar datos", "downloadBallotsCsv": "Descargar votos como hoja de cálculo (formato CSV)", @@ -66,6 +106,16 @@ "logout": "salir" }, "learnMore": "Más información..", + "adminFields": { + "title": "Campos admin", + "intro": "Campos admin", + "maxError": "__value__ es mayor de __max__", + "minError": "__value__ es menor de __min__", + "textError": "error en texto __value__", + "emailError": "error on email __value__", + "numberError": "__value__ ha de ser un número", + "requiredError": "Campo requerido con valor vacío" + }, "sidebar": { "elections": "Votaciones", "newel": "Nueva votación", @@ -83,14 +133,24 @@ "myAccount": "Mi cuenta", "billinfo": "Información de pago", "billhistory": "Historial de pagos", - "buy": "Comprar créditos" + "buy": "Comprar créditos", + "adminFields": "Admin fields" }, "elections": { "filter": "Filtrar...", "test": "prueba", + "draft": "borrador", "real": "real", "status": "Estado", - "participation": "Participación" + "participation": "Participación", + "useDraftModal": { + "title": "Quieres cargar el borrador de la votación?", + "body": "Esta acción cargará el borrador de la votación con título '__title__'. Esta acción cargará el borrador y subsecuentes modificaciones de su configuración reescribirán el borrador. Desea proceder?" + }, + "eraseDraftModal": { + "title": "Deseas eliminar el borrador de la votación?", + "body": "Esta acción eliminará el borrador de la votación con título '__title__'. Esta acción no podrá ser revertida. Desea proceder?" + } }, "action": { "create": "crear", @@ -145,6 +205,9 @@ "createHeader": "Crear votación", "createBody": "Para crear la votación, su configuración es enviada a las autoridades y estas crean las claves públicas de la votación. Tras esto, la votación ya no es editable. Antes de proceder a realizar este paso, por favor comprueba que la configuración es correcta (por ejemplo comprobándolo en la cabina de votación de prueba). Si necesitas cambiar la votación pasado este paso, deberás crear una nueva.", "confirmCreateButton": "Confirmar y CREAR la votación", + "createRealHeader": "Crear votación real", + "createRealBody": "Esta es una votación de prueba. Por motivos de seguridad, sólo pueden crearse votaciones reales a partir de votaciones de prueba exitosas. Para ello, debes completar el proceso de la votación hasta el paso de publicar resultados.", + "confirmCreateRealButton": "Confirmar y CREAR votación real", "calculateResultsHeader": "Calcular resultados", "calculateResultsBody": "El recuento se ha realizado y el próximo paso es el cálculo de resultados. Aunque el recuento sólo se permite realizar una vez, el cálculo de resultados se puede realizar tantas veces como sea necesario antes de publicarlos.", "calculateResultsButton": "Confirmar y CALCULAR resultados", @@ -295,6 +358,10 @@ "fields": {"label": "Campos extra", "placeholder": "Campos extra del votante"}, "modals": { + "comment": "Observaciones", + "csvLoadingHeader": "Uploading CSV to census", + "csvLoadingBody": "Please click on the start upload button to add the users from the CSV to the census. Please wait until the process is finished and don't close this window.", + "csvLoadingStartButton": "Start upload NOW", "addCsvHeader": "Añadir CSV al censo", "addCsvBody": "Aqui puedes añadir muchas personas al censo de una tacada. El formato es CSV, con una persona por línea de texto. Se utiliza el punto y coma como separador de campos, en orden. Nótese que no se realiza validación alguna de los datos, así que es tu responsabilidad insertar datos correctos.", "addCsvTextareaPlaceholder": "Un elector por línea, siguiendo el orden de los campos separando por punto y coma", @@ -330,6 +397,11 @@ "deactivatedCensusSuccessfully": "Se desactivaron las personas seleccionadas correctamente", "sentCodesSuccessfully": "Enviados los códigos de autenticación correctamente", + "activateOneAction": "Activar", + "deactivateOneAction": "Desactivar", + "removeCensusOneAction": "Eliminar", + "sendAuthCodesOneAction": "Enviar código de autenticación", + "voted": "votó", "notVoted": "no votó", "votedColumnHeader": "Votó?", @@ -350,6 +422,7 @@ "codeOption": "código", "textOption": "texto", "tlfOption": "teléfono", + "dateOption": "fecha", "intOption": "entero", "boolOption": "booleano", "fieldRegExLabel": "Expresión regular", @@ -368,6 +441,7 @@ "fieldRequiredAuthLabel": "Contrastar con el censo durante la autenticación", "fieldMatchCensusOnRegistrationLabel": "Contrastar con el censo durante el registro (sólo para pre-registro)", "fieldFillIfEmptyOnRegistrationLabel": "Requerido durante el registro, pero puede estar vacío en el pre-registro", + "fieldAutofillLabel": "Relleno automático", "fieldUniqueLabel": "Único (cada votante debe aportar un valor diferente a este campo)", "fieldPrivateLabel": "Privado (solo los administradores pueden ver y usar este campo)", "extraFieldsHeader": "Campos extra", @@ -390,7 +464,7 @@ "auth": { "intro": "La autenticación se usa para asegurar que un elector es quien dice ser. Aquí tienes que llegar a un compromiso entre seguridad, usabilidad y coste", "auth": "Método de autenticación", - "auths": {"email": "Correo electrónico", "sms": "SMS", "dnie": "DNI electrónico"}, + "auths": {"email": "Correo electrónico", "sms": "SMS", "dnie": "DNI electrónico", "user-and-password": "Usuario y contraseña"}, "sms": "Mensaje SMS", "smstemp": "Plantilla usada en el SMS a enviar al usuario. Usa __CODE__ para insertar el código SMS y __URL__ para insertar el enlace de votación, o __URL2__ para insertar un enlace que incluya el código", "smsdef": "Este es tu código de votación __CODE__", @@ -461,7 +535,33 @@ "election-question-int-size-min-max": "Votación '__eltitle__', pregunta '__qtitle__': el máximo número de candidaturas que puede seleccionar un votante es __value__, pero debe ser mayor o igual que el mínimo, que es __min__", "election-question-int-size-max-max": "Votación '__eltitle__', pregunta '__qtitle__': el máximo número de candidaturas que puede seleccionar un votante es __value__, pero no debe ser mayor que el número total de candidaturas, que es __max__", "election-question-int-size-min-num-winners": "Votación '__eltitle__', pregunta '__qtitle__': debe haber al menos un ganador, pero hay __value__", - "election-question-int-size-max-num-winners": "Votación '__eltitle__', pregunta '__qtitle__': el número de ganadores es __value__, pero no puede ser mayor que el número total de candidaturas, que es __max__" + "election-question-int-size-max-num-winners": "Votación '__eltitle__', pregunta '__qtitle__': el número de ganadores es __value__, pero no puede ser mayor que el número total de candidaturas, que es __max__", + "election-census-createel-unknown": "Votación, un error desconocido ocurrió después de crear la votación: __message__", + "election-census-is-array-admin-fields": "Votación '__eltitle__': admin_fields ha de ser un array", + "election-census-array-length-max-admin-fields": "Votación '__eltitle__': el array admin_fields tiene una longitud de __len__ caracteres, excediendo el máximo de __max__", + "election-census-lambda-admin-fields-int-type-value": "Votación '__eltitle__': los siguientes admin fields son de tipo entero pero sus valores no lo son: __admin_names__.", + "election-census-lambda-admin-fields-email-type-value": "Votación '__eltitle__': los siguientes admin fields tienen emails inválidos: __admin_names__.", + "election-census-lambda-admin-fields-int-min-value": "Votación '__eltitle__': los siguientes admin fields de tipo int tienen un valor menor que el mínimo valor permitido para los mismos: __admin_names__.", + "election-census-lambda-admin-fields-int-max-value": "Votación '__eltitle__': hay al menos un campo en admin fields con tipo int donde el value es mayor que el máximo valor permitido", + "election-census-lambda-admin-fields-string-type-value": "Votación '__eltitle__': los siguientes admin fields son de tipo string pero sus valores no lo son: __admin_names__.", + "election-census-lambda-admin-fields-required-value": "Election '__eltitle__': estos campos de admin fields son obligatorios pero están vacíos: __admin_names__.", + "election-census-lambda-admin-fields-string-value-array-length": "Votación '__eltitle__': los siguientes admin fields de tipo string tienen un string con un número de caracteres que ha excedido el máximo número de caracteres permitidos __max__: __admin_names__.", + "election-census-lambda-admin-fields-email-value-array-length": "Votación '__eltitle__': los siguientes admin fields de tipo email tienen un string con un número de caracteres que ha excedido el máximo número de caracteres permitidos __max__: __admin_names__.", + "election-census-admin-fields-is-string-if-defined-placeholder": "Votación '__eltitle__': el campo de admin field __fname__ placeholder ha de ser un string", + "election-census-admin-fields-array-length-if-defined-max-placeholder": "Votación '__eltitle__': el campo de admin field __fname__ placeholder tiene una longitud de __len__ caracteres, excediendo el máximo de __max__", + "election-census-admin-fields-is-string-label": "Votación '__eltitle__': el campo de admin field __fname__ label ha de ser un string", + "election-census-admin-fields-array-length-max-label": "Votación '__eltitle__': el campo de admin field __fname__ label tiene una longitud de __len__ caracteres, excediendo el máximo de __max__", + "election-census-admin-fields-is-string-if-defined-description": "Votación '__eltitle__': el campo de admin field __fname__ description ha de ser un string", + "election-census-admin-fields-array-length-if-defined-max-description": "Votación '__eltitle__': el campo de admin field __fname__ description tiene una longitud de __len__ caracteres, excediendo el máximo de __max__", + "election-census-admin-fields-is-string-name": "Votación '__eltitle__': el campo de admin field __fname__ name ha de ser un string", + "election-census-admin-fields-array-length-max-name": "Votación '__eltitle__': el campo de admin field __fname__ name tiene una longitud de __len__ caracteres, excediendo el máximo de __max__", + "election-census-admin-fields-is-string-type": "Votación '__eltitle__': el campo de admin field __fname__ type ha de ser un string", + "election-census-admin-fields-array-length-max-type": "Votación '__eltitle__': el campo de admin field __fname__ type tiene una longitud de __len__ caracteres, excediendo el máximo de __max__", + "election-census-admin-fields-lambda-min-number": "Votación '__eltitle__': el campo admin field __fname__ min ha de ser un número", + "election-census-admin-fields-lambda-max-number": "Votación '__eltitle__': el campo admin field __fname__ max ha de ser un número", + "election-census-admin-fields-lambda-step-number": "Votación '__eltitle__': el campo admin field __fname__ step ha de ser un número", + "election-census-admin-fields-lambda-required-boolean": "Votación '__eltitle__': el campo admin field __fname__ required ha de ser un booleano", + "election-census-admin-fields-lambda-private-boolean": "Votación '__eltitle__': el campo admin field __fname__ private ha de ser un booleano" } }, diff --git a/locales/gl.json b/locales/gl.json index b69ee884..4230b910 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -30,10 +30,55 @@ }, "avAdmin": { + "profile": { + "title": "Perfil", + "save": "Guardar cambios" + }, + "onboarding": { + "help_tour": { + "step0_help_title": "Ayuda", + "step0_help_content": "Si necesitas más ayuda, haz clic aquí para obtenerla. ¡También puedes iniciar de nuevo el tour desde aquí!", + + "step1_chat_title": "Chatea con nosotros", + "step1_chat_content": "Si tienes alguna pregunta,, estaremos encantados de responderla rápidamente a través del chat." + }, + "dashboard_tour": { + "step0_test_title": "Votación de prueba", + "step0_test_content": "Has creado una votación de prueba. Las elecciones de prueba tienen un tamaño de censo limitado y una votación real sólo se puede crear a partir de una votación de prueba recontada con éxito.", + + "step1_real_title": "Votación real", + "step1_real_content": "Una vez que se realize el recuento de ésta votación, podras crear una votación real haciendo clic aquí.", + + "step2_status_title": "Estado de la votación", + "step2_status_content": "Esta sección muestra el estado de la votación y en ella siempre aparece un botón para pasar la votación al siguiente estado.", + + "step3_start_title": "Comienza la votación", + "step3_start_content": "Haz clic en este botón apra comenzar la votación y enviar el email de autenticación a todo el censo." + }, + "hello_tour": { + "step0_create_title": "Crea la votación", + "step0_create_content": "Comienza a crear una votación de prueba haciendo clic en el botón Nueva votación.

    O visualiza nuestro video de introducción:
    ", + + "step1_change_title": "Cambiar el título", + "step1_change_content": "Ahora puedes editar todos los detalles de la elección, por ejemplo podrías editar el título de la votación haciendo clic aquí.", + + "step2_sidebar_title": "Barra lateral", + "step2_sidebar_content": "Podrías personalizar la votación haciendo clic en éstas secciones.", + + "step3_review_title": "Revisa la votación antes de crearla", + "step3_review_content": "Una vez hayas terminado configurando la votación, haz clic en el botón Crear Votación para revisar y crear la votación.", + + "step4_create2_title": "Crear la votación", + "step4_create2_content": "Haz clic en éste botón para crear la votación.

    Ten en cuenta que la votación no se creará hasta que hagas clic en éste botón, y por razones de seguridad no se podrá editar posteriormente." + } + }, "show_points_policy": { "label": "Amosar puntos", "text": "Amosar os puntos que o voto dá a cada opción seleccionada" }, + "setting": { + "helpTooltip": "Clickea para expandir/colapsar texto de ayuda sobre esta configuración" + }, "shuffling_policy": { "categories_label": "Ramdomize categories", "categories_label_text": "Check to randomize", @@ -45,11 +90,6 @@ "shuffle-all": "Shuffle all options. Each voter will see the options for this question in a different order in the voting booth. If there are different categories, they will also be randomized.", "shuffle-some": "Shuffle the options of a selected list of categories." }, - "notes": { - "noteTag": "Nota", - "thisisATestElection": "Esta es una votación de prueba. Por motivos de seguridad, sólo pueden crearse votaciones reales a partir de votaciones de prueba exitosas.", - "thisisATestElectionPopover": "Esta es una votación de prueba. Por motivos de seguridad, sólo pueden crearse votaciones reales a partir de votaciones de prueba exitosas. Para ello, debes completar el proceso de la votación hasta el paso de publicar resultados" - }, "results": { "downloadData": "Descargar datos", "downloadBallotsCsv": "Descargar votos como hoja de cálculo (formato CSV)", @@ -67,6 +107,14 @@ "logout": "sair" }, "learnMore": "Learn more..", + "adminFields": { + "title": "Campos admin", + "intro": "Campos admin", + "maxError": "__value__ es mayor de __max__", + "minError": "__value__ es menor de __min__", + "textError": "error en texto __value__", + "numberError": "__value__ ha de ser un número" + }, "sidebar": { "elections": "Votacións", "newel": "Nova votación", @@ -84,7 +132,8 @@ "myAccount": "A miña conta", "billinfo": "Información de pago", "billhistory": "Historial de pagos", - "buy": "Comprar créditos" + "buy": "Comprar créditos", + "adminFields": "Admin fields" }, "elections": { "filter": "Filtrar...", @@ -146,6 +195,9 @@ "createHeader": "Crear votación", "createBody": "Para crear a votación, a súa configuración é enviada ás autoridades e estas crean as claves públicas da votación. Tras isto, a votación xa non é editable. Antes de proceder a realizar este paso, por favor comproba que a configuración é correcta (por exemplo comprobándoo na cabina de votación de proba). Se necesitas cambiar a votación pasado este paso, deberás crear unha nova.", "confirmCreateButton": "Confirmar e CREAR a votación", + "createRealHeader": "Crear votación real", + "createRealBody": "Esta es una votación de prueba. Por motivos de seguridad, sólo pueden crearse votaciones reales a partir de votaciones de prueba exitosas. Para ello, debes completar el proceso de la votación hasta el paso de publicar resultados.", + "confirmCreateRealButton": "Confirmar y CREAR votación real", "calculateResultsHeader": "Calcular resultados", "calculateResultsBody": "El recuento se ha realizado y el próximo paso es el cálculo de resultados. Aunque el recuento sólo se permite realizar una vez, el cálculo de resultados se puede realizar tantas veces como sea necesario antes de publicarlos.", "calculateResultsButton": "Confirmar y CALCULAR resultados", @@ -295,6 +347,9 @@ "fields": {"label": "Campos extra", "placeholder": "Campos extra do votante"}, "modals": { + "csvLoadingHeader": "Uploading CSV to census", + "csvLoadingBody": "Please click on the start upload button to add the users from the CSV to the census. Please wait until the process is finished and don't close this window.", + "csvLoadingStartButton": "Start upload NOW", "addCsvHeader": "Engadir CSV ao censo", "addCsvBody": "Aqui podes engadir moitas persoas ao censo dunha tacada. O formato é CSV, cunha persoa por liña de texto. Utilízase o punto e coma como separador de campos, en orde. Nótese que non se realiza validación algunha dos datos, así que é a túa responsabilidade inserir datos correctos.", "addCsvTextareaPlaceholder": "Un elector por liña, seguindo a orde dos campos separando por punto e coma", diff --git a/package.json b/package.json index 5120c22e..4009f749 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agora-gui-admin", - "version" : "17.04.0", + "version" : "103111.8.0", "devDependencies": { "debug": "^2.2.0", "engine.io": "^1.6.8", @@ -20,7 +20,7 @@ "grunt-contrib-watch": "~0.6", "grunt-dom-munger": "~3.4", "grunt-karma": "^0.12.1", - "grunt-merge-json": "^0.9.5", + "grunt-merge-json": "0.9.5", "grunt-ng-annotate": "^0.9.2", "grunt-protractor-runner": "^1.1.4", "jshint-stylish": "^2.1.0", @@ -29,7 +29,6 @@ "karma-firefox-launcher": "~0.1.3", "karma-jasmine": "~0.3.7", "karma-mocha-reporter": "~1.1.5", - "karma-phantomjs-launcher": "~1.0.0", "load-grunt-tasks": "~0.2", "socket.io": "1.3.2", "socket.io-parser": "2.2.2" diff --git a/vendor/hopscotch-0.3.1/LICENSE b/vendor/hopscotch-0.3.1/LICENSE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/vendor/hopscotch-0.3.1/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/vendor/hopscotch-0.3.1/css/hopscotch.css b/vendor/hopscotch-0.3.1/css/hopscotch.css new file mode 100644 index 00000000..6a59b37f --- /dev/null +++ b/vendor/hopscotch-0.3.1/css/hopscotch.css @@ -0,0 +1,519 @@ +/**! hopscotch - v0.3.1 +* +* Copyright 2017 LinkedIn Corp. All rights reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +/** + * This fade animation is based on Dan Eden's animate.css (http://daneden.me/animate/), under the terms of the MIT license. + * + * Copyright 2013 Dan Eden. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +.animated { + -webkit-animation-fill-mode: both; + -moz-animation-fill-mode: both; + -ms-animation-fill-mode: both; + -o-animation-fill-mode: both; + animation-fill-mode: both; + -webkit-animation-duration: 1s; + -moz-animation-duration: 1s; + -ms-animation-duration: 1s; + -o-animation-duration: 1s; + animation-duration: 1s; +} +@-webkit-keyframes fadeInUp { + 0% { + opacity: 0; + -webkit-transform: translateY(20px); + } + 100% { + opacity: 1; + -webkit-transform: translateY(0); + } +} +@-moz-keyframes fadeInUp { + 0% { + opacity: 0; + -moz-transform: translateY(20px); + } + 100% { + opacity: 1; + -moz-transform: translateY(0); + } +} +@-o-keyframes fadeInUp { + 0% { + opacity: 0; + -o-transform: translateY(20px); + } + 100% { + opacity: 1; + -o-transform: translateY(0); + } +} +@keyframes fadeInUp { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} +.fade-in-up { + -webkit-animation-name: fadeInUp; + -moz-animation-name: fadeInUp; + -o-animation-name: fadeInUp; + animation-name: fadeInUp; +} +@-webkit-keyframes fadeInDown { + 0% { + opacity: 0; + -webkit-transform: translateY(-20px); + } + 100% { + opacity: 1; + -webkit-transform: translateY(0); + } +} +@-moz-keyframes fadeInDown { + 0% { + opacity: 0; + -moz-transform: translateY(-20px); + } + 100% { + opacity: 1; + -moz-transform: translateY(0); + } +} +@-o-keyframes fadeInDown { + 0% { + opacity: 0; + -ms-transform: translateY(-20px); + } + 100% { + opacity: 1; + -ms-transform: translateY(0); + } +} +@keyframes fadeInDown { + 0% { + opacity: 0; + transform: translateY(-20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} +.fade-in-down { + -webkit-animation-name: fadeInDown; + -moz-animation-name: fadeInDown; + -o-animation-name: fadeInDown; + animation-name: fadeInDown; +} +@-webkit-keyframes fadeInRight { + 0% { + opacity: 0; + -webkit-transform: translateX(-20px); + } + 100% { + opacity: 1; + -webkit-transform: translateX(0); + } +} +@-moz-keyframes fadeInRight { + 0% { + opacity: 0; + -moz-transform: translateX(-20px); + } + 100% { + opacity: 1; + -moz-transform: translateX(0); + } +} +@-o-keyframes fadeInRight { + 0% { + opacity: 0; + -o-transform: translateX(-20px); + } + 100% { + opacity: 1; + -o-transform: translateX(0); + } +} +@keyframes fadeInRight { + 0% { + opacity: 0; + transform: translateX(-20px); + } + 100% { + opacity: 1; + transform: translateX(0); + } +} +.fade-in-right { + -webkit-animation-name: fadeInRight; + -moz-animation-name: fadeInRight; + -o-animation-name: fadeInRight; + animation-name: fadeInRight; +} +@-webkit-keyframes fadeInLeft { + 0% { + opacity: 0; + -webkit-transform: translateX(20px); + } + 100% { + opacity: 1; + -webkit-transform: translateX(0); + } +} +@-moz-keyframes fadeInLeft { + 0% { + opacity: 0; + -moz-transform: translateX(20px); + } + 100% { + opacity: 1; + -moz-transform: translateX(0); + } +} +@-o-keyframes fadeInLeft { + 0% { + opacity: 0; + -o-transform: translateX(20px); + } + 100% { + opacity: 1; + -o-transform: translateX(0); + } +} +@keyframes fadeInLeft { + 0% { + opacity: 0; + transform: translateX(20px); + } + 100% { + opacity: 1; + transform: translateX(0); + } +} +.fade-in-left { + -webkit-animation-name: fadeInLeft; + -moz-animation-name: fadeInLeft; + -o-animation-name: fadeInLeft; + animation-name: fadeInLeft; +} +div.hopscotch-bubble .hopscotch-nav-button { + /* borrowed from katy styles */ + font-weight: bold; + border-width: 1px; + border-style: solid; + cursor: pointer; + margin: 0; + overflow: visible; + text-decoration: none !important; + width: auto; + padding: 0 10px; + height: 26px; + line-height: 24px; + font-size: 12px; + *zoom: 1; + white-space: nowrap; + display: -moz-inline-stack; + display: inline-block; + *vertical-align: auto; + zoom: 1; + *display: inline; + vertical-align: middle; + -moz-border-radius: 3px; + -ms-border-radius: 3px; + -o-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +div.hopscotch-bubble .hopscotch-nav-button:hover { + *zoom: 1; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); +} +div.hopscotch-bubble .hopscotch-nav-button:active { + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25) inset; + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25) inset; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25) inset; +} +div.hopscotch-bubble .hopscotch-nav-button.next { + border-color: #1b5480; + color: #fff; + margin: 0 0 0 10px; + /* HS specific*/ + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.35); + background-color: #287bbc; + filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#287bbc', endColorstr='#23639a'); + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #287bbc), color-stop(100%, #23639a)); + background-image: -webkit-linear-gradient(to bottom, #287bbc 0%, #23639a 100%); + background-image: -moz-linear-gradient(to bottom, #287bbc 0%, #23639a 100%); + background-image: -o-linear-gradient(to bottom, #287bbc 0%, #23639a 100%); + background-image: linear-gradient(to bottom, #287bbc 0%, #23639a 100%); +} +div.hopscotch-bubble .hopscotch-nav-button.next:hover { + background-color: #2672ae; + filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#2672ae', endColorstr='#1e4f7e'); + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #2672ae), color-stop(100%, #1e4f7e)); + background-image: -webkit-linear-gradient(to bottom, #2672ae 0%, #1e4f7e 100%); + background-image: -moz-linear-gradient(to bottom, #2672ae 0%, #1e4f7e 100%); + background-image: -o-linear-gradient(to bottom, #2672ae 0%, #1e4f7e 100%); + background-image: linear-gradient(to bottom, #2672ae 0%, #1e4f7e 100%); +} +div.hopscotch-bubble .hopscotch-nav-button.prev { + border-color: #a7a7a7; + color: #444; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + background-color: #f2f2f2; + filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#f2f2f2', endColorstr='#e9e9e9'); + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f2f2f2), color-stop(100%, #e9e9e9)); + background-image: -webkit-linear-gradient(to bottom, #f2f2f2 0%, #e9e9e9 100%); + background-image: -moz-linear-gradient(to bottom, #f2f2f2 0%, #e9e9e9 100%); + background-image: -o-linear-gradient(to bottom, #f2f2f2 0%, #e9e9e9 100%); + background-image: linear-gradient(to bottom, #f2f2f2 0%, #e9e9e9 100%); +} +div.hopscotch-bubble .hopscotch-nav-button.prev:hover { + background-color: #e8e8e8; + filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FFE8E8E8', endColorstr='#FFA9A9A9'); + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #e8e8e8), color-stop(13%, #e3e3e3), color-stop(32%, #d7d7d7), color-stop(71%, #b9b9b9), color-stop(100%, #a9a9a9)); + background-image: -webkit-linear-gradient(to bottom, #e8e8e8 0%, #e3e3e3 13%, #d7d7d7 32%, #b9b9b9 71%, #a9a9a9 100%); + background-image: -moz-linear-gradient(to bottom, #e8e8e8 0%, #e3e3e3 13%, #d7d7d7 32%, #b9b9b9 71%, #a9a9a9 100%); + background-image: -o-linear-gradient(to bottom, #e8e8e8 0%, #e3e3e3 13%, #d7d7d7 32%, #b9b9b9 71%, #a9a9a9 100%); + background-image: linear-gradient(to bottom, #e8e8e8 0%, #e3e3e3 13%, #d7d7d7 32%, #b9b9b9 71%, #a9a9a9 100%); +} +div.hopscotch-bubble { + background-color: #ffffff; + border: 5px solid #000000; + /* default */ + border: 5px solid rgba(0, 0, 0, 0.5); + /* transparent, if supported */ + color: #333; + font-family: Helvetica, Arial; + font-size: 13px; + position: absolute; + z-index: 999999; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -moz-background-clip: padding; + /* for Mozilla browsers*/ + -webkit-background-clip: padding; + /* Webkit */ + background-clip: padding-box; + /* browsers with full support */ +} +div.hopscotch-bubble * { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +div.hopscotch-bubble.animate { + -moz-transition-property: top, left; + -moz-transition-duration: 1s; + -moz-transition-timing-function: ease-in-out; + -ms-transition-property: top, left; + -ms-transition-duration: 1s; + -ms-transition-timing-function: ease-in-out; + -o-transition-property: top, left; + -o-transition-duration: 1s; + -o-transition-timing-function: ease-in-out; + -webkit-transition-property: top, left; + -webkit-transition-duration: 1s; + -webkit-transition-timing-function: ease-in-out; + transition-property: top, left; + transition-duration: 1s; + transition-timing-function: ease-in-out; +} +div.hopscotch-bubble.invisible { + opacity: 0; +} +div.hopscotch-bubble.hide, +div.hopscotch-bubble .hide, +div.hopscotch-bubble .hide-all { + display: none; +} +div.hopscotch-bubble h3 { + color: #000; + font-family: Helvetica, Arial; + font-size: 16px; + font-weight: bold; + line-height: 19px; + margin: -1px 15px 0 0; + padding: 0; +} +div.hopscotch-bubble .hopscotch-bubble-container { + padding: 15px; + position: relative; + text-align: left; + -webkit-font-smoothing: antialiased; + /* to fix text flickering */ +} +div.hopscotch-bubble .hopscotch-content { + font-family: Helvetica, Arial; + font-weight: normal; + line-height: 17px; + margin: -5px 0 11px; + padding-top: 8px; +} +div.hopscotch-bubble .hopscotch-bubble-content { + margin: 0 0 0 40px; +} +div.hopscotch-bubble.no-number .hopscotch-bubble-content { + margin: 0; +} +div.hopscotch-bubble .hopscotch-bubble-close { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 0; + color: #000; + background: transparent url(/admin/img/sprite-green.png) -192px -92px no-repeat; + display: block; + padding: 8px; + position: absolute; + text-decoration: none; + text-indent: -9999px; + width: 8px; + height: 8px; + top: 0; + right: 0; +} +div.hopscotch-bubble .hopscotch-bubble-close.hide, +div.hopscotch-bubble .hopscotch-bubble-close.hide-all { + display: none; +} +div.hopscotch-bubble .hopscotch-bubble-number { + background: transparent url(/admin/img/sprite-green.png) 0 0 no-repeat; + color: #fff; + display: block; + float: left; + font-size: 17px; + font-weight: bold; + line-height: 31px; + padding: 0 10px 0 0; + text-align: center; + width: 30px; + height: 30px; +} +div.hopscotch-bubble .hopscotch-bubble-arrow-container { + position: absolute; + width: 34px; + height: 34px; +} +div.hopscotch-bubble .hopscotch-bubble-arrow-container .hopscotch-bubble-arrow, +div.hopscotch-bubble .hopscotch-bubble-arrow-container .hopscotch-bubble-arrow-border { + width: 0; + height: 0; +} +div.hopscotch-bubble .hopscotch-bubble-arrow-container.up { + top: -22px; + left: 10px; +} +div.hopscotch-bubble .hopscotch-bubble-arrow-container.up .hopscotch-bubble-arrow { + border-bottom: 17px solid #ffffff; + border-left: 17px solid transparent; + border-right: 17px solid transparent; + position: relative; + top: -10px; +} +div.hopscotch-bubble .hopscotch-bubble-arrow-container.up .hopscotch-bubble-arrow-border { + border-bottom: 17px solid #000000; + border-bottom: 17px solid rgba(0, 0, 0, 0.5); + border-left: 17px solid transparent; + border-right: 17px solid transparent; +} +div.hopscotch-bubble .hopscotch-bubble-arrow-container.down { + bottom: -39px; + left: 10px; +} +div.hopscotch-bubble .hopscotch-bubble-arrow-container.down .hopscotch-bubble-arrow { + border-top: 17px solid #ffffff; + border-left: 17px solid transparent; + border-right: 17px solid transparent; + position: relative; + top: -24px; +} +div.hopscotch-bubble .hopscotch-bubble-arrow-container.down .hopscotch-bubble-arrow-border { + border-top: 17px solid #000000; + border-top: 17px solid rgba(0, 0, 0, 0.5); + border-left: 17px solid transparent; + border-right: 17px solid transparent; +} +div.hopscotch-bubble .hopscotch-bubble-arrow-container.left { + top: 10px; + left: -22px; +} +div.hopscotch-bubble .hopscotch-bubble-arrow-container.left .hopscotch-bubble-arrow { + border-bottom: 17px solid transparent; + border-right: 17px solid #ffffff; + border-top: 17px solid transparent; + position: relative; + left: 7px; + top: -34px; +} +div.hopscotch-bubble .hopscotch-bubble-arrow-container.left .hopscotch-bubble-arrow-border { + border-right: 17px solid #000000; + border-right: 17px solid rgba(0, 0, 0, 0.5); + border-bottom: 17px solid transparent; + border-top: 17px solid transparent; +} +div.hopscotch-bubble .hopscotch-bubble-arrow-container.right { + top: 10px; + right: -39px; +} +div.hopscotch-bubble .hopscotch-bubble-arrow-container.right .hopscotch-bubble-arrow { + border-bottom: 17px solid transparent; + border-left: 17px solid #ffffff; + border-top: 17px solid transparent; + position: relative; + left: -7px; + top: -34px; +} +div.hopscotch-bubble .hopscotch-bubble-arrow-container.right .hopscotch-bubble-arrow-border { + border-left: 17px solid #000000; + border-left: 17px solid rgba(0, 0, 0, 0.5); + border-bottom: 17px solid transparent; + border-top: 17px solid transparent; +} +div.hopscotch-bubble .hopscotch-actions { + margin: 10px 0 0; + text-align: right; +} diff --git a/vendor/hopscotch-0.3.1/img/sprite-green.png b/vendor/hopscotch-0.3.1/img/sprite-green.png new file mode 100644 index 0000000000000000000000000000000000000000..89fc8f2343c554babe0753c42c0d59ec3059f15a GIT binary patch literal 5405 zcmbW4=R2HTw8ozq6CTEl5iR;?A&E}(GBbKjloTyQ_cDSYL>pa1gapw`5D8JD2hl}K z5Jd02_g;_pJ%7PDU-q^3UTfWJ?=Qdm+OO_wt02f2$N&I9sHrOI0ssVZ6_NoA>U)f=rRdmFKcWu7N6l2?_Qihn8km%^UeoOWUdu~V%|J4JX))~2y^MfU8} zX>cA`ZWO>^G+GR-Dsv7TQ zD`sIJ{&dpMCnvi0csf4jQJndtmXGrH{oa8=Eav0~d$Sh#UjBK$0wVR#%af1m)DEAs zM+}ofGW%K7<`6KcK!;CmS-M}kVja}4g{S#M+&kzc2DCK-R`YKMZ1J5aB@(Wrqo@SMFo(oX8#hsZzGdEa?4z*RWw_n z0M=IJ@9%GN<{3}aNP4=`z;5g4k8@7u$W3%*)CBW@$VyR>VN{Q+py3`oEpi zg6m%~%YxC$cV_x6NeFSOx_`ZXNKc*lUJ5=F+1y!dF_#j=4-v!?1fEHOZ9<*AiW;F! zr6thpre;Fo$G26ZMqjea-p|~6uzBo{&3S(LyAymKdn?zXWU|C8fX`(%Vi}EYVOHjbb9LXpe+^QnO?%xpZl4KuK%2BzI-FASE|5T;_3Km`ZT-cox3h)+~zT9ioNBqsI|6$~?S>dYgeufO?SamcN@s&n!K7>ksN$dr*1 z1jE{K@w_nL6T)Id5UBKTn?fjdmDJTn{=S%nXgl2za^xZgC}yL zCB1*vXA=gugT6Ov3JU!A+Qb5IvPG-*OiiW#i!KfhjzV|R;#wrM*RR4@Ofuw^5a9e! z+}SfCJ#y#q!8T8DK$*UgJ(WY|pJmD0W^SoBV85QF{K^~d*Ir0far=x;kBW$*rK7WZ z`O>fojHQHGa+)HbRW=7gg#L|QMa%P78l^;>su?)f_wVCaMS%K7U$Gk0h(zZ_Sug6!dEhYu@jwj z)SREBS$SyVlj#Q;%!?$QA+krb1Lt1&8+};0bT(M3-I40&W!9sz!ViH)oiCuV6l-zK z>5iA6+jKc6MCSn5zbaoaf(U*hqO^6#z!IOlb})Pqh~_?|t=j}jQ= ztr(AGrBGn2WL;FbI_&i4H$ZMRFpB%e$Oxcjr6 z62;qrY8YDhx5r_qI7Ep;Il>DS7g7aw(S zM;*?!F3Big?;uuIwxBe&S(`R+nndjwtBX6bbHRa11Qd=E9dsWCvQb0+-`jS=t5>h+ z%)8uVUzzquiix?~dU#aci#(lw_TU2Pzg5XP*hAuT{82}j+S=Nh0cZd6<%z8;vQ;9~ zKA6sbczD=`^TtFJP$DqrNJ$JH&WsHjkMZu{!A+dv|nk%KH83_pqzGP|SB+511l#dVRX zaJC&n{+*j~FGpU%eYd*f%ZPe6D;UX{F0i5YPnz$VA&9BwfdcN=320!~oAm!;giC`rVv%w=Lhh;^K$N`OIU1UOs+_##$-)4L{UcGgt>o2x5`& zYNq;=ad4!>PI#{JkwgyOoSan>;INg~S`8ga?iCXX>71hvKqK7M#4dfmUQU2=r_=e0 zzJfwaEI;5*YTbbAR2C_EsMqsa#Jmd~@JdFOsI7xysJ9*WsG7JfCWi(xR7Brb!D{O# ze%r%HJ(X$mcY}aP2D~wqwW}WPp3>0zHcDa|2Y@vZ)Cb!K;mT{&)_Fu7C%X zJTT!EfVdYVj9q4z#%Z3P#)oK;bl0Oe---XD+dL7I?+Y>`y$lJj%zj+Lc-8^`Abe^%AYlZ_#>7xyin6#5v zxwK#auzR*5SCn6HD5(l+-=_SWLj$yMS5Y{q3<}=-K;-y=$co~=X{Y}MkA%+NEDiu1 z@>b80I}ijLz;oiv1_0%M_lc@(0>L1iJ;~~?w+(AG*iL@4WzsazZx z7ZGelw16JW?8AKd#u<;7(}I=3E@QCOT?jlp4__$Y8^5OKrwQKcb>@n^?PiZr5!49Bi zj+6lmrazHi6QSVS*bZ z)KUVc56s7&M4%i+JKiXhPRGrTU4fHDuCYl4oEEZ_35h_lsCE#kkxu`a3qpkhY(eZ8 zFat#4c%c9PZOxdi{%2~2kJVGWx?G6A^zy2sA(^Bj5$8t>bfkYJ5z7iHEn>F>{vBDc-xKKsEvl@vJvco4 zJv^MyKr>0{HwwcDiiOpc-#0QcQfHdF#w1P7^IA$B?=6*bVa)WBF_M`$Mv0m0U{fM9Q6VzJ%~N*2+`VtFF&q}X7B1ND zLyB*K1Qr7FDDuc?fT|6=2~W6`jFi}+>O5`@P8=B-VWikmWSLW<&x_=CW>p;-96T&> zpJ`~omekZZbai+C)`ECO(OJ1Ne-^nZecPec`33sWPj{pt6_{C9A*jUd`c4iF9a+~R zK9YBg$OcrBmwN%Tv$G3FM?-Ur_ld36S+L|{nh9G^JIa_x8iXLt_7GJAXwTy|P_kpj z7^ea1I!8ckb5c@L?$XiG-E_bVCnV5ku10u7pkuqAt!`{67q)1tdkTX~4&hs6TcLOo z8xr$G=1Lj!v)~PgErdrAbbSlp)AM<3Ac9djF9Ab<>^gLu1H(w!#gJvNX4u4=5SjWaT9LI$`TVcydjZC|7d7*s+|0(XW;O-5i1nm zt7idZut4@I>jwtn{=6lA83EvxEL+3Z*RI@fkPKsDl5%CoLJuErPc0a>&#Ya}p$HVg zh|$O_X>fD-$_%G!XpT#&BN)r6|c>udL{RoG#P-qxr}7^0P3&UVIsEZ z*n!*v$z{r#LTkj6+|mwn#()RD|1h8;jl+N?Kkt(2YR0VLzR<%=R~B-`D|a+?I~GZZ z2n0NCL340#R(3WeL7)-a%Ph)IX?x#fzcrYQ;IjA3m8Czwmw`Q=(t|~Ud*S3Y8(`+> z*tJBc3KT7+cN0I^+uKtIbMx|;VfCY}{#0DBXC~Z$HxrLPXg2OQmHc>*>dGzpnX@u~ zU){&;(*JqK5}p%ExaxN;rEeu9(n6OX%%gx!65=vcO_CNMA^12kwO;kA^E`(tDKT*( zdXF{ukc<)_R6R(IzS5v0Ko^@=h-8sjxvn!;UcNS|huVH$9u{FlrXxP(hK>7xnb%eK^&4WtNNt>5w04gXW z318+Ty&6Q??*?7Z9}jTB=A8$i}OZ0r~|lAknhlp&5Sg#5I*Pym7^-*1RK4e|3=Cxg;k|Gi$u;0 zHDECmba2T%P|Ad13;!*O4@N~r^^?!ils9INF^to8xQG@yogh(fk3@J>I6Q_o3=PFM z{&I4>G}`nQKJ;5T>M)PX0j$_vSg+{v9pQO zsGYn~=pbowJ`gd)wC9l#5-69RL6>b2B4*004oG z_wS%A$7@=4MhXD%elj;QaEutC=eYUsp66}%q)pe`G%+OXcbDFrp~wxRCHOw|#r!s2 zG(1ztg9Mukm}#3AO7k(PT1XEId1^_>@i`2%-D;XOn2_jZ^|3o63}!(|6`Id&X3v&u zll036SI545IfU(}mz)Yc$kO%hIY`ftml)w~FMRF3xyX`_H5yR(%6FLlF`;v`Spdy`I!2~qqqaz`|JZh-C-kRsjCGn74e!R96U z$}bV@;B%VUa!uHZ^UBupphIhGtHW``!=8eg_WF{!*(y5u(dYY+&4VZ{KGY-RqfDN3~ z6B?fHaHsQL{Dm*RGNq64!_5{)(sHI>>X^!zUBJE?ST;2EHLH7efQpVvZ@|pVzAdJu zZ*Ts-fwygc>Cd2$b*xSO%^;6X)D|$a-l1~G%nu}cPUB_5?H7hbe7C*hm|B&j=I%%5 z4+eRRrE6MK^`PlLvE6GgkPu^nY1u$zFp4~sUuDDi>C{t1k5@A|FCAmobxejU`bEcQZk zV(7B9#~^;OGRAlJ%oS#?`F_WaVb0R$l%74m+-J4iCu@W|#0fjy`&hmlaY6sS6G4&Gmv`AFPXF^DS4I}cp4991U3!?`3CB+K3Bns=5&F6#C>hrM@)BFeM z9e8UB{qRkscfE{nj%m(t++DtasXyt!6sOk~zzQm?_;Or=+4O7Ya!ErAw*8#8=Uvn_ zHLD#wOx9r0?Zo2Hqt>p#IV}fA(@**pL=1GmW|SLj@12n-u-KtGQ-ohGJ^8Lckp4XN zYDTm9l2GrSp}NsO88dpxt(KA{{NL>sx&4@Qx&1(5?GE?S8>uqAFN2}=z z0-Sd3Xwep>67I{uBQ>L;3rB9eNl;@}_cl9G)B@z=WpPU+80b$@4G;iM<1Zh8p3ffj z*)8ge(l|t=+wwmGlP@JA3ZXzmqky{#3*fGChyOm9d_H;R(F`y-BgtSu0S2d_o*@gq zxrNpIw}Cw#3I+hkF~ky%31$EQWHbQa;Q)Y!0stHW0RKPPR@{IbkTs)IpHcVY#}9{w z{fAGi5{ll$knBx5S+{d6+Uaq`1>*RRcrG+x zgxe=sKom~&X~Fm&AX?nlz_4W?@*C;1o2z1`z3`y7N4xqQQoW^ttDO5g$c%u?DG=5U z?ART#r?3937tVqN{zD_tPLCEH238)|CaiBP!Tt!N8+ z!)1ga#$NT-@^YxpFa>?jqmWJ12+q#V&oeRveI<+}p=LHstB}pFj;PTC5$4_;@}W41 zq2ovEY&`gCOTZp|>t{QYttY>1nJxN7#X-~}W5Cz^=1>~0>Zah8p4TfemT1z#9N8-* z{8p2!2I~OrDPL+`v_FfKK)KNe49ek{PpOBbSNbDX98y(yxkr50^__DGJziuk z)TQlofl%WB6}9hQtYv7p>+QK-moHNT#~TW_6B|8ktss8a=@;?`7Tb-`y0B*UP(3Tz z5V;g36&3a6Wt;Z8itYrDXY<{V1lr@eceMvy<2%%I*NiV_JNs#~Ge08a)fFC{2`|2* ztgFJpUi-O>E9m$Pd_i@mQkg|F3mdlfuydd4QVzg}-#Q(G$-7!bhc zefQ*R$mY*vuNCO)eR#HBOL`N<`yi{61N6^Z-IhXkzW6M=_cqk`?tR{;Qa%yP;L1u$ ze6O5j<+rzf_F&DRJq#r1-eRoK+tIa)N<8GKl<(XIhfkQiqLk_U*6eR}^qsWmE!F?5 zPF!tG=iUI3#5XN}ni8yznoEw>(GA-~CC8m+cXQ6AzbUpSn)ojr$nL3gGnH;+?Rq|SMRZicXp~18I+*CB-zl1V?5-W70JL34gYPTf z!na<1VD-mNr^qPx=H0~D`^C@hE8LKe3bu!AsDO!CzpA1(W+J5^U#ItFqk==Qy^>uZ zy@;8%lyX*?6BOMvfmz9#;oTNNJhlod4nANWl7Lk2s3rUF6M1N#2#QDv>XlYSVHqk6 z{u?AgV92V9Ny|`G_~iC1jG!J9&|OGGeKF|nz3Q|Od;2SZSe zz%a0nrKQ1RGqd3Y1e$6Kfr|5j2vB44e{HG}Lqaxeztld({N})6WMq=>CnvLnoR*DK z*a(MA>frcuVY?f~`7dO@eEm9@I5##nR@j3TnGq@GbB7?2$Z;BxVFun$T0ct&oD?1F zI-l&_YF+t$ZDMh9`S11h&?tEy;9qa)N<)2mdQOfm4|CKI0-XDm@1hBO`^`>MuoZJP zUk$$&Cx(pJtJc`uFgXzC!}5_9xajvESSD{7L|vjS)a;7T~Iuz_?phR;F(Lqho>6RMTF1jjgJF$H0DP+hOOFRGdfL z_k)EKTQN(2C@*%OIIt#L0mJ(b`(O$PA31~%@*E^A1LWFL()iYHj1tu?7(Z{5x;)wV zzNRLk_XqcCOJkG1RdGSjkIC^DGrh3o3wVz9L^p2YK_FyhM?)}VOu@BROEoD)gp7C> z-oGoZMr|rLYw9xf#;3}Vi5ByGCm(mY z0a6>AoM;7oq6LP-ZAA#}@bnyn*y?3D5u00ibg1qhcRuE&*qFqo6nLnw9}A31lWU(M zqt+H)#`hqIiZ{E+7ju|O-N5M{myyEWn|;k42+sO-kc*eEF`(t&wz+cCudD(*)N!>8 z?!}h)nQijC^&kjc_QW30;|pY4BlWZv*qN4oH5RAYv7Ssb0cH<_UTR)h5|e!(*hhjW z?>CP}IwQzjqt{|~^PK5LfL5n_(X27V_Lnz(Wz9ko==`B%*DIL=eDC7Amr*JC&qn_! znQ`<20chs?bpDuHOisD62lupx$buWW!yw=-g&0*15YWeRU2N#wAQLNFi=Z^P@*Oz0 zCV#KF>I}Rm7&{q;wfzfhRf-7EA(%U{u26s+K#J)#(92b(0>=8v5TLoL&fWV`Z@{He zI%w&tjICWuj1$VAc{6d}xTRzW*l)?!-Zs1DAG%xP-^1>FWF`7t!%! zH>h-u86^N4r6Tyg^&&Y)-#q0q82(;llA;}Wj2~i{zN&x<#KReicjuuLz{xqs>a+hN zka9KDO1zXG2)%1Nf?ac=7Qlx%$hF4Mz&}?+h8ZnVMEARi(}|NI4?!Vzio7b2qq1;i z@dqe7fg~FB^J{fm`=8$? zkEJF~9|8{|tb~6M*5NsR+!{5&xBonqkdOg0T53Ln?UssLD-6dW_0@~8NY}1csK7}+KneIyiXv$m(vrLHC{*Oy?5oFrY}iRK z(bF}*(Kas(oM+2ABZRi%UobH->0iZ&ii&C}C@9QLPk$KWrLp)vFntoQCRv`}8730W z8ghhriSlPdkH<1c_+!_$t*oqqkj~;8=#rljw>pXS%qOu}Uu$b?%RJC=H3Jbw#MXOk zqF%1cc_{h`d;^k9l?JRnZPFxFdZgb`(FkKPa@a_1EjBbV(l>!+HYX=Xv+fIR56Y9c zUo}4N{?3{1!e)a5z@9Yvx?$ zTv_p6CGY@Z`X7V7Eo~Bsl#!V!)Zg1noUp#J-Yd-kC#$GMt`3KNjEPVyj|2wag2FYw{FIsAiygb)03_n@}t&BBUCdSauaqu&Ap zgM(Q=f4X@lo`s1Pe;4UFv=Id{Y=_>h+^VOTfU^Jc>B0b1)F&u;J?Er~B#YP^22M8x zASJ0H%(|KICu(Iu=}(^~BED&2;d+k z=~G9FO~fRb9kzf&kf5QDUUWBk(VKUv)4^K?DQ;QiFwKPq>Qrw9xon&pv^aFenhx3E$YiK|)+=>8H_EZ)(DdeG5MA*ll*e+%8 z#!m6N@N^{{f&g3~wty{wAx=<&7(kjuC5$e7trax&r4uo^H&YWuoFQ(?dZ~$&au57G zCkMq3>Zm8nuEJoo2f=dJ9$4I-Ib5f8lC$nW#vG+qVcdlxHH}W>l~2=$y|m5*;rlE@ zJ{qequ0q9{UlAaeWCy@Z`a`Q}AP_msikIgTHJl6P6f^~O#I5{P{mf%-PUYX%YVv_6~Nrs+NhX7O!yx~3Y1C! literal 0 HcmV?d00001 diff --git a/vendor/hopscotch-0.3.1/js/hopscotch.js b/vendor/hopscotch-0.3.1/js/hopscotch.js new file mode 100644 index 00000000..a609bf55 --- /dev/null +++ b/vendor/hopscotch-0.3.1/js/hopscotch.js @@ -0,0 +1,2511 @@ +/**! hopscotch - v0.3.1 +* +* Copyright 2017 LinkedIn Corp. All rights reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.hopscotch = factory()); +}(this, (function () { 'use strict'; + + var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { + return typeof obj; + } : function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + + /* global document */ + + var Hopscotch; + var HopscotchBubble; + var HopscotchCalloutManager; + var HopscotchI18N; + var customI18N; + var customRenderer; + var customEscape; + var templateToUse = 'bubble_default'; + var Sizzle = window.Sizzle || null; + var utils; + var callbacks; + var helpers; + var winLoadHandler; + var defaultOpts; + var winHopscotch; + var undefinedStr = 'undefined'; + var waitingToStart = false; + var hasJquery = (typeof jQuery === 'undefined' ? 'undefined' : _typeof(jQuery)) !== undefinedStr; + var hasSessionStorage = false; + var isStorageWritable = false; + var validIdRegEx = /^[a-zA-Z]+[a-zA-Z0-9_-]*$/; + var rtlMatches = { + left: 'right', + right: 'left' + }; + + // If cookies are disabled, accessing sessionStorage can throw an error. + // sessionStorage could also throw an error in Safari on write (even though it exists). + // So, we'll try writing to sessionStorage to verify it's available. + try { + if (_typeof(window.sessionStorage) !== undefinedStr) { + hasSessionStorage = true; + sessionStorage.setItem('hopscotch.test.storage', 'ok'); + sessionStorage.removeItem('hopscotch.test.storage'); + isStorageWritable = true; + } + } catch (err) {} + + defaultOpts = { + smoothScroll: true, + scrollDuration: 1000, + scrollTopMargin: 200, + showCloseButton: true, + showPrevButton: false, + showNextButton: true, + bubbleWidth: 280, + bubblePadding: 15, + arrowWidth: 20, + skipIfNoElement: true, + isRtl: false, + cookieName: 'hopscotch.tour.state' + }; + + if (!Array.isArray) { + Array.isArray = function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; + } + + /** + * Called when the page is done loading. + * + * @private + */ + winLoadHandler = function winLoadHandler() { + if (waitingToStart) { + winHopscotch.startTour(); + } + }; + + /** + * utils + * ===== + * A set of utility functions, mostly for standardizing to manipulate + * and extract information from the DOM. Basically these are things I + * would normally use jQuery for, but I don't want to require it for + * this framework. + * + * @private + */ + utils = { + /** + * addClass + * ======== + * Adds one or more classes to a DOM element. + * + * @private + */ + addClass: function addClass(domEl, classToAdd) { + var domClasses, classToAddArr, setClass, i, len; + + if (!domEl.className) { + domEl.className = classToAdd; + } else { + classToAddArr = classToAdd.split(/\s+/); + domClasses = ' ' + domEl.className + ' '; + for (i = 0, len = classToAddArr.length; i < len; ++i) { + if (domClasses.indexOf(' ' + classToAddArr[i] + ' ') < 0) { + domClasses += classToAddArr[i] + ' '; + } + } + domEl.className = domClasses.replace(/^\s+|\s+$/g, ''); + } + }, + + /** + * removeClass + * =========== + * Remove one or more classes from a DOM element. + * + * @private + */ + removeClass: function removeClass(domEl, classToRemove) { + var domClasses, classToRemoveArr, currClass, i, len; + + classToRemoveArr = classToRemove.split(/\s+/); + domClasses = ' ' + domEl.className + ' '; + for (i = 0, len = classToRemoveArr.length; i < len; ++i) { + domClasses = domClasses.replace(' ' + classToRemoveArr[i] + ' ', ' '); + } + domEl.className = domClasses.replace(/^\s+|\s+$/g, ''); + }, + + /** + * hasClass + * ======== + * Determine if a given DOM element has a class. + */ + hasClass: function hasClass(domEl, classToCheck) { + var classes; + + if (!domEl.className) { + return false; + } + classes = ' ' + domEl.className + ' '; + return classes.indexOf(' ' + classToCheck + ' ') !== -1; + }, + + /** + * @private + */ + getPixelValue: function getPixelValue(val) { + var valType = typeof val === 'undefined' ? 'undefined' : _typeof(val); + if (valType === 'number') { + return val; + } + if (valType === 'string') { + return parseInt(val, 10); + } + return 0; + }, + + /** + * Inspired by Python... returns val if it's defined, otherwise returns the default. + * + * @private + */ + valOrDefault: function valOrDefault(val, valDefault) { + return (typeof val === 'undefined' ? 'undefined' : _typeof(val)) !== undefinedStr ? val : valDefault; + }, + + /** + * Invokes a single callback represented by an array. + * Example input: ["my_fn", "arg1", 2, "arg3"] + * @private + */ + invokeCallbackArrayHelper: function invokeCallbackArrayHelper(arr) { + // Logic for a single callback + var fn; + if (Array.isArray(arr)) { + fn = helpers[arr[0]]; + if (typeof fn === 'function') { + return fn.apply(this, arr.slice(1)); + } + } + }, + + /** + * Invokes one or more callbacks. Array should have at most one level of nesting. + * Example input: + * ["my_fn", "arg1", 2, "arg3"] + * [["my_fn_1", "arg1", "arg2"], ["my_fn_2", "arg2-1", "arg2-2"]] + * [["my_fn_1", "arg1", "arg2"], function() { ... }] + * @private + */ + invokeCallbackArray: function invokeCallbackArray(arr) { + var i, len; + + if (Array.isArray(arr)) { + if (typeof arr[0] === 'string') { + // Assume there are no nested arrays. This is the one and only callback. + return utils.invokeCallbackArrayHelper(arr); + } else { + // assume an array + for (i = 0, len = arr.length; i < len; ++i) { + utils.invokeCallback(arr[i]); + } + } + } + }, + + /** + * Helper function for invoking a callback, whether defined as a function literal + * or an array that references a registered helper function. + * @private + */ + invokeCallback: function invokeCallback(cb) { + if (typeof cb === 'function') { + return cb(); + } + if (typeof cb === 'string' && helpers[cb]) { + // name of a helper + return helpers[cb](); + } else { + // assuming array + return utils.invokeCallbackArray(cb); + } + }, + + /** + * If stepCb (the step-specific helper callback) is passed in, then invoke + * it first. Then invoke tour-wide helper. + * + * @private + */ + invokeEventCallbacks: function invokeEventCallbacks(evtType, stepCb) { + var cbArr = callbacks[evtType], + callback, + fn, + i, + len; + + if (stepCb) { + return this.invokeCallback(stepCb); + } + + for (i = 0, len = cbArr.length; i < len; ++i) { + this.invokeCallback(cbArr[i].cb); + } + }, + + /** + * @private + */ + getScrollTop: function getScrollTop() { + var scrollTop; + if (_typeof(window.pageYOffset) !== undefinedStr) { + scrollTop = window.pageYOffset; + } else { + // Most likely IE <=8, which doesn't support pageYOffset + scrollTop = document.documentElement.scrollTop; + } + return scrollTop; + }, + + /** + * @private + */ + getScrollLeft: function getScrollLeft() { + var scrollLeft; + if (_typeof(window.pageXOffset) !== undefinedStr) { + scrollLeft = window.pageXOffset; + } else { + // Most likely IE <=8, which doesn't support pageXOffset + scrollLeft = document.documentElement.scrollLeft; + } + return scrollLeft; + }, + + /** + * @private + */ + getWindowHeight: function getWindowHeight() { + return window.innerHeight || document.documentElement.clientHeight; + }, + + /** + * @private + */ + addEvtListener: function addEvtListener(el, evtName, fn) { + if (el) { + return el.addEventListener ? el.addEventListener(evtName, fn, false) : el.attachEvent('on' + evtName, fn); + } + }, + + /** + * @private + */ + removeEvtListener: function removeEvtListener(el, evtName, fn) { + if (el) { + return el.removeEventListener ? el.removeEventListener(evtName, fn, false) : el.detachEvent('on' + evtName, fn); + } + }, + + documentIsReady: function documentIsReady() { + return document.readyState === 'complete'; + }, + + /** + * @private + */ + evtPreventDefault: function evtPreventDefault(evt) { + if (evt.preventDefault) { + evt.preventDefault(); + } else if (event) { + event.returnValue = false; + } + }, + + /** + * @private + */ + extend: function extend(obj1, obj2) { + var prop; + for (prop in obj2) { + if (obj2.hasOwnProperty(prop)) { + obj1[prop] = obj2[prop]; + } + } + }, + + /** + * Helper function to get a single target DOM element. We will try to + * locate the DOM element through several ways, in the following order: + * + * 1) Passing the string into document.querySelector + * 2) Passing the string to jQuery, if it exists + * 3) Passing the string to Sizzle, if it exists + * 4) Calling document.getElementById if it is a plain id + * + * Default case is to assume the string is a plain id and call + * document.getElementById on it. + * + * @private + */ + getStepTargetHelper: function getStepTargetHelper(target) { + var result = document.getElementById(target); + + //Backwards compatibility: assume the string is an id + if (result) { + return result; + } + if (hasJquery) { + result = jQuery(target); + return result.length ? result[0] : null; + } + if (Sizzle) { + result = new Sizzle(target); + return result.length ? result[0] : null; + } + if (document.querySelector) { + try { + return document.querySelector(target); + } catch (err) {} + } + // Regex test for id. Following the HTML 4 spec for valid id formats. + // (http://www.w3.org/TR/html4/types.html#type-id) + if (/^#[a-zA-Z][\w-_:.]*$/.test(target)) { + return document.getElementById(target.substring(1)); + } + + return null; + }, + + /** + * Given a step, returns the target DOM element associated with it. It is + * recommended to only assign one target per step. However, there are + * some use cases which require multiple step targets to be supplied. In + * this event, we will use the first target in the array that we can + * locate on the page. See the comments for getStepTargetHelper for more + * information. + * + * @private + */ + getStepTarget: function getStepTarget(step) { + var queriedTarget; + + if (!step || !step.target) { + return null; + } + + if (typeof step.target === 'string') { + //Just one target to test. Check and return its results. + return utils.getStepTargetHelper(step.target); + } else if (Array.isArray(step.target)) { + // Multiple items to check. Check each and return the first success. + // Assuming they are all strings. + var i, len; + + for (i = 0, len = step.target.length; i < len; i++) { + if (typeof step.target[i] === 'string') { + queriedTarget = utils.getStepTargetHelper(step.target[i]); + + if (queriedTarget) { + return queriedTarget; + } + } + } + return null; + } + + // Assume that the step.target is a DOM element + return step.target; + }, + + /** + * Convenience method for getting an i18n string. Returns custom i18n value + * or the default i18n value if no custom value exists. + * + * @private + */ + getI18NString: function getI18NString(key) { + return customI18N[key] || HopscotchI18N[key]; + }, + + // Tour session persistence for multi-page tours. Uses HTML5 sessionStorage if available, then + // falls back to using cookies. + // + // The following cookie-related logic is borrowed from: + // http://www.quirksmode.org/js/cookies.html + + /** + * @private + */ + setState: function setState(name, value, days) { + var expires = '', + date; + + if (hasSessionStorage && isStorageWritable) { + try { + sessionStorage.setItem(name, value); + } catch (err) { + isStorageWritable = false; + this.setState(name, value, days); + } + } else { + if (hasSessionStorage) { + //Clear out existing sessionStorage key so the new value we set to cookie gets read. + //(If we're here, we've run into an error while trying to write to sessionStorage). + sessionStorage.removeItem(name); + } + if (days) { + date = new Date(); + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); + expires = '; expires=' + date.toGMTString(); + } + document.cookie = name + '=' + value + expires + '; path=/'; + } + }, + + /** + * @private + */ + getState: function getState(name) { + var nameEQ = name + '=', + ca = document.cookie.split(';'), + i, + c, + state; + + //return value from session storage if we have it + if (hasSessionStorage) { + state = sessionStorage.getItem(name); + if (state) { + return state; + } + } + + //else, try cookies + for (i = 0; i < ca.length; i++) { + c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + state = c.substring(nameEQ.length, c.length); + break; + } + } + + return state; + }, + + /** + * @private + */ + clearState: function clearState(name) { + if (hasSessionStorage) { + sessionStorage.removeItem(name); + } else { + this.setState(name, '', -1); + } + }, + + /** + * Originally called it orientation, but placement is more intuitive. + * Allowing both for now for backwards compatibility. + * @private + */ + normalizePlacement: function normalizePlacement(step) { + if (!step.placement && step.orientation) { + step.placement = step.orientation; + } + }, + + /** + * If step is right-to-left enabled, flip the placement and xOffset, but only once. + * @private + */ + flipPlacement: function flipPlacement(step) { + if (step.isRtl && !step._isFlipped) { + var props = ['orientation', 'placement'], + prop, + i; + if (step.xOffset) { + step.xOffset = -1 * this.getPixelValue(step.xOffset); + } + for (i in props) { + prop = props[i]; + if (step.hasOwnProperty(prop) && rtlMatches.hasOwnProperty(step[prop])) { + step[prop] = rtlMatches[step[prop]]; + } + } + step._isFlipped = true; + } + } + }; + + utils.addEvtListener(window, 'load', winLoadHandler); + + callbacks = { + next: [], + prev: [], + start: [], + end: [], + show: [], + error: [], + close: [] + }; + + /** + * helpers + * ======= + * A map of functions to be used as callback listeners. Functions are + * added to and removed from the map using the functions + * Hopscotch.registerHelper() and Hopscotch.unregisterHelper(). + */ + helpers = {}; + + HopscotchI18N = { + stepNums: null, + nextBtn: 'Next', + prevBtn: 'Back', + doneBtn: 'Done', + skipBtn: 'Skip', + closeTooltip: 'Close' + }; + + customI18N = {}; // Developer's custom i18n strings goes here. + + /** + * HopscotchBubble + * + * @class The HopscotchBubble class represents the view of a bubble. This class is also used for Hopscotch callouts. + */ + HopscotchBubble = function HopscotchBubble(opt) { + this.init(opt); + }; + + HopscotchBubble.prototype = { + isShowing: false, + + currStep: undefined, + + /** + * setPosition + * + * Sets the position of the bubble using the bounding rectangle of the + * target element and the orientation and offset information specified by + * the JSON. + */ + setPosition: function setPosition(step) { + var bubbleBoundingHeight, + bubbleBoundingWidth, + boundingRect, + top, + left, + arrowOffset, + verticalLeftPosition, + targetEl = utils.getStepTarget(step), + el = this.element, + arrowEl = this.arrowEl, + arrowPos = step.isRtl ? 'right' : 'left'; + + utils.flipPlacement(step); + utils.normalizePlacement(step); + + bubbleBoundingWidth = el.offsetWidth; + bubbleBoundingHeight = el.offsetHeight; + utils.removeClass(el, 'fade-in-down fade-in-up fade-in-left fade-in-right'); + + // SET POSITION + boundingRect = targetEl.getBoundingClientRect(); + + verticalLeftPosition = step.isRtl ? boundingRect.right - bubbleBoundingWidth : boundingRect.left; + + if (step.placement === 'top') { + top = boundingRect.top - bubbleBoundingHeight - this.opt.arrowWidth; + left = verticalLeftPosition; + } else if (step.placement === 'bottom') { + top = boundingRect.bottom + this.opt.arrowWidth; + left = verticalLeftPosition; + } else if (step.placement === 'left') { + top = boundingRect.top; + left = boundingRect.left - bubbleBoundingWidth - this.opt.arrowWidth; + } else if (step.placement === 'right') { + top = boundingRect.top; + left = boundingRect.right + this.opt.arrowWidth; + } else { + throw new Error('Bubble placement failed because step.placement is invalid or undefined!'); + } + + // SET (OR RESET) ARROW OFFSETS + if (step.arrowOffset !== 'center') { + arrowOffset = utils.getPixelValue(step.arrowOffset); + } else { + arrowOffset = step.arrowOffset; + } + if (!arrowOffset) { + arrowEl.style.top = ''; + arrowEl.style[arrowPos] = ''; + } else if (step.placement === 'top' || step.placement === 'bottom') { + arrowEl.style.top = ''; + if (arrowOffset === 'center') { + arrowEl.style[arrowPos] = Math.floor(bubbleBoundingWidth / 2 - arrowEl.offsetWidth / 2) + 'px'; + } else { + // Numeric pixel value + arrowEl.style[arrowPos] = arrowOffset + 'px'; + } + } else if (step.placement === 'left' || step.placement === 'right') { + arrowEl.style[arrowPos] = ''; + if (arrowOffset === 'center') { + arrowEl.style.top = Math.floor(bubbleBoundingHeight / 2 - arrowEl.offsetHeight / 2) + 'px'; + } else { + // Numeric pixel value + arrowEl.style.top = arrowOffset + 'px'; + } + } + + // HORIZONTAL OFFSET + if (step.xOffset === 'center') { + left = boundingRect.left + targetEl.offsetWidth / 2 - bubbleBoundingWidth / 2; + } else { + left += utils.getPixelValue(step.xOffset); + } + // VERTICAL OFFSET + if (step.yOffset === 'center') { + top = boundingRect.top + targetEl.offsetHeight / 2 - bubbleBoundingHeight / 2; + } else { + top += utils.getPixelValue(step.yOffset); + } + + // ADJUST TOP FOR SCROLL POSITION + if (!step.fixedElement) { + top += utils.getScrollTop(); + left += utils.getScrollLeft(); + } + + // ACCOUNT FOR FIXED POSITION ELEMENTS + el.style.position = step.fixedElement ? 'fixed' : 'absolute'; + + el.style.top = top + 'px'; + el.style.left = left + 'px'; + }, + + /** + * Renders the bubble according to the step JSON. + * + * @param {Object} step Information defining how the bubble should look. + * @param {Number} idx The index of the step in the tour. Not used for callouts. + * @param {Function} callback Function to be invoked after rendering is finished. + */ + render: function render(step, idx, callback) { + var el = this.element, + tourSpecificRenderer, + customTourData, + unsafe, + currTour, + totalSteps, + totalStepsI18n, + nextBtnText, + isLast, + i, + opts; + + // Cache current step information. + if (step) { + this.currStep = step; + } else if (this.currStep) { + step = this.currStep; + } + + // Check current tour for total number of steps and custom render data + if (this.opt.isTourBubble) { + currTour = winHopscotch.getCurrTour(); + if (currTour) { + customTourData = currTour.customData; + tourSpecificRenderer = currTour.customRenderer; + step.isRtl = step.hasOwnProperty('isRtl') ? step.isRtl : currTour.hasOwnProperty('isRtl') ? currTour.isRtl : this.opt.isRtl; + unsafe = currTour.unsafe; + if (Array.isArray(currTour.steps)) { + totalSteps = currTour.steps.length; + totalStepsI18n = this._getStepI18nNum(this._getStepNum(totalSteps - 1)); + isLast = this._getStepNum(idx) === this._getStepNum(totalSteps - 1); + } + } + } else { + customTourData = step.customData; + tourSpecificRenderer = step.customRenderer; + unsafe = step.unsafe; + step.isRtl = step.hasOwnProperty('isRtl') ? step.isRtl : this.opt.isRtl; + } + + // Determine label for next button + if (isLast) { + nextBtnText = utils.getI18NString('doneBtn'); + } else if (step.showSkip) { + nextBtnText = utils.getI18NString('skipBtn'); + } else { + nextBtnText = utils.getI18NString('nextBtn'); + } + + utils.flipPlacement(step); + utils.normalizePlacement(step); + + this.placement = step.placement; + + // Setup the configuration options we want to pass along to the template + opts = { + i18n: { + prevBtn: utils.getI18NString('prevBtn'), + nextBtn: nextBtnText, + closeTooltip: utils.getI18NString('closeTooltip'), + stepNum: this._getStepI18nNum(this._getStepNum(idx)), + numSteps: totalStepsI18n + }, + buttons: { + showPrev: utils.valOrDefault(step.showPrevButton, this.opt.showPrevButton) && this._getStepNum(idx) > 0, + showNext: utils.valOrDefault(step.showNextButton, this.opt.showNextButton), + showCTA: utils.valOrDefault(step.showCTAButton && step.ctaLabel, false), + ctaLabel: step.ctaLabel, + showClose: utils.valOrDefault(this.opt.showCloseButton, true) + }, + step: { + num: idx, + isLast: utils.valOrDefault(isLast, false), + title: step.title || '', + content: step.content || '', + isRtl: step.isRtl, + placement: step.placement, + padding: utils.valOrDefault(step.padding, this.opt.bubblePadding), + width: utils.getPixelValue(step.width) || this.opt.bubbleWidth, + customData: step.customData || {} + }, + tour: { + isTour: this.opt.isTourBubble, + numSteps: totalSteps, + unsafe: utils.valOrDefault(unsafe, false), + customData: customTourData || {} + } + }; + + // Render the bubble's content. + // Use tour renderer if available, then the global customRenderer if defined. + if (typeof tourSpecificRenderer === 'function') { + el.innerHTML = tourSpecificRenderer(opts); + } else if (typeof tourSpecificRenderer === 'string') { + if (!winHopscotch.templates || typeof winHopscotch.templates[tourSpecificRenderer] !== 'function') { + throw new Error('Bubble rendering failed - template "' + tourSpecificRenderer + '" is not a function.'); + } + el.innerHTML = winHopscotch.templates[tourSpecificRenderer](opts); + } else if (customRenderer) { + el.innerHTML = customRenderer(opts); + } else { + if (!winHopscotch.templates || typeof winHopscotch.templates[templateToUse] !== 'function') { + throw new Error('Bubble rendering failed - template "' + templateToUse + '" is not a function.'); + } + el.innerHTML = winHopscotch.templates[templateToUse](opts); + } + + // Find arrow among new child elements. + var children = el.children; + var numChildren = children.length; + var node; + for (i = 0; i < numChildren; i++) { + node = children[i]; + + if (utils.hasClass(node, 'hopscotch-arrow')) { + this.arrowEl = node; + } + } + + // Set z-index and arrow placement + el.style.zIndex = typeof step.zindex === 'number' ? step.zindex : ''; + this._setArrow(step.placement); + + // Set bubble positioning + // Make sure we're using visibility:hidden instead of display:none for height/width calculations. + this.hide(false); + this.setPosition(step); + + // only want to adjust window scroll for non-fixed elements + if (callback) { + callback(!step.fixedElement); + } + + return this; + }, + /** + * Get step number considering steps that were skipped because their target wasn't found + * + * @private + */ + _getStepNum: function _getStepNum(idx) { + var skippedStepsCount = 0, + stepIdx, + skippedSteps = winHopscotch.getSkippedStepsIndexes(), + i, + len = skippedSteps.length; + //count number of steps skipped before current step + for (i = 0; i < len; i++) { + stepIdx = skippedSteps[i]; + if (stepIdx < idx) { + skippedStepsCount++; + } + } + return idx - skippedStepsCount; + }, + /** + * Get the I18N step number for the current step. + * + * @private + */ + _getStepI18nNum: function _getStepI18nNum(idx) { + var stepNumI18N = utils.getI18NString('stepNums'); + if (stepNumI18N && idx < stepNumI18N.length) { + idx = stepNumI18N[idx]; + } else { + idx = idx + 1; + } + return idx; + }, + + /** + * Sets which side the arrow is on. + * + * @private + */ + _setArrow: function _setArrow(placement) { + utils.removeClass(this.arrowEl, 'down up right left'); + + // Whatever the orientation is, we want to arrow to appear + // "opposite" of the orientation. E.g., a top orientation + // requires a bottom arrow. + if (placement === 'top') { + utils.addClass(this.arrowEl, 'down'); + } else if (placement === 'bottom') { + utils.addClass(this.arrowEl, 'up'); + } else if (placement === 'left') { + utils.addClass(this.arrowEl, 'right'); + } else if (placement === 'right') { + utils.addClass(this.arrowEl, 'left'); + } + }, + + /** + * @private + */ + _getArrowDirection: function _getArrowDirection() { + if (this.placement === 'top') { + return 'down'; + } + if (this.placement === 'bottom') { + return 'up'; + } + if (this.placement === 'left') { + return 'right'; + } + if (this.placement === 'right') { + return 'left'; + } + }, + + show: function show() { + var self = this, + fadeClass = 'fade-in-' + this._getArrowDirection(), + fadeDur = 1000; + + utils.removeClass(this.element, 'hide'); + utils.addClass(this.element, fadeClass); + setTimeout(function () { + utils.removeClass(self.element, 'invisible'); + }, 50); + setTimeout(function () { + utils.removeClass(self.element, fadeClass); + }, fadeDur); + this.isShowing = true; + return this; + }, + + hide: function hide(remove) { + var el = this.element; + + remove = utils.valOrDefault(remove, true); + el.style.top = ''; + el.style.left = ''; + + // display: none + if (remove) { + utils.addClass(el, 'hide'); + utils.removeClass(el, 'invisible'); + } + // opacity: 0 + else { + utils.removeClass(el, 'hide'); + utils.addClass(el, 'invisible'); + } + utils.removeClass(el, 'animate fade-in-up fade-in-down fade-in-right fade-in-left'); + this.isShowing = false; + return this; + }, + + destroy: function destroy() { + var el = this.element; + + if (el) { + el.parentNode.removeChild(el); + } + utils.removeEvtListener(el, 'click', this.clickCb); + }, + + _handleBubbleClick: function _handleBubbleClick(evt) { + var action; + + // Override evt for IE8 as IE8 doesn't pass event but binds it to window + evt = evt || window.event; // get window.event if argument is falsy (in IE) + + // get srcElement if target is falsy (IE) + var targetElement = evt.target || evt.srcElement; + + //Recursively look up the parent tree until we find a match + //with one of the classes we're looking for, or the triggering element. + function findMatchRecur(el) { + /* We're going to make the assumption that we're not binding + * multiple event classes to the same element. + * (next + previous = wait... err... what?) + * + * In the odd event we end up with an element with multiple + * possible matches, the following priority order is applied: + * hopscotch-cta, hopscotch-next, hopscotch-prev, hopscotch-close + */ + if (el === evt.currentTarget) { + return null; + } + if (utils.hasClass(el, 'hopscotch-cta')) { + return 'cta'; + } + if (utils.hasClass(el, 'hopscotch-next')) { + return 'next'; + } + if (utils.hasClass(el, 'hopscotch-prev')) { + return 'prev'; + } + if (utils.hasClass(el, 'hopscotch-close')) { + return 'close'; + } + /*else*/return findMatchRecur(el.parentElement); + } + + action = findMatchRecur(targetElement); + + //Now that we know what action we should take, let's take it. + if (action === 'cta') { + if (!this.opt.isTourBubble) { + // This is a callout. Close the callout when CTA is clicked. + winHopscotch.getCalloutManager().removeCallout(this.currStep.id); + } + // Call onCTA callback if one is provided + if (this.currStep.onCTA) { + utils.invokeCallback(this.currStep.onCTA); + } + } else if (action === 'next') { + winHopscotch.nextStep(true); + } else if (action === 'prev') { + winHopscotch.prevStep(true); + } else if (action === 'close') { + if (this.opt.isTourBubble) { + var currStepNum = winHopscotch.getCurrStepNum(), + currTour = winHopscotch.getCurrTour(), + doEndCallback = currStepNum === currTour.steps.length - 1; + + utils.invokeEventCallbacks('close'); + + winHopscotch.endTour(true, doEndCallback); + } else { + if (this.opt.onClose) { + utils.invokeCallback(this.opt.onClose); + } + if (this.opt.id && !this.opt.isTourBubble) { + // Remove via the HopscotchCalloutManager. + // removeCallout() calls HopscotchBubble.destroy internally. + winHopscotch.getCalloutManager().removeCallout(this.opt.id); + } else { + this.destroy(); + } + } + + utils.evtPreventDefault(evt); + } + //Otherwise, do nothing. We didn't click on anything relevant. + }, + + init: function init(initOpt) { + var el = document.createElement('div'), + self = this, + resizeCooldown = false, + // for updating after window resize + onWinResize, + _appendToBody2, + children, + numChildren, + node, + i, + currTour, + opt; + + //Register DOM element for this bubble. + this.element = el; + + //Merge bubble options with defaults. + opt = { + showPrevButton: defaultOpts.showPrevButton, + showNextButton: defaultOpts.showNextButton, + bubbleWidth: defaultOpts.bubbleWidth, + bubblePadding: defaultOpts.bubblePadding, + arrowWidth: defaultOpts.arrowWidth, + isRtl: defaultOpts.isRtl, + showNumber: true, + isTourBubble: true + }; + initOpt = (typeof initOpt === 'undefined' ? 'undefined' : _typeof(initOpt)) === undefinedStr ? {} : initOpt; + utils.extend(opt, initOpt); + this.opt = opt; + + //Apply classes to bubble. Add "animated" for fade css animation + el.className = 'hopscotch-bubble animated'; + if (!opt.isTourBubble) { + utils.addClass(el, 'hopscotch-callout no-number'); + } else { + currTour = winHopscotch.getCurrTour(); + if (currTour) { + utils.addClass(el, 'tour-' + currTour.id); + } + } + + /** + * Not pretty, but IE8 doesn't support Function.bind(), so I'm + * relying on closures to keep a handle of "this". + * Reset position of bubble when window is resized + * + * @private + */ + onWinResize = function onWinResize() { + if (resizeCooldown || !self.isShowing) { + return; + } + + resizeCooldown = true; + setTimeout(function () { + self.setPosition(self.currStep); + resizeCooldown = false; + }, 100); + }; + + //Add listener to reset bubble position on window resize + utils.addEvtListener(window, 'resize', onWinResize); + + //Create our click callback handler and keep a + //reference to it for later. + this.clickCb = function (evt) { + self._handleBubbleClick(evt); + }; + utils.addEvtListener(el, 'click', this.clickCb); + + //Hide the bubble by default + this.hide(); + + //Finally, append our new bubble to body once the DOM is ready. + if (utils.documentIsReady()) { + document.body.appendChild(el); + } else { + // Moz, webkit, Opera + if (document.addEventListener) { + _appendToBody2 = function appendToBody() { + document.removeEventListener('DOMContentLoaded', _appendToBody2); + window.removeEventListener('load', _appendToBody2); + + document.body.appendChild(el); + }; + + document.addEventListener('DOMContentLoaded', _appendToBody2, false); + } + // IE + else { + _appendToBody2 = function _appendToBody() { + if (document.readyState === 'complete') { + document.detachEvent('onreadystatechange', _appendToBody2); + window.detachEvent('onload', _appendToBody2); + document.body.appendChild(el); + } + }; + + document.attachEvent('onreadystatechange', _appendToBody2); + } + utils.addEvtListener(window, 'load', _appendToBody2); + } + } + }; + + /** + * HopscotchCalloutManager + * + * @class Manages the creation and destruction of single callouts. + * @constructor + */ + HopscotchCalloutManager = function HopscotchCalloutManager() { + var callouts = {}, + calloutOpts = {}; + + /** + * createCallout + * + * Creates a standalone callout. This callout has the same API + * as a Hopscotch tour bubble. + * + * @param {Object} opt The options for the callout. For the most + * part, these are the same options as you would find in a tour + * step. + */ + this.createCallout = function (opt) { + var callout; + + if (opt.id) { + if (!validIdRegEx.test(opt.id)) { + throw new Error('Callout ID is using an invalid format. Use alphanumeric, underscores, and/or hyphens only. First character must be a letter.'); + } + if (callouts[opt.id]) { + throw new Error('Callout by that id already exists. Please choose a unique id.'); + } + if (!utils.getStepTarget(opt)) { + throw new Error('Must specify existing target element via \'target\' option.'); + } + opt.showNextButton = opt.showPrevButton = false; + opt.isTourBubble = false; + callout = new HopscotchBubble(opt); + callouts[opt.id] = callout; + calloutOpts[opt.id] = opt; + callout.render(opt, null, function () { + callout.show(); + if (opt.onShow) { + utils.invokeCallback(opt.onShow); + } + }); + } else { + throw new Error('Must specify a callout id.'); + } + return callout; + }; + + /** + * getCallout + * + * Returns a callout by its id. + * + * @param {String} id The id of the callout to fetch. + * @returns {Object} HopscotchBubble + */ + this.getCallout = function (id) { + return callouts[id]; + }; + + /** + * removeAllCallouts + * + * Removes all existing callouts. + */ + this.removeAllCallouts = function () { + var calloutId; + + for (calloutId in callouts) { + if (callouts.hasOwnProperty(calloutId)) { + this.removeCallout(calloutId); + } + } + }; + + /** + * removeCallout + * + * Removes an existing callout by id. + * + * @param {String} id The id of the callout to remove. + */ + this.removeCallout = function (id) { + var callout = callouts[id]; + + callouts[id] = null; + calloutOpts[id] = null; + if (!callout) { + return; + } + + callout.destroy(); + }; + + /** + * refreshCalloutPositions + * + * Refresh the positions for all callouts known by the + * callout manager. Typically you'll use + * hopscotch.refreshBubblePosition() to refresh ALL + * bubbles instead of calling this directly. + */ + this.refreshCalloutPositions = function () { + var calloutId, callout, opts; + + for (calloutId in callouts) { + if (callouts.hasOwnProperty(calloutId) && calloutOpts.hasOwnProperty(calloutId)) { + callout = callouts[calloutId]; + opts = calloutOpts[calloutId]; + if (callout && opts) { + callout.setPosition(opts); + } + } + } + }; + }; + + /** + * Hopscotch + * + * @class Creates the Hopscotch object. Used to manage tour progress and configurations. + * @constructor + * @param {Object} initOptions Options to be passed to `configure()`. + */ + Hopscotch = function Hopscotch(initOptions) { + var self = this, + // for targetClickNextFn + bubble, + calloutMgr, + opt, + currTour, + currStepNum, + skippedSteps = {}, + cookieTourId, + cookieTourStep, + cookieSkippedSteps = [], + _configure, + + + /** + * getBubble + * + * Singleton accessor function for retrieving or creating bubble object. + * + * @private + * @param setOptions {Boolean} when true, transfers configuration options to the bubble + * @returns {Object} HopscotchBubble + */ + getBubble = function getBubble(setOptions) { + if (!bubble || !bubble.element || !bubble.element.parentNode) { + bubble = new HopscotchBubble(opt); + } + if (setOptions) { + utils.extend(bubble.opt, { + bubblePadding: getOption('bubblePadding'), + bubbleWidth: getOption('bubbleWidth'), + showNextButton: getOption('showNextButton'), + showPrevButton: getOption('showPrevButton'), + showCloseButton: getOption('showCloseButton'), + arrowWidth: getOption('arrowWidth'), + isRtl: getOption('isRtl') + }); + } + return bubble; + }, + + + /** + * Destroy the bubble currently associated with Hopscotch. + * This is done when we end the current tour. + * + * @private + */ + destroyBubble = function destroyBubble() { + if (bubble) { + bubble.destroy(); + bubble = null; + } + }, + + + /** + * Convenience method for getting an option. Returns custom config option + * or the default config option if no custom value exists. + * + * @private + * @param name {String} config option name + * @returns {Object} config option value + */ + getOption = function getOption(name) { + if (typeof opt === 'undefined') { + return defaultOpts[name]; + } + return utils.valOrDefault(opt[name], defaultOpts[name]); + }, + + + /** + * getCurrStep + * + * @private + * @returns {Object} the step object corresponding to the current value of currStepNum + */ + getCurrStep = function getCurrStep() { + var step; + + if (!currTour || currStepNum < 0 || currStepNum >= currTour.steps.length) { + step = null; + } else { + step = currTour.steps[currStepNum]; + } + + return step; + }, + + + /** + * Used for nextOnTargetClick + * + * @private + */ + targetClickNextFn = function targetClickNextFn() { + self.nextStep(); + }, + + + /** + * adjustWindowScroll + * + * Checks if the bubble or target element is partially or completely + * outside of the viewport. If it is, adjust the window scroll position + * to bring it back into the viewport. + * + * @private + * @param {Function} cb Callback to invoke after done scrolling. + */ + adjustWindowScroll = function adjustWindowScroll(cb) { + var bubble = getBubble(), + + + // Calculate the bubble element top and bottom position + bubbleEl = bubble.element, + bubbleTop = utils.getPixelValue(bubbleEl.style.top), + bubbleBottom = bubbleTop + utils.getPixelValue(bubbleEl.offsetHeight), + + + // Calculate the target element top and bottom position + targetEl = utils.getStepTarget(getCurrStep()), + targetBounds = targetEl.getBoundingClientRect(), + targetElTop = targetBounds.top + utils.getScrollTop(), + targetElBottom = targetBounds.bottom + utils.getScrollTop(), + + + // The higher of the two: bubble or target + targetTop = bubbleTop < targetElTop ? bubbleTop : targetElTop, + + // The lower of the two: bubble or target + targetBottom = bubbleBottom > targetElBottom ? bubbleBottom : targetElBottom, + + + // Calculate the current viewport top and bottom + windowTop = utils.getScrollTop(), + windowBottom = windowTop + utils.getWindowHeight(), + + + // This is our final target scroll value. + scrollToVal = targetTop - getOption('scrollTopMargin'), + scrollEl, + yuiAnim, + yuiEase, + direction, + scrollIncr, + scrollTimeout, + _scrollTimeoutFn; + + // Target and bubble are both visible in viewport + if (targetTop >= windowTop && (targetTop <= windowTop + getOption('scrollTopMargin') || targetBottom <= windowBottom)) { + if (cb) { + cb(); + } // HopscotchBubble.show + } + + // Abrupt scroll to scroll target + else if (!getOption('smoothScroll')) { + window.scrollTo(0, scrollToVal); + + if (cb) { + cb(); + } // HopscotchBubble.show + } + + // Smooth scroll to scroll target + else { + // Use YUI if it exists + if ((typeof YAHOO === 'undefined' ? 'undefined' : _typeof(YAHOO)) !== undefinedStr && _typeof(YAHOO.env) !== undefinedStr && _typeof(YAHOO.env.ua) !== undefinedStr && _typeof(YAHOO.util) !== undefinedStr && _typeof(YAHOO.util.Scroll) !== undefinedStr) { + scrollEl = YAHOO.env.ua.webkit ? document.body : document.documentElement; + yuiEase = YAHOO.util.Easing ? YAHOO.util.Easing.easeOut : undefined; + yuiAnim = new YAHOO.util.Scroll(scrollEl, { + scroll: { to: [0, scrollToVal] } + }, getOption('scrollDuration') / 1000, yuiEase); + yuiAnim.onComplete.subscribe(cb); + yuiAnim.animate(); + } + + // Use jQuery if it exists + else if (hasJquery) { + jQuery('body, html').animate({ scrollTop: scrollToVal }, getOption('scrollDuration'), cb); + } + + // Use my crummy setInterval scroll solution if we're using plain, vanilla Javascript. + else { + if (scrollToVal < 0) { + scrollToVal = 0; + } + + // 48 * 10 == 480ms scroll duration + // make it slightly less than CSS transition duration because of + // setInterval overhead. + // To increase or decrease duration, change the divisor of scrollIncr. + direction = windowTop > targetTop ? -1 : 1; // -1 means scrolling up, 1 means down + scrollIncr = Math.abs(windowTop - scrollToVal) / (getOption('scrollDuration') / 10); + _scrollTimeoutFn = function scrollTimeoutFn() { + var scrollTop = utils.getScrollTop(), + scrollTarget = scrollTop + direction * scrollIncr; + + if (direction > 0 && scrollTarget >= scrollToVal || direction < 0 && scrollTarget <= scrollToVal) { + // Overshot our target. Just manually set to equal the target + // and clear the interval + scrollTarget = scrollToVal; + if (cb) { + cb(); + } // HopscotchBubble.show + window.scrollTo(0, scrollTarget); + return; + } + + window.scrollTo(0, scrollTarget); + + if (utils.getScrollTop() === scrollTop) { + // Couldn't scroll any further. + if (cb) { + cb(); + } // HopscotchBubble.show + return; + } + + // If we reached this point, that means there's still more to scroll. + setTimeout(_scrollTimeoutFn, 10); + }; + + _scrollTimeoutFn(); + } + } + }, + + + /** + * goToStepWithTarget + * + * Helper function to increment the step number until a step is found where + * the step target exists or until we reach the end/beginning of the tour. + * + * @private + * @param {Number} direction Either 1 for incrementing or -1 for decrementing + * @param {Function} cb The callback function to be invoked when the step has been found + */ + goToStepWithTarget = function goToStepWithTarget(direction, cb) { + var target, step, goToStepFn; + + if (currStepNum + direction >= 0 && currStepNum + direction < currTour.steps.length) { + + currStepNum += direction; + step = getCurrStep(); + + goToStepFn = function goToStepFn() { + target = utils.getStepTarget(step); + + if (target) { + //this step was previously skipped, but now its target exists, + //remove this step from skipped steps set + if (skippedSteps[currStepNum]) { + delete skippedSteps[currStepNum]; + } + // We're done! Return the step number via the callback. + cb(currStepNum); + } else { + //mark this step as skipped, since its target wasn't found + skippedSteps[currStepNum] = true; + // Haven't found a valid target yet. Recursively call + // goToStepWithTarget. + utils.invokeEventCallbacks('error'); + goToStepWithTarget(direction, cb); + } + }; + + if (step.delay) { + setTimeout(goToStepFn, step.delay); + } else { + goToStepFn(); + } + } else { + cb(-1); // signal that we didn't find any step with a valid target + } + }, + + + /** + * changeStep + * + * Helper function to change step by going forwards or backwards 1. + * nextStep and prevStep are publicly accessible wrappers for this function. + * + * @private + * @param {Boolean} doCallbacks Flag for invoking onNext or onPrev callbacks + * @param {Number} direction Either 1 for "next" or -1 for "prev" + */ + changeStep = function changeStep(doCallbacks, direction) { + var bubble = getBubble(), + self = this, + step, + origStep, + wasMultiPage, + changeStepCb; + + bubble.hide(); + + doCallbacks = utils.valOrDefault(doCallbacks, true); + + step = getCurrStep(); + + if (step.nextOnTargetClick) { + // Detach the listener when tour is moving to a different step + utils.removeEvtListener(utils.getStepTarget(step), 'click', targetClickNextFn); + } + + origStep = step; + if (direction > 0) { + wasMultiPage = origStep.multipage; + } else { + wasMultiPage = currStepNum > 0 && currTour.steps[currStepNum - 1].multipage; + } + + /** + * Callback for goToStepWithTarget + * + * @private + */ + changeStepCb = function changeStepCb(stepNum) { + var doShowFollowingStep; + + if (stepNum === -1) { + // Wasn't able to find a step with an existing element. End tour. + return this.endTour(true); + } + + if (doCallbacks) { + if (direction > 0) { + doShowFollowingStep = utils.invokeEventCallbacks('next', origStep.onNext); + } else { + doShowFollowingStep = utils.invokeEventCallbacks('prev', origStep.onPrev); + } + } + + // If the state of the tour is updated in a callback, assume the client + // doesn't want to go to next step since they specifically updated. + if (stepNum !== currStepNum) { + return; + } + + if (wasMultiPage) { + // Update state for the next page + setStateHelper(); + + // Next step is on a different page, so no need to attempt to render it. + return; + } + + doShowFollowingStep = utils.valOrDefault(doShowFollowingStep, true); + + // If the onNext/onPrev callback returned false, halt the tour and + // don't show the next step. + if (doShowFollowingStep) { + this.showStep(stepNum); + } else { + // Halt tour (but don't clear state) + this.endTour(false); + } + }; + + if (!wasMultiPage && getOption('skipIfNoElement')) { + goToStepWithTarget(direction, function (stepNum) { + changeStepCb.call(self, stepNum); + }); + } else if (currStepNum + direction >= 0 && currStepNum + direction < currTour.steps.length) { + // only try incrementing once, and invoke error callback if no target is found + currStepNum += direction; + step = getCurrStep(); + if (!utils.getStepTarget(step) && !wasMultiPage) { + utils.invokeEventCallbacks('error'); + return this.endTour(true, false); + } + changeStepCb.call(this, currStepNum); + } else if (currStepNum + direction === currTour.steps.length) { + return this.endTour(); + } + + return this; + }, + + + /** + * loadTour + * + * Loads, but does not display, tour. + * + * @private + * @param tour The tour JSON object + */ + loadTour = function loadTour(tour) { + var tmpOpt = {}, + prop, + tourState, + tourStateValues; + + // Set tour-specific configurations + for (prop in tour) { + if (tour.hasOwnProperty(prop) && prop !== 'id' && prop !== 'steps') { + tmpOpt[prop] = tour[prop]; + } + } + + //this.resetDefaultOptions(); // reset all options so there are no surprises + // TODO check number of config properties of tour + _configure.call(this, tmpOpt, true); + + // Get existing tour state, if it exists. + tourState = utils.getState(getOption('cookieName')); + if (tourState) { + tourStateValues = tourState.split(':'); + cookieTourId = tourStateValues[0]; // selecting tour is not supported by this framework. + cookieTourStep = tourStateValues[1]; + + if (tourStateValues.length > 2) { + cookieSkippedSteps = tourStateValues[2].split(','); + } + + cookieTourStep = parseInt(cookieTourStep, 10); + } + + return this; + }, + + + /** + * Find the first step to show for a tour. (What is the first step with a + * target on the page?) + */ + findStartingStep = function findStartingStep(startStepNum, savedSkippedSteps, cb) { + var step, target; + + currStepNum = startStepNum || 0; + skippedSteps = savedSkippedSteps || {}; + step = getCurrStep(); + target = utils.getStepTarget(step); + + if (target) { + // First step had an existing target. + cb(currStepNum); + return; + } + + if (!target) { + // Previous target doesn't exist either. The user may have just + // clicked on a link that wasn't part of the tour. Another possibility is that + // the user clicked on the correct link, but the target is just missing for + // whatever reason. In either case, we should just advance until we find a step + // that has a target on the page or end the tour if we can't find such a step. + utils.invokeEventCallbacks('error'); + + //this step was skipped, since its target does not exist + skippedSteps[currStepNum] = true; + + if (getOption('skipIfNoElement')) { + goToStepWithTarget(1, cb); + return; + } else { + currStepNum = -1; + cb(currStepNum); + } + } + }, + showStepHelper = function showStepHelper(stepNum) { + var step = currTour.steps[stepNum], + bubble = getBubble(), + targetEl = utils.getStepTarget(step); + + function showBubble() { + bubble.show(); + utils.invokeEventCallbacks('show', step.onShow); + } + + if (currStepNum !== stepNum && getCurrStep().nextOnTargetClick) { + // Detach the listener when tour is moving to a different step + utils.removeEvtListener(utils.getStepTarget(getCurrStep()), 'click', targetClickNextFn); + } + + // Update bubble for current step + currStepNum = stepNum; + + bubble.hide(false); + + bubble.render(step, stepNum, function (adjustScroll) { + // when done adjusting window scroll, call showBubble helper fn + if (adjustScroll) { + adjustWindowScroll(showBubble); + } else { + showBubble(); + } + + // If we want to advance to next step when user clicks on target. + if (step.nextOnTargetClick) { + utils.addEvtListener(targetEl, 'click', targetClickNextFn); + } + }); + + setStateHelper(); + }, + setStateHelper = function setStateHelper() { + var cookieVal = currTour.id + ':' + currStepNum, + skipedStepIndexes = winHopscotch.getSkippedStepsIndexes(); + + if (skipedStepIndexes && skipedStepIndexes.length > 0) { + cookieVal += ':' + skipedStepIndexes.join(','); + } + + utils.setState(getOption('cookieName'), cookieVal, 1); + }, + + + /** + * init + * + * Initializes the Hopscotch object. + * + * @private + */ + init = function init(initOptions) { + if (initOptions) { + //initOptions.cookieName = initOptions.cookieName || 'hopscotch.tour.state'; + this.configure(initOptions); + } + }; + + /** + * getCalloutManager + * + * Gets the callout manager. + * + * @returns {Object} HopscotchCalloutManager + * + */ + this.getCalloutManager = function () { + if ((typeof calloutMgr === 'undefined' ? 'undefined' : _typeof(calloutMgr)) === undefinedStr) { + calloutMgr = new HopscotchCalloutManager(); + } + + return calloutMgr; + }; + + /** + * startTour + * + * Begins the tour. + * + * @param {Object} tour The tour JSON object + * @stepNum {Number} stepNum __Optional__ The step number to start from + * @returns {Object} Hopscotch + * + */ + this.startTour = function (tour, stepNum) { + var bubble, + currStepNum, + skippedSteps = {}, + self = this; + + // loadTour if we are calling startTour directly. (When we call startTour + // from window onLoad handler, we'll use currTour) + if (!currTour) { + + // Sanity check! Is there a tour? + if (!tour) { + throw new Error('Tour data is required for startTour.'); + } + + // Check validity of tour ID. If invalid, throw an error. + if (!tour.id || !validIdRegEx.test(tour.id)) { + throw new Error('Tour ID is using an invalid format. Use alphanumeric, underscores, and/or hyphens only. First character must be a letter.'); + } + + currTour = tour; + loadTour.call(this, tour); + } + + if ((typeof stepNum === 'undefined' ? 'undefined' : _typeof(stepNum)) !== undefinedStr) { + if (stepNum >= currTour.steps.length) { + throw new Error('Specified step number out of bounds.'); + } + currStepNum = stepNum; + } + + // If document isn't ready, wait for it to finish loading. + // (so that we can calculate positioning accurately) + if (!utils.documentIsReady()) { + waitingToStart = true; + return this; + } + + if (typeof currStepNum === "undefined" && currTour.id === cookieTourId && (typeof cookieTourStep === 'undefined' ? 'undefined' : _typeof(cookieTourStep)) !== undefinedStr) { + currStepNum = cookieTourStep; + if (cookieSkippedSteps.length > 0) { + for (var i = 0, len = cookieSkippedSteps.length; i < len; i++) { + skippedSteps[cookieSkippedSteps[i]] = true; + } + } + } else if (!currStepNum) { + currStepNum = 0; + } + + // Find the current step we should begin the tour on, and then actually start the tour. + findStartingStep(currStepNum, skippedSteps, function (stepNum) { + var target = stepNum !== -1 && utils.getStepTarget(currTour.steps[stepNum]); + + if (!target) { + // Should we trigger onEnd callback? Let's err on the side of caution + // and not trigger it. Don't want weird stuff happening on a page that + // wasn't meant for the tour. Up to the developer to fix their tour. + self.endTour(false, false); + return; + } + + utils.invokeEventCallbacks('start'); + + bubble = getBubble(); + // TODO: do we still need this call to .hide()? No longer using opt.animate... + // Leaving it in for now to play it safe + bubble.hide(false); // make invisible for boundingRect calculations when opt.animate == true + + self.isActive = true; + + if (!utils.getStepTarget(getCurrStep())) { + // First step element doesn't exist + utils.invokeEventCallbacks('error'); + if (getOption('skipIfNoElement')) { + self.nextStep(false); + } + } else { + self.showStep(stepNum); + } + }); + + return this; + }; + + /** + * showStep + * + * Skips to a specific step and renders the corresponding bubble. + * + * @stepNum {Number} stepNum The step number to show + * @returns {Object} Hopscotch + */ + this.showStep = function (stepNum) { + var step = currTour.steps[stepNum], + prevStepNum = currStepNum; + if (!utils.getStepTarget(step)) { + currStepNum = stepNum; + utils.invokeEventCallbacks('error'); + currStepNum = prevStepNum; + return; + } + + if (step.delay) { + setTimeout(function () { + showStepHelper(stepNum); + }, step.delay); + } else { + showStepHelper(stepNum); + } + return this; + }; + + /** + * prevStep + * + * Jump to the previous step. + * + * @param {Boolean} doCallbacks Flag for invoking onPrev callback. Defaults to true. + * @returns {Object} Hopscotch + */ + this.prevStep = function (doCallbacks) { + changeStep.call(this, doCallbacks, -1); + return this; + }; + + /** + * nextStep + * + * Jump to the next step. + * + * @param {Boolean} doCallbacks Flag for invoking onNext callback. Defaults to true. + * @returns {Object} Hopscotch + */ + this.nextStep = function (doCallbacks) { + changeStep.call(this, doCallbacks, 1); + return this; + }; + + /** + * endTour + * + * Cancels out of an active tour. + * + * @param {Boolean} clearState Flag for clearing state. Defaults to true. + * @param {Boolean} doCallbacks Flag for invoking 'onEnd' callbacks. Defaults to true. + * @returns {Object} Hopscotch + */ + this.endTour = function (clearState, doCallbacks) { + var bubble = getBubble(), + currentStep; + + clearState = utils.valOrDefault(clearState, true); + doCallbacks = utils.valOrDefault(doCallbacks, true); + + //remove event listener if current step had it added + if (currTour) { + currentStep = getCurrStep(); + if (currentStep && currentStep.nextOnTargetClick) { + utils.removeEvtListener(utils.getStepTarget(currentStep), 'click', targetClickNextFn); + } + } + + currStepNum = 0; + cookieTourStep = undefined; + + bubble.hide(); + if (clearState) { + utils.clearState(getOption('cookieName')); + } + if (this.isActive) { + this.isActive = false; + + if (currTour && doCallbacks) { + utils.invokeEventCallbacks('end'); + } + } + + this.removeCallbacks(null, true); + this.resetDefaultOptions(); + destroyBubble(); + + currTour = null; + + return this; + }; + + /** + * getCurrTour + * + * @return {Object} The currently loaded tour. + */ + this.getCurrTour = function () { + return currTour; + }; + + /** + * getCurrTarget + * + * @return {Object} The currently visible target. + */ + this.getCurrTarget = function () { + return utils.getStepTarget(getCurrStep()); + }; + + /** + * getCurrStepNum + * + * @return {number} The current zero-based step number. + */ + this.getCurrStepNum = function () { + return currStepNum; + }; + + /** + * getSkippedStepsIndexes + * + * @return {Array} Array of skipped step indexes + */ + this.getSkippedStepsIndexes = function () { + var skippedStepsIdxArray = [], + stepIds; + + for (stepIds in skippedSteps) { + skippedStepsIdxArray.push(stepIds); + } + + return skippedStepsIdxArray; + }; + + /** + * refreshBubblePosition + * + * Tell hopscotch that the position of the current tour element changed + * and the bubble therefore needs to be redrawn. Also refreshes position + * of all Hopscotch Callouts on the page. + * + * @returns {Object} Hopscotch + */ + this.refreshBubblePosition = function () { + var currStep = getCurrStep(); + if (currStep) { + getBubble().setPosition(currStep); + } + this.getCalloutManager().refreshCalloutPositions(); + return this; + }; + + /** + * listen + * + * Adds a callback for one of the event types. Valid event types are: + * + * @param {string} evtType "start", "end", "next", "prev", "show", "close", or "error" + * @param {Function} cb The callback to add. + * @param {Boolean} isTourCb Flag indicating callback is from a tour definition. + * For internal use only! + * @returns {Object} Hopscotch + */ + this.listen = function (evtType, cb, isTourCb) { + if (evtType) { + callbacks[evtType].push({ cb: cb, fromTour: isTourCb }); + } + return this; + }; + + /** + * unlisten + * + * Removes a callback for one of the event types, e.g. 'start', 'next', etc. + * + * @param {string} evtType "start", "end", "next", "prev", "show", "close", or "error" + * @param {Function} cb The callback to remove. + * @returns {Object} Hopscotch + */ + this.unlisten = function (evtType, cb) { + var evtCallbacks = callbacks[evtType], + i, + len; + + for (i = 0, len = evtCallbacks.length; i < len; ++i) { + if (evtCallbacks[i].cb === cb) { + evtCallbacks.splice(i, 1); + } + } + return this; + }; + + /** + * removeCallbacks + * + * Remove callbacks for hopscotch events. If tourOnly is set to true, only + * removes callbacks specified by a tour (callbacks set by external calls + * to hopscotch.configure or hopscotch.listen will not be removed). If + * evtName is null or undefined, callbacks for all events will be removed. + * + * @param {string} evtName Optional Event name for which we should remove callbacks + * @param {boolean} tourOnly Optional flag to indicate we should only remove callbacks added + * by a tour. Defaults to false. + * @returns {Object} Hopscotch + */ + this.removeCallbacks = function (evtName, tourOnly) { + var cbArr, i, len, evt; + + // If evtName is null or undefined, remove callbacks for all events. + for (evt in callbacks) { + if (!evtName || evtName === evt) { + if (tourOnly) { + cbArr = callbacks[evt]; + for (i = 0, len = cbArr.length; i < len; ++i) { + if (cbArr[i].fromTour) { + cbArr.splice(i--, 1); + --len; + } + } + } else { + callbacks[evt] = []; + } + } + } + return this; + }; + + /** + * registerHelper + * ============== + * Registers a helper function to be used as a callback function. + * + * @param {String} id The id of the function. + * @param {Function} id The callback function. + */ + this.registerHelper = function (id, fn) { + if (typeof id === 'string' && typeof fn === 'function') { + helpers[id] = fn; + } + }; + + this.unregisterHelper = function (id) { + helpers[id] = null; + }; + + this.invokeHelper = function (id) { + var args = [], + i, + len; + + for (i = 1, len = arguments.length; i < len; ++i) { + args.push(arguments[i]); + } + if (helpers[id]) { + helpers[id].call(null, args); + } + }; + + /** + * setCookieName + * + * Sets the cookie name (or sessionStorage name, if supported) used for multi-page + * tour persistence. + * + * @param {String} name The cookie name + * @returns {Object} Hopscotch + */ + this.setCookieName = function (name) { + opt.cookieName = name; + return this; + }; + + /** + * resetDefaultOptions + * + * Resets all configuration options to default. + * + * @returns {Object} Hopscotch + */ + this.resetDefaultOptions = function () { + opt = {}; + return this; + }; + + /** + * resetDefaultI18N + * + * Resets all i18n. + * + * @returns {Object} Hopscotch + */ + this.resetDefaultI18N = function () { + customI18N = {}; + return this; + }; + + /** + * hasState + * + * Returns state from a previous tour run, if it exists. + * + * @returns {String} State of previous tour run, or empty string if none exists. + */ + this.getState = function () { + return utils.getState(getOption('cookieName')); + }; + + /** + * _configure + * + * @see this.configure + * @private + * @param options + * @param {Boolean} isTourOptions Should be set to true when setting options from a tour definition. + */ + _configure = function _configure(options, isTourOptions) { + var bubble, + events = ['next', 'prev', 'start', 'end', 'show', 'error', 'close'], + eventPropName, + callbackProp, + i, + len; + + if (!opt) { + this.resetDefaultOptions(); + } + + utils.extend(opt, options); + + if (options) { + utils.extend(customI18N, options.i18n); + } + + for (i = 0, len = events.length; i < len; ++i) { + // At this point, options[eventPropName] may have changed from an array + // to a function. + eventPropName = 'on' + events[i].charAt(0).toUpperCase() + events[i].substring(1); + if (options[eventPropName]) { + this.listen(events[i], options[eventPropName], isTourOptions); + } + } + + bubble = getBubble(true); + + return this; + }; + + /** + * configure + * + *
    +     * VALID OPTIONS INCLUDE...
    +     *
    +     * - bubbleWidth:     Number   - Default bubble width. Defaults to 280.
    +     * - bubblePadding:   Number   - DEPRECATED. Default bubble padding. Defaults to 15.
    +     * - smoothScroll:    Boolean  - should the page scroll smoothly to the next
    +     *                               step? Defaults to TRUE.
    +     * - scrollDuration:  Number   - Duration of page scroll. Only relevant when
    +     *                               smoothScroll is set to true. Defaults to
    +     *                               1000ms.
    +     * - scrollTopMargin: NUMBER   - When the page scrolls, how much space should there
    +     *                               be between the bubble/targetElement and the top
    +     *                               of the viewport? Defaults to 200.
    +     * - showCloseButton: Boolean  - should the tour bubble show a close (X) button?
    +     *                               Defaults to TRUE.
    +     * - showPrevButton:  Boolean  - should the bubble have the Previous button?
    +     *                               Defaults to FALSE.
    +     * - showNextButton:  Boolean  - should the bubble have the Next button?
    +     *                               Defaults to TRUE.
    +     * - arrowWidth:      Number   - Default arrow width. (space between the bubble
    +     *                               and the targetEl) Used for bubble position
    +     *                               calculation. Only use this option if you are
    +     *                               using your own custom CSS. Defaults to 20.
    +     * - skipIfNoElement  Boolean  - If a specified target element is not found,
    +     *                               should we skip to the next step? Defaults to
    +     *                               TRUE.
    +     * - onNext:          Function - A callback to be invoked after every click on
    +     *                               a "Next" button.
    +     * - isRtl:           Boolean  - Set to true when instantiating in a right-to-left
    +     *                               language environment, or if mirrored positioning is
    +     *                               needed.
    +     *                               Defaults to FALSE.
    +     *
    +     * - i18n:            Object   - For i18n purposes. Allows you to change the
    +     *                               text of button labels and step numbers.
    +     * - i18n.stepNums:   Array\ - Provide a list of strings to be shown as
    +     *                               the step number, based on index of array. Unicode
    +     *                               characters are supported. (e.g., ['一',
    +     *                               '二', '三']) If there are more steps
    +     *                               than provided numbers, Arabic numerals
    +     *                               ('4', '5', '6', etc.) will be used as default.
    +     * // =========
    +     * // CALLBACKS
    +     * // =========
    +     * - onNext:          Function - Invoked after every click on a "Next" button.
    +     * - onPrev:          Function - Invoked after every click on a "Prev" button.
    +     * - onStart:         Function - Invoked when the tour is started.
    +     * - onEnd:           Function - Invoked when the tour ends.
    +     * - onClose:         Function - Invoked when the user closes the tour before finishing.
    +     * - onError:         Function - Invoked when the specified target element doesn't exist on the page.
    +     *
    +     * // ====
    +     * // I18N
    +     * // ====
    +     * i18n:              OBJECT      - For i18n purposes. Allows you to change the text
    +     *                                  of button labels and step numbers.
    +     * i18n.nextBtn:      STRING      - Label for next button
    +     * i18n.prevBtn:      STRING      - Label for prev button
    +     * i18n.doneBtn:      STRING      - Label for done button
    +     * i18n.skipBtn:      STRING      - Label for skip button
    +     * i18n.closeTooltip: STRING      - Text for close button tooltip
    +     * i18n.stepNums:   ARRAY - Provide a list of strings to be shown as
    +     *                                  the step number, based on index of array. Unicode
    +     *                                  characters are supported. (e.g., ['一',
    +     *                                  '二', '三']) If there are more steps
    +     *                                  than provided numbers, Arabic numerals
    +     *                                  ('4', '5', '6', etc.) will be used as default.
    +     * 
    + * + * @example hopscotch.configure({ scrollDuration: 1000, scrollTopMargin: 150 }); + * @example + * hopscotch.configure({ + * scrollTopMargin: 150, + * onStart: function() { + * alert("Have fun!"); + * }, + * i18n: { + * nextBtn: 'Forward', + * prevBtn: 'Previous' + * closeTooltip: 'Quit' + * } + * }); + * + * @param {Object} options A hash of configuration options. + * @returns {Object} Hopscotch + */ + this.configure = function (options) { + return _configure.call(this, options, false); + }; + + /** + * Set the template that should be used for rendering Hopscotch bubbles. + * If a string, it's assumed your template is available in the + * hopscotch.templates namespace. + * + * @param {String|Function(obj)} The template to use for rendering. + * @returns {Object} The Hopscotch object (for chaining). + */ + this.setRenderer = function (render) { + var typeOfRender = typeof render === 'undefined' ? 'undefined' : _typeof(render); + + if (typeOfRender === 'string') { + templateToUse = render; + customRenderer = undefined; + } else if (typeOfRender === 'function') { + customRenderer = render; + } + return this; + }; + + /** + * Sets the escaping method to be used by JST templates. + * + * @param {Function} - The escape method to use. + * @returns {Object} The Hopscotch object (for chaining). + */ + this.setEscaper = function (esc) { + if (typeof esc === 'function') { + customEscape = esc; + } + return this; + }; + + init.call(this, initOptions); + }; + + winHopscotch = new Hopscotch(); + + // Template includes, placed inside a closure to ensure we don't + // end up declaring our shim globally. + (function () { + var _ = {}; +/* + * Adapted from the Underscore.js framework. Check it out at + * https://github.com/jashkenas/underscore + */ +_.escape = function(str){ + if(customEscape){ return customEscape(str); } + + if(str == null) return ''; + return ('' + str).replace(new RegExp('[&<>"\']', 'g'), function(match){ + if(match == '&'){ return '&' } + if(match == '<'){ return '<' } + if(match == '>'){ return '>' } + if(match == '"'){ return '"' } + if(match == "'"){ return ''' } + }); +} + + this["templates"] = this["templates"] || {}; + +this["templates"]["bubble_default"] = function(data) { +var __t, __p = '', __e = _.escape, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } + + + function optEscape(str, unsafe){ + if(unsafe){ + return _.escape(str); + } + return str; + } +; +__p += '\n'; + +var i18n = data.i18n; +var buttons = data.buttons; +var step = data.step; +var tour = data.tour; +; +__p += '\n
    \n '; + if(tour.isTour){ ; +__p += '' + +((__t = ( i18n.stepNum )) == null ? '' : __t) + +''; + } ; +__p += '\n
    \n '; + if(step.title !== ''){ ; +__p += '

    ' + +((__t = ( optEscape(step.title, tour.unsafe) )) == null ? '' : __t) + +'

    '; + } ; +__p += '\n '; + if(step.content !== ''){ ; +__p += '
    ' + +((__t = ( optEscape(step.content, tour.unsafe) )) == null ? '' : __t) + +'
    '; + } ; +__p += '\n
    \n
    \n '; + if(buttons.showPrev){ ; +__p += ''; + } ; +__p += '\n '; + if(buttons.showCTA){ ; +__p += ''; + } ; +__p += '\n '; + if(buttons.showNext){ ; +__p += ''; + } ; +__p += '\n
    \n '; + if(buttons.showClose){ ; +__p += ''; + } ; +__p += '\n
    \n
    \n
    \n
    \n
    \n'; +return __p +}; + }).call(winHopscotch); + + var winHopscotch$1 = winHopscotch; + + return winHopscotch$1; + +})));