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 526814ab..9c829a67 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 @@
+
+{{ 'avBooth.skipLinks.skipToMain' | i18next }}
+{{ 'avBooth.skipLinks.skipToFooter' | i18next }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+