Skip to content

Commit

Permalink
Improve vote flow (decidim#7682)
Browse files Browse the repository at this point in the history
  • Loading branch information
leio10 authored Mar 26, 2021
1 parent 13561fe commit 8d3aef3
Show file tree
Hide file tree
Showing 45 changed files with 453 additions and 216 deletions.
4 changes: 2 additions & 2 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"trailingComma": true
}
"trailingComma": "none"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// = require decidim/bulletin_board/decidim-bulletin_board

$(async () => {
const $castingVoteWrapper = $(".casting-vote-wrapper");
const { Client } = decidimBulletinBoard;

const bulletinBoardClient = new Client({
apiEndpointUrl: $castingVoteWrapper.data("apiEndpointUrl")
});
const messageId = $castingVoteWrapper.data("messageId");

await bulletinBoardClient.waitForPendingMessageToBeProcessed(messageId);

$("form.update_vote_status").trigger("submit");
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-disable no-console */
// = require ./vote_questions.component

$(async () => {
const { VoteQuestionsComponent, setupVoteComponent } = window.Decidim;

// UI Elements
const $voteWrapper = $(".vote-wrapper");
const $ballotHash = $voteWrapper.find(".ballot-hash");

// Use the questions component
const questionsComponent = new VoteQuestionsComponent($voteWrapper);
questionsComponent.init();
$(document).on("on.zf.toggler", () => {
// continue and back btn
questionsComponent.init();
});

// Get the vote component and bind it to all UI events
const voteComponent = setupVoteComponent($voteWrapper);
await voteComponent.bindEvents({
onBindEncryptButton(onEventTriggered) {
$(".button.confirm").on("click", onEventTriggered);
},
onStart() {},
onVoteEncryption(validVoteFn) {
const getFormData = (formData) => {
return formData.serializeArray().reduce((acc, { name, value }) => {
if (!acc[name]) {
acc[name] = [];
}
acc[name] = [...acc[name], `${name}_${value}`];
return acc;
}, {});
};
const formData = getFormData($voteWrapper.find(".answer_input"));
validVoteFn(formData);
},
castOrAuditBallot({ encryptedData, encryptedDataHash }) {
$voteWrapper.find("#encrypting").addClass("hide");
$ballotHash.text(`Your ballot identifier is: ${encryptedDataHash}`);
$voteWrapper.find("#ballot_decision").removeClass("hide");

const $form = $("form.new_vote");
$("#vote_encrypted_data", $form).val(encryptedData);
$("#vote_encrypted_data_hash", $form).val(encryptedDataHash);
},
onBindAuditBallotButton(onEventTriggered) {
$(".audit_ballot").on("click", onEventTriggered);
},
onBindCastBallotButton(onEventTriggered) {
$(".cast_ballot").on("click", onEventTriggered);
},
onAuditBallot(auditedData, auditedDataFileName) {
const vote = JSON.stringify(auditedData);
const link = document.createElement("a");
$voteWrapper.find(".button.cast_ballot").addClass("hide");
$voteWrapper.find(".button.back").removeClass("hide");
questionsComponent.voteCasted = true;

link.setAttribute("href", `data:text/plain;charset=utf-8,${vote}`);
link.setAttribute("download", auditedDataFileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
onAuditComplete() {
console.log("Audit completed");
},
onCastBallot() {
questionsComponent.voteCasted = true;
$(".cast_ballot").prop("disabled", true);
},
onCastComplete() {
console.log("Cast completed");
},
onInvalid() {
console.log("Something went wrong.");
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// The wait time used to simulate the encryption of the vote during the preview
const FAKE_ENCRYPTION_TIME = 1000;

((exports) => {
class PreviewVoteComponent {
constructor({ electionUniqueId, voterUniqueId }) {
this.electionUniqueId = electionUniqueId;
this.voterUniqueId = voterUniqueId;
}

async bindEvents({
onBindEncryptButton,
onStart,
onVoteEncryption,
castOrAuditBallot,
onBindAuditBallotButton,
onBindCastBallotButton,
onAuditBallot,
onCastBallot,
onAuditComplete,
onCastComplete,
onInvalid
}) {
onBindEncryptButton(async () => {
onStart();
onVoteEncryption(
(plainVote) => {
this.fakeEncrypt(plainVote).then((ballot) => {
castOrAuditBallot(ballot);
onBindAuditBallotButton(() => {
onAuditBallot(
ballot,
`${this.voterUniqueId}-election-${this.electionUniqueId}.txt`
);
onAuditComplete();
});

onBindCastBallotButton(async () => {
await onCastBallot(ballot);
onCastComplete();
});
});
},
() => {
onInvalid();
}
);
});
}
async fakeEncrypt(plainVote) {
await new Promise((resolve) => setTimeout(resolve, FAKE_ENCRYPTION_TIME));

return {
encryptedData: plainVote,
encryptedDataHash: this.generateHexString(64),
auditableData: plainVote
};
}
generateHexString(length) {
return Array(length).
fill("").
map(() => Math.random().toString(16).charAt(2)).
join("");
}
}

const setupVoteComponent = ($voteWrapper) => {
const voterUniqueId = $voteWrapper.data("voterId");
const electionUniqueId = $voteWrapper.data("electionUniqueId");

return new PreviewVoteComponent({
electionUniqueId,
voterUniqueId
});
};

exports.Decidim = exports.Decidim || {};
exports.Decidim.setupVoteComponent = setupVoteComponent;
})(window);
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// = require decidim/bulletin_board/decidim-bulletin_board
// = require decidim/bulletin_board/dummy-voting-scheme

// = require decidim/bulletin_board/election_guard-voting-scheme
// Note: these gems will be moved to the application in the next release
// = require voting_schemes/dummy/dummy
// = require voting_schemes/electionguard/electionguard

((exports) => {
const setupVoteComponent = ($voteWrapper) => {
const { VoteComponent } = window.decidimBulletinBoard;
const {
VoterWrapperAdapter: DummyVoterWrapperAdapter
} = window.dummyVotingScheme;
const {
VoterWrapperAdapter: ElectionGuardVoterWrapperAdapter
} = window.electionGuardVotingScheme;

// Data
const bulletinBoardClientParams = {
apiEndpointUrl: $voteWrapper.data("apiEndpointUrl")
};
const electionUniqueId = $voteWrapper.data("electionUniqueId");
const authorityPublicKeyJSON = JSON.stringify(
$voteWrapper.data("authorityPublicKey")
);
const voterUniqueId = $voteWrapper.data("voterId");
const schemeName = $voteWrapper.data("schemeName");

// Use the correct voter wrapper adapter
let voterWrapperAdapter = null;

if (schemeName === "dummy") {
voterWrapperAdapter = new DummyVoterWrapperAdapter({
voterId: voterUniqueId
});
} else if (schemeName === "electionguard") {
voterWrapperAdapter = new ElectionGuardVoterWrapperAdapter({
voterId: voterUniqueId,
workerUrl: "/assets/electionguard/webworker.js"
});
} else {
throw new Error(`Voting scheme ${schemeName} not supported.`);
}

// Returns the vote component
return new VoteComponent({
bulletinBoardClientParams,
authorityPublicKeyJSON,
electionUniqueId,
voterUniqueId,
voterWrapperAdapter
});
};

exports.Decidim = exports.Decidim || {};
exports.Decidim.setupVoteComponent = setupVoteComponent;
})(window);
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

$(() => {
const { Client } = decidimBulletinBoard;
const $voteVerifyWrapper = $(".vote-verify-wrapper");
const $voteVerifyWrapper = $(".verify-vote-wrapper");
const $verifySubmitButton = $voteVerifyWrapper.find("a.focus__next.confirm");

let $formData = $voteVerifyWrapper.find(".vote-identifier");
Expand All @@ -14,7 +14,7 @@ $(() => {
onVoteIdentifierChange();
}

initStep()
initStep();

function onVoteIdentifierChange() {
$formData.on("keyup input", (event) => {
Expand All @@ -26,9 +26,9 @@ $(() => {

function toggleVerifyButton() {
if ($formData.val().length > 5) {
$($verifySubmitButton).removeClass("disabled")
$($verifySubmitButton).removeClass("disabled");
} else {
$($verifySubmitButton).addClass("disabled")
$($verifySubmitButton).addClass("disabled");
}
}

Expand All @@ -47,25 +47,26 @@ $(() => {

function verifyVoteIdentifier() {
const bulletinBoardClient = new Client({
apiEndpointUrl: $voteVerifyWrapper.data("apiEndpointUrl"),
wsEndpointUrl: $voteVerifyWrapper.data("websocketUrl")
apiEndpointUrl: $voteVerifyWrapper.data("apiEndpointUrl")
});

bulletinBoardClient.getLogEntry({
electionUniqueId: $voteVerifyWrapper.data("electionUniqueId"),
contentHash: $formData.val()
}).then((result) => {
if (result) {
hideErrorCallout();
$voteVerifyWrapper.find(".verify-vote-success").removeClass("hide");
} else {
hideSuccessCallout();
$voteVerifyWrapper.find(".verify-vote-error").removeClass("hide");
}
})
bulletinBoardClient.
getLogEntry({
electionUniqueId: $voteVerifyWrapper.data("electionUniqueId"),
contentHash: $formData.val()
}).
then((result) => {
if (result) {
hideErrorCallout();
$voteVerifyWrapper.find(".verify-vote-success").removeClass("hide");
} else {
hideSuccessCallout();
$voteVerifyWrapper.find(".verify-vote-error").removeClass("hide");
}
});
}

$(document).on("on.zf.toggler", (event) => {
initStep()
initStep();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ $(() => {
const unit = $("html").css("font-size");

if (typeof count !== "undefined" && count > 0) {
return (parseInt(unit, 0) * count);
return (parseInt(unit, 10) || 0) * count;
}
return parseInt(unit, 0);
}
return parseInt(unit, 10) || 0;
};

const $button = $(".voting-description-cell .content-height-toggler .button");
const $content = $button.closest(".voting-description-cell").find(".content");
Expand All @@ -22,7 +22,7 @@ $(() => {
if (contentHeight < contentMaxHeight) {
$button.hide();
} else {
$content.css("max-height", contentMaxHeight)
$content.css("max-height", contentMaxHeight);
}

$button.on("click", (event) => {
Expand All @@ -38,4 +38,4 @@ $(() => {
$buttonTextLess.toggleClass("hide");
$buttonTextMore.toggleClass("hide");
});
})
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<% if model.started? %>
<%= t("verify.already_voted", scope: "decidim.elections.elections.show") %>

<%= link_to t("verify.verify_here", scope: "decidim.elections.elections.show"), verify_election_vote_path %>
<%= link_to t("verify.verify_here", scope: "decidim.elections.elections.show"), election_vote_verify_path %>
<% else %>
<span class="disabled"><%= t("verify.will_verify", scope: "decidim.elections.elections.show") %></span>
<% end %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,19 @@ def vote_action_button_text
end
end

def verify_election_vote_path
engine_router.verify_election_vote_path(
def election_vote_verify_path
engine_router.election_vote_verify_path(
"#{key_participatory_space_slug}": current_participatory_space.slug,
component_id: current_component.id,
election_id: model.id
election_id: model.id,
vote_id: "_"
)
end

def callout_text
if last_vote_accepted?
if last_vote_pending?
t("callout.pending_vote", scope: "decidim.elections.elections.show")
elsif last_vote_accepted?
t("callout.already_voted", scope: "decidim.elections.elections.show")
else
t("callout.vote_rejected", scope: "decidim.elections.elections.show")
Expand All @@ -54,6 +57,10 @@ def already_voted?
last_vote.present?
end

def last_vote_pending?
!!last_vote&.pending?
end

def last_vote_accepted?
!!last_vote&.accepted?
end
Expand Down
Loading

0 comments on commit 8d3aef3

Please sign in to comment.