Skip to content

Commit

Permalink
✨ Voter eligibility check (#454)
Browse files Browse the repository at this point in the history
Parent issue: sequentech/meta#234
  • Loading branch information
Findeton authored Jul 17, 2024
1 parent e842cb7 commit 8b81d74
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 2 deletions.
8 changes: 8 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions app.less
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ <h3 class="election-title" id="election-title-{{ election.event_id }}">{{electio
<button
class="btn btn-lg btn-success-action btn-plain click-to-vote-btn"
ng-if="checkElectionStarted(election.electionData)"
ng-disabled="!canVoteElection(election)"
ng-disabled="!canVoteElection(election) || !canVote"
ng-click="canVoteElection(election) && click(election)"
ng-i18next="avBooth.electionChooserScreen.clickToVoteBtn">
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
2 changes: 2 additions & 0 deletions avBooth/booth-directive/booth-directive.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
</div>
<div avb-election-chooser-screen ng-if="state == stateEnum.electionChooserScreen">
</div>
<div avb-voter-eligibility-screen ng-if="state == stateEnum.voterEligibilityScreen">
</div>
<div avb-error-screen ng-if="state == stateEnum.errorScreen">
</div>
<div avb-help-screen ng-if="state == stateEnum.helpScreen">
Expand Down
4 changes: 3 additions & 1 deletion avBooth/booth-directive/booth-directive.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions avBooth/booth.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
is-preview="{{isPreview}}"
is-uuid-preview="{{isUuidPreview}}"
preview-election="{{previewElection}}"
is-eligibility="{{isEligibility}}"
>
</div>
1 change: 1 addition & 0 deletions avBooth/booth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!-- top navbar -->
<a href="#main-content" class="skip-link">{{ 'avBooth.skipLinks.skipToMain' | i18next }}</a>
<a href="#footer" class="skip-link">{{ 'avBooth.skipLinks.skipToFooter' | i18next }}</a>
<div avb-common-header></div>
<div avb-watermark></div>


<h1
class="election-header"
ng-if="election.presentation && election.presentation.voter_eligibility_screen"
ng-bind-html="(election.presentation.voter_eligibility_screen | customI18n : 'title')"
>
</h1>

<div
class="eligibility-description container"
ng-if="election.presentation && election.presentation.voter_eligibility_screen"
ng-bind-html="(election.presentation.voter_eligibility_screen | customI18n : 'description')">
</div>

<main class="container" id="main-content">
<div
av-booth-children-elections
mode="toggle-and-callback"
callback="chooseElection(electionId)"
parent-election-id="{{parentAuthEvent.id}}"
hide-parent="true"
can-vote="false"
has-voted="hasVoted"
children-election-info="childrenElectionInfo"
show-skipped-elections="showSkippedElections"
skipped-elections="skippedElections"
parent-election="parentElection"
></div>
</main>


<div
class="eligibility-footer container"
ng-if="election.presentation && election.presentation.voter_eligibility_screen"
ng-bind-html="(election.presentation.voter_eligibility_screen | customI18n : 'footer')">
</div>

<div avb-common-footer id="footer"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/**
* This file is part of voting-booth.
* Copyright (C) 2024 Sequent Tech Inc <[email protected]>
* 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 <http://www.gnu.org/licenses/>.
**/

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'
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[avb-voter-eligibility-screen] {
}
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
<script src="avBooth/voting-step-directive/voting-step-directive.js" class="app"></script>
<script src="avBooth/accordion-option-directive/accordion-option-directive.js" class="app"></script>
<script src="avBooth/election-chooser-screen-directive/election-chooser-screen-directive.js" class="app"></script>
<script src="avBooth/voter-eligibility-screen-directive/voter-eligibility-screen-directive.js" class="app"></script>
<script src="avBooth/accordion-options-directive/accordion-options-directive.js" class="app"></script>
<script src="avBooth/available-options-directive/available-options-directive.js" class="app"></script>
<script src="avBooth/audit-ballot-screen-directive/audit-ballot-screen-directive.js" class="app"></script>
Expand Down

0 comments on commit 8b81d74

Please sign in to comment.