From 8b81d747a0903823bfd9b28ec8d723dcae34a868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Robles?= Date: Wed, 17 Jul 2024 08:40:47 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Voter=20eligibility=20check=20(#454?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parent issue: https://github.com/sequentech/meta/issues/234 --- app.js | 8 + app.less | 1 + .../booth-children-elections-directive.html | 2 +- .../booth-children-elections-directive.js | 1 + avBooth/booth-directive/booth-directive.html | 2 + avBooth/booth-directive/booth-directive.js | 4 +- avBooth/booth.html | 1 + avBooth/booth.js | 1 + .../election-chooser-screen-directive.js | 10 + .../voter-eligibility-screen-directive.html | 44 ++++ .../voter-eligibility-screen-directive.js | 209 ++++++++++++++++++ .../voter-eligibility-screen-directive.less | 2 + index.html | 1 + 13 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.html create mode 100644 avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.js create mode 100644 avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.less diff --git a/app.js b/app.js index bd16739b..d2c304ee 100755 --- a/app.js +++ b/app.js @@ -206,6 +206,14 @@ angular.module('voting-booth').config( isDemo: true } }) + .state('election.booth-eligibility', { + url: '/:id/eligibility', + templateUrl: 'avBooth/booth.html', + controller: "BoothController", + params: { + isEligibility: true + } + }) .state('election.booth-preview', { url: '/:id/preview-vote', templateUrl: 'avBooth/booth.html', diff --git a/app.less b/app.less index 383b4be1..098997d0 100755 --- a/app.less +++ b/app.less @@ -19,6 +19,7 @@ @import "avBooth/booth.less"; @import "avBooth/2questions-conditional-screen-directive/2questions-conditional-screen-directive.less"; @import "avBooth/election-chooser-screen-directive/election-chooser-screen-directive.less"; +@import "avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.less"; @import "avBooth/voting-step-directive/voting-step-directive.less"; @import "avBooth/watermark-directive/watermark-directive.less"; @import "avBooth/accordion-option-directive/accordion-option-directive.less"; diff --git a/avBooth/booth-children-elections-directive/booth-children-elections-directive.html b/avBooth/booth-children-elections-directive/booth-children-elections-directive.html index 649da3f5..5b7d13e2 100644 --- a/avBooth/booth-children-elections-directive/booth-children-elections-directive.html +++ b/avBooth/booth-children-elections-directive/booth-children-elections-directive.html @@ -91,7 +91,7 @@

{{electio diff --git a/avBooth/booth-children-elections-directive/booth-children-elections-directive.js b/avBooth/booth-children-elections-directive/booth-children-elections-directive.js index 94e827fb..f5a3bdc5 100644 --- a/avBooth/booth-children-elections-directive/booth-children-elections-directive.js +++ b/avBooth/booth-children-elections-directive/booth-children-elections-directive.js @@ -63,6 +63,7 @@ angular.module('avUi') { if (!scope.canVote) { console.log("user cannot vote, so ignoring click"); + return; } if (scope.hasVoted) { console.log("user has already voted, so ignoring click"); diff --git a/avBooth/booth-directive/booth-directive.html b/avBooth/booth-directive/booth-directive.html index 1b7fe007..259c0aa4 100644 --- a/avBooth/booth-directive/booth-directive.html +++ b/avBooth/booth-directive/booth-directive.html @@ -4,6 +4,8 @@
+
+
diff --git a/avBooth/booth-directive/booth-directive.js b/avBooth/booth-directive/booth-directive.js index aca29f8b..a370946c 100644 --- a/avBooth/booth-directive/booth-directive.js +++ b/avBooth/booth-directive/booth-directive.js @@ -46,6 +46,7 @@ angular.module('avBooth') scope.isDemo = (attrs.isDemo === "true"); scope.isPreview = (attrs.isPreview === "true"); scope.isUuidPreview = (attrs.isUuidPreview === "true"); + scope.isEligibility = (attrs.isEligibility === "true"); scope.documentation = ConfigService.documentation; scope.hasSeenStartScreenInThisSession = false; @@ -68,7 +69,8 @@ angular.module('avBooth') castingBallotScreen: 'castingBallotScreen', successScreen: 'successScreen', showPdf: 'showPdf', - simultaneousQuestionsV2Screen: 'simultaneousQuestionsV2Screen' + simultaneousQuestionsV2Screen: 'simultaneousQuestionsV2Screen', + voterEligibilityScreen: 'voterEligibilityScreen' }; // This is used to enable custom css overriding diff --git a/avBooth/booth.html b/avBooth/booth.html index c5fe2570..23cac9e5 100644 --- a/avBooth/booth.html +++ b/avBooth/booth.html @@ -6,5 +6,6 @@ is-preview="{{isPreview}}" is-uuid-preview="{{isUuidPreview}}" preview-election="{{previewElection}}" + is-eligibility="{{isEligibility}}" >
diff --git a/avBooth/booth.js b/avBooth/booth.js index d12da70e..6d07f099 100644 --- a/avBooth/booth.js +++ b/avBooth/booth.js @@ -49,6 +49,7 @@ angular $scope.previewElection = previewElectionParam && decodeURIComponent(previewElectionParam); $scope.isPreview = $stateParams.isPreview || false; $scope.isUuidPreview = $stateParams.isUuidPreview || false; + $scope.isEligibility = $stateParams.isEligibility || false; $scope.electionId = $stateParams.id; $scope.baseUrl = ConfigService.baseUrl; $scope.config = $filter('json')(ConfigService); diff --git a/avBooth/election-chooser-screen-directive/election-chooser-screen-directive.js b/avBooth/election-chooser-screen-directive/election-chooser-screen-directive.js index 3e939b06..d5751488 100644 --- a/avBooth/election-chooser-screen-directive/election-chooser-screen-directive.js +++ b/avBooth/election-chooser-screen-directive/election-chooser-screen-directive.js @@ -90,6 +90,9 @@ angular.module('avBooth') election.event_id, credentials ); var canVote = calculateCanVote(elCredentials); + if (canVote) { + scope.canVote = true; + } var isVoter = calculateIsVoter(elCredentials); if ( elCredentials && @@ -201,6 +204,13 @@ angular.module('avBooth') checkDisabled(); scope.chooseElection = chooseElection; + scope.goToVoterEligibility = function () { + scope.setState(scope.stateEnum.voterEligibilityScreen, {}); + }; + + if (scope.isEligibility) { + scope.goToVoterEligibility(); + } scope.showHelp = function () { $modal.open({ diff --git a/avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.html b/avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.html new file mode 100644 index 00000000..b6a96341 --- /dev/null +++ b/avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.html @@ -0,0 +1,44 @@ + + + +
+
+ + +

+

+ +
+
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.js b/avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.js new file mode 100644 index 00000000..de70903f --- /dev/null +++ b/avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.js @@ -0,0 +1,209 @@ +/** + * This file is part of voting-booth. + * Copyright (C) 2024 Sequent Tech Inc + + * voting-booth 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. + + * voting-booth 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 voting-booth. If not, see . +**/ + +angular.module('avBooth') + .directive('avbVoterEligibilityScreen', function($window, $timeout, $q, $modal, ConfigService) { + + function link(scope, element, attrs) { + scope.showSkippedElections = false; + scope.organization = ConfigService.organization; + function findElectionCredentials(electionId, credentials) { + return _.find( + credentials, + function (credential) { + return credential.electionId === electionId; + } + ); + } + + function calculateCanVote(elCredentials) { + return ( + !!elCredentials && + !!elCredentials.token && + ( + elCredentials.numSuccessfulLogins < elCredentials.numSuccessfulLoginsAllowed || + elCredentials.numSuccessfulLoginsAllowed === 0 + ) + ); + } + + function calculateIsVoter(elCredentials) { + return ( + !!elCredentials && + elCredentials.numSuccessfulLoginsAllowed !== -1 + ); + } + + function getElectionCredentials() { + // need to reload in case this changed in success screen.. + var credentialsStr = $window.sessionStorage.getItem("vote_permission_tokens"); + return JSON.parse(credentialsStr); + } + + function isChooserDisabled() { + return ( + scope.parentElection && + scope.parentElection.presentation && + scope.parentElection.presentation.extra_options && + !!scope.parentElection.presentation.extra_options.disable__election_chooser_screen + ); + } + + function generateChildrenInfo() { + var childrenInfo = angular.copy( + scope.parentAuthEvent.children_election_info + ); + + // need to reload in case this changed in success screen.. + var credentials = getElectionCredentials(); + + // if it's a demo, yes, allow voting by default + scope.canVote = scope.isDemo || scope.isPreview; + scope.hasVoted = false; + scope.skippedElections = []; + childrenInfo.presentation.categories = _.map( + childrenInfo.presentation.categories, + function (category) { + category.events = _.map( + category.events, + function (election) { + var elCredentials = findElectionCredentials( + election.event_id, credentials + ); + var canVote = calculateCanVote(elCredentials); + var isVoter = calculateIsVoter(elCredentials); + if ( + elCredentials && + elCredentials.numSuccessfulLogins > 0 + ) { + scope.hasVoted = true; + } + var retValue = Object.assign( + {}, + election, + elCredentials || {}, + { + disabled: (!scope.isDemo && !scope.isPreview && !canVote), + hidden: (!scope.isDemo && !scope.isPreview && !isVoter) + } + ); + if (!!retValue.skipped) { + scope.skippedElections.push(retValue); + } + return retValue; + } + ); + return category; + }); + return childrenInfo; + } + + function getChildrenElectionsData() { + if (!scope.childrenElectionInfo || isChooserDisabled()) { + return; + } + + _.map( + scope.childrenElectionInfo.presentation.categories, + function (category) { + _.map( + category.events, + function (event) { + if (event.hidden) { + return {}; + } + return scope.simpleGetElection(event.event_id).then( + function (electionData) { + event.electionData = electionData; + $timeout(function () { + scope.$apply(); + }); + } + ); + } + ); + } + ); + } + + function chooseElection(electionId) { + scope.setState(scope.stateEnum.receivingElection, {}); + scope.retrieveElectionConfig(electionId + ""); + } + + scope.childrenElectionInfo = generateChildrenInfo(); + getChildrenElectionsData(); + + function checkDisabled() { + // if election chooser is disabled and can vote, then go to the first + // election in which it can vote + if (isChooserDisabled()) { + var orderedElectionIds = scope + .childrenElectionInfo + .natural_order; + // If it's a demo booth, do not rely on election credentials + if (scope.isDemo || scope.isPreview) { + scope.increaseDemoElectionIndex(); + if (scope.demoElectionIndex < orderedElectionIds.length) { + chooseElection( + orderedElectionIds[scope.demoElectionIndex] + ); + } else { + scope.hasVoted = true; + scope.canVote = false; + } + return; + } + + var credentials = getElectionCredentials(); + for (var i = 0; i < orderedElectionIds.length; i++) { + var electionId = orderedElectionIds[i]; + var elCredentials = findElectionCredentials( + electionId, + credentials + ); + if ( + !elCredentials.skipped && + !elCredentials.voted && + calculateCanVote(elCredentials) + ) { + chooseElection(electionId); + return; + } + } + // If redirected to no election but there are skipped elections, it + // means that the voter can re-login to vote again so we set the + // showSkippedElections flag + if (scope.skippedElections.length > 0) { + scope.showSkippedElections = true; + } + } + } + + checkDisabled(); + scope.chooseElection = chooseElection; + scope.goToVoterEligibility = function () { + scope.setState(scope.stateEnum.voterEligibilityScreen, {}); + }; + } + return { + restrict: 'AE', + scope: true, + link: link, + templateUrl: 'avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.html' + }; + }); diff --git a/avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.less b/avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.less new file mode 100644 index 00000000..0fdefa06 --- /dev/null +++ b/avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.less @@ -0,0 +1,2 @@ +[avb-voter-eligibility-screen] { +} \ No newline at end of file diff --git a/index.html b/index.html index 0bcea4eb..f5755357 100755 --- a/index.html +++ b/index.html @@ -66,6 +66,7 @@ +