From a168ba9e28640dabe300b72a913675605a4b6150 Mon Sep 17 00:00:00 2001 From: vince0656 Date: Fri, 31 Aug 2018 19:39:12 +0100 Subject: [PATCH] Fixed a number of bugs that arose from merging the code from Bens fork. Added an extra step when voting which asks the user to confirm their voting choices. The voter now gets an email when they've successfully submitted thier vote. --- allauthdemo/polls/models.py | 2 +- allauthdemo/polls/tasks.py | 16 ++ allauthdemo/polls/views.py | 5 +- allauthdemo/templates/polls/event_vote.html | 24 +- static/css/main.css | 26 ++ static/js/event_vote.js | 283 ++++++++++++++++---- 6 files changed, 281 insertions(+), 75 deletions(-) diff --git a/allauthdemo/polls/models.py b/allauthdemo/polls/models.py index 13b872e..3766f4b 100755 --- a/allauthdemo/polls/models.py +++ b/allauthdemo/polls/models.py @@ -211,7 +211,7 @@ class Ballot(models.Model): class EncBallot(models.Model): handle = models.CharField(primary_key=True, default=uuid.uuid4, editable=False, max_length=255) - ballot = models.CharField(max_length=4096) + ballot = models.CharField(max_length=10240) # Implements the new binary encoding scheme diff --git a/allauthdemo/polls/tasks.py b/allauthdemo/polls/tasks.py index 277d0ad..35d9766 100755 --- a/allauthdemo/polls/tasks.py +++ b/allauthdemo/polls/tasks.py @@ -212,6 +212,22 @@ def email_voters_vote_url(voters, event): voter.send_email(email_subject, email_body) +@task() +def email_voting_success(voter, ballotHandle, eventTitle): + email_subject = "Vote(s) received for Event '" + eventTitle + "'" + + # Plain text email - this could be replaced for a HTML-based email in the future + email_body_base = str("") + email_body_base += "Dear Voter,\n\n" + email_body_base += "Thank you for your vote(s) for the event: " + eventTitle + ". This has been securely encrypted " + email_body_base += "and anonymously stored in our system.\n\n" + email_body_base += "For your reference, the identifier for your selected ballot is:\n" + email_body_base += ballotHandle + email_body_base += get_email_sign_off() + + voter.send_email(email_subject, email_body_base) + + ''' Updates the EID of an event to contain 2 event IDs: a human readable one (hr) and a crypto one (GP from param()) ''' diff --git a/allauthdemo/polls/views.py b/allauthdemo/polls/views.py index 0799c14..8ae6a4a 100755 --- a/allauthdemo/polls/views.py +++ b/allauthdemo/polls/views.py @@ -18,6 +18,7 @@ from allauthdemo.auth.models import DemoUser from .tasks import email_trustees_prep, update_EID, generate_combpk, event_ended, create_ballots from .tasks import create_ballots_for_poll, email_voters_vote_url, combine_decryptions_and_tally, combine_encrypted_votes +from .tasks import email_voting_success from .utils.EventModelAdaptor import EventModelAdaptor @@ -220,7 +221,9 @@ def event_vote(request, event_id, poll_id): ballot.selection = selection ballot.save() - combine_encrypted_votes.delay(email_key[0].user, poll) + voter = email_key[0].user + combine_encrypted_votes.delay(voter, poll) + email_voting_success.delay(voter, handle_json, event.title) if next_poll_uuid: return HttpResponseRedirect(reverse('polls:event-vote', kwargs={'event_id': event.uuid, diff --git a/allauthdemo/templates/polls/event_vote.html b/allauthdemo/templates/polls/event_vote.html index 563fa43..bdd92b4 100755 --- a/allauthdemo/templates/polls/event_vote.html +++ b/allauthdemo/templates/polls/event_vote.html @@ -34,8 +34,6 @@ {% endif %}
- Number of polls for this event: {{ poll_count }} -

Instructions: You will be shown each poll for this event one by one where you will need to make a selection for the current @@ -61,26 +59,21 @@
Options
- {% comment %}{% endcomment %} {% for option in object.options.all %}
{% load custom_filters_tags %} - +
{% endfor %}
- + - +
diff --git a/static/css/main.css b/static/css/main.css index 7bf1fe1..b5af29c 100755 --- a/static/css/main.css +++ b/static/css/main.css @@ -208,4 +208,30 @@ input[type="file"] { .choice-group { width: 54%; margin: 0 auto; +} + +img[src^="data"] { + width: 45%; + margin-left: auto; + margin-right: auto; +} + +.containerMarginTop { + margin-top: 1em; +} + +.skDIV { + width: 75%; + margin: 0 auto; + height: 3em; + background-color: darkslategrey; + color: white; + font-weight: bold; +} + +.skDIV > p { + text-align: center; + height: 3em; + vertical-align: middle; + line-height: 2.9em; } \ No newline at end of file diff --git a/static/js/event_vote.js b/static/js/event_vote.js index 8167a0d..6807fba 100644 --- a/static/js/event_vote.js +++ b/static/js/event_vote.js @@ -1,4 +1,11 @@ var dialogOpen = false; +var DIALOG_BTN_STATES = { + STEP_1: 1, + STEP_2: 2, + STEP_3: 3, + VOTE_SUCCESS: 4, + VOTE_ERROR: 5 +}; function showDialogWithText(titleTxt, bodyTxt) { var modalDialog = $('#modalDialog'); @@ -10,14 +17,55 @@ function showDialogWithText(titleTxt, bodyTxt) { var p = document.createElement("p"); p.innerHTML = bodyTxt; body.empty(); - body.append( p ); + body.append(p); - if(!dialogOpen) { + if (!dialogOpen) { modalDialog.modal('toggle'); dialogOpen = true; } } +function updateDialogButtons(state) { + // Trigger the btn selectors once here + let nextDialogBtn = $('#nextDialogBtn'); + let cancelDialogBtn = $('#cancelDialogBtn'); + let closeDialogBtn = $('#closeDialogBtn'); + let startOverDialogBtn = $('#startOverDialogBtn'); + let submitDialogBtn = $('#submitDialogBtn'); + + switch(state) { + case DIALOG_BTN_STATES.STEP_1: + nextDialogBtn.removeClass("hidden"); + cancelDialogBtn.removeClass("hidden"); + closeDialogBtn.addClass("hidden"); + startOverDialogBtn.addClass("hidden"); + submitDialogBtn.addClass("hidden"); + break; + case DIALOG_BTN_STATES.STEP_2: + nextDialogBtn.addClass("hidden"); + cancelDialogBtn.removeClass("hidden"); + closeDialogBtn.addClass("hidden"); + startOverDialogBtn.addClass("hidden"); + submitDialogBtn.addClass("hidden"); + break; + case DIALOG_BTN_STATES.STEP_3: + nextDialogBtn.addClass("hidden"); + cancelDialogBtn.addClass("hidden"); + closeDialogBtn.addClass("hidden"); + startOverDialogBtn.removeClass("hidden"); + submitDialogBtn.removeClass("hidden"); + break; + case DIALOG_BTN_STATES.VOTE_SUCCESS: + case DIALOG_BTN_STATES.VOTE_ERROR: + nextDialogBtn.addClass("hidden"); + cancelDialogBtn.addClass("hidden"); + closeDialogBtn.removeClass("hidden"); + startOverDialogBtn.addClass("hidden"); + submitDialogBtn.addClass("hidden"); + break; + } +} + // This should stop people ticking more than the maximum permitted function updateCheckboxInteractivity() { var inputs = $("label input[type=checkbox]"); @@ -65,10 +113,6 @@ function isVotingInputValid() { valid = false; } - if(selectedCount < MAX_SELECTIONS) { - valid = false; - } - // This will highlight when people haven't selected enough options if(!valid) { @@ -86,6 +130,7 @@ function isVotingInputValid() { let titleTxt = 'Voting Error'; showDialogWithText(titleTxt, errText); + updateDialogButtons(DIALOG_BTN_STATES.VOTE_ERROR); return; } @@ -259,6 +304,18 @@ function SHA256Hash(bytes, toStr) { } function generateBallots() { + // Get the user's selected option + let inputs = $("label input[type=checkbox]"); + let selectedOption = ""; + inputs.each(function() { + let input = $(this); + + if(input.prop('checked')) { + selectedOption = input.val(); + selectedOption = document.getElementById(selectedOption).innerText; + } + }); + // Generate Ballot A and Ballot B to be displayed to the user // This fn starts the process var ballotA = generateBallot(); @@ -271,11 +328,12 @@ function generateBallots() { var ballotB = generateBallot(); progressBar.setAttribute("style", "width: 100%;"); - showFirstQRCode(ballotA, ballotB); + showFirstQRCode(ballotA, ballotB, selectedOption); }, 150); } -function showFirstQRCode(ballotA, ballotB) { +// Called in stage 1 of 3 in the voting process +function showFirstQRCode(ballotA, ballotB, selectedOption) { var ballots = new Array(ballotA, ballotB); var ballotHashes = new Array(2); @@ -287,14 +345,19 @@ function showFirstQRCode(ballotA, ballotB) { var modalDialog = $('#modalDialog'); var title = modalDialog.find('.modal-title'); var body = modalDialog.find('.modal-body'); - var footer = modalDialog.find('.modal-footer'); body.empty(); - title.text('Please Scan this QR Code'); + title.text('Step 1 of 3: Link Your Vote'); + let pleaseScanP = document.createElement('p'); + pleaseScanP.innerHTML = "Please scan the following QR code from your DEMOS 2 mobile application:"; + + let QRDiv = document.createElement('div'); var QRCodeImg = document.createElement('img'); QRCodeImg.setAttribute('class', 'QR-code'); + QRCodeImg.setAttribute('id', "qr-img"); new QRCode(QRCodeImg, ballotHashes[0] + ';' + ballotHashes[1]); + QRDiv.append(QRCodeImg); // ---------------------------------------------- @@ -315,37 +378,31 @@ function showFirstQRCode(ballotA, ballotB) { // ----------------------------------------------- - body.append(QRCodeImg); + body.append(pleaseScanP); + body.append(QRDiv); body.append(hashGroupDiv); - var closeButton = $('close-button'); - closeButton.removeClass('btn-success'); - closeButton.addClass('btn-danger'); - closeButton.text("Close without submitting vote"); + // Prepare the appropriate dialog buttons + updateDialogButtons(DIALOG_BTN_STATES.STEP_1); - var nextButton = document.createElement('button'); - nextButton.setAttribute('type', 'button'); - nextButton.setAttribute('id', 'next-button'); - nextButton.setAttribute('class', 'btn btn-default'); - nextButton.innerHTML = "Next"; + if(!dialogOpen) { + modalDialog.modal('toggle'); + dialogOpen = true; + } - footer.prepend(nextButton); - - modalDialog.modal('show'); - - $('#next-button').click(function(e) { - showBallotChoiceDialog(ballots); + $('#nextDialogBtn').click(function(e) { + showBallotChoiceDialog(ballots, ballotHashes, selectedOption, modalDialog); }); } -function showBallotChoiceDialog(ballots) { +// Called in stage 2 of 3 in the voting process +function showBallotChoiceDialog(ballots, ballotHashes, selectedOption, dialog) { // Display the ballot choice dialog - var modalDialog = $('#modalDialog'); - var title = modalDialog.find('.modal-title'); - var body = modalDialog.find('.modal-body'); + var title = dialog.find('.modal-title'); + var body = dialog.find('.modal-body'); body.empty(); - title.text('Please Select a Ballot'); + title.text('Step 2 of 3: Select a Ballot'); // Generate the body of the dialog which consists of a button for A and for B var choiceGroupDiv = document.createElement('div'); @@ -363,21 +420,104 @@ function showBallotChoiceDialog(ballots) { btnChoiceB.innerHTML = 'B'; choiceGroupDiv.append(btnChoiceB); - body.append(choiceGroupDiv); + // ---------------------------------------------- - modalDialog.modal('show'); + var hashGroupDiv = document.createElement('div'); + var br = document.createElement('br'); + hashGroupDiv.append( br ); + + var hashA = document.createElement("span"); + hashA.innerHTML = "Hash A: " + ballotHashes[0]; + hashGroupDiv.append( hashA ); + + var br2 = document.createElement('br'); + hashGroupDiv.append( br2 ); + + var hashB = document.createElement("span"); + hashB.innerHTML = "Hash B: " + ballotHashes[1]; + hashGroupDiv.append( hashB ); + + // ----------------------------------------------- + + body.append(choiceGroupDiv); + body.append(hashGroupDiv); // Register callback functions for the selection of either A or B $('#choice-A').click(function(e) { - sendBallotsToServer(ballots[0], ballots[1]); + showSelectionConfirmationDialog("A", ballots[0], ballotHashes[0], ballots[1], selectedOption, dialog); }); $('#choice-B').click(function(e) { - sendBallotsToServer(ballots[1], ballots[0]); + showSelectionConfirmationDialog("B", ballots[1], ballotHashes[1], ballots[0], selectedOption, dialog); }); + + updateDialogButtons(DIALOG_BTN_STATES.STEP_2); + + if(!dialogOpen) { + modalDialog.modal('toggle'); + dialogOpen = true; + } } -function sendBallotsToServer(selection, alt) { +// Called in stage 3 of 3 in the voting process +function showSelectionConfirmationDialog(selection, selectedBallot, selectedBallotHash, + otherBallot, selectedOption, dialog) { + let title = dialog.find('.modal-title'); + let body = dialog.find('.modal-body'); + body.empty(); + + title.text("Step 3 of 3: Confirm Ballot Selection"); + + // Ballot detail section + let selectedInfoSecDiv = document.createElement('div'); + + let detailsP = document.createElement('p'); + detailsP.innerHTML = "Please check the following details are correct: "; + selectedInfoSecDiv.append(detailsP); + + let ul = document.createElement('ul'); + + let selectedOptionLi = document.createElement('li'); + selectedOptionLi.innerHTML = "Selected Option: " + selectedOption; + + let ballotSelectionLi = document.createElement('li'); + ballotSelectionLi.innerHTML = "Selected Ballot: " + selection; + + let ballotHashLi = document.createElement('li'); + ballotHashLi.innerHTML = "SHA256 Ballot Fingerprint: " + selectedBallotHash; + + ul.append(selectedOptionLi); + ul.append(ballotSelectionLi); + ul.append(ballotHashLi); + selectedInfoSecDiv.append(ul); + + // Instruction section + let instructionsP = document.createElement('p'); + instructionsP.innerHTML = "If you are happy with your selection you can click on the 'Submit' button below to store" + + " your vote. Otherwise you can select 'Start Over' to go through the voting process again."; + selectedInfoSecDiv.append(instructionsP); + + let additionalInstructionsP = document.createElement('p'); + additionalInstructionsP.innerHTML = "You can overwrite your vote later by re-visiting this page and voting again."; + selectedInfoSecDiv.append(additionalInstructionsP); + + body.append(selectedInfoSecDiv); + + // Update the dialog buttons accordingly + updateDialogButtons(DIALOG_BTN_STATES.STEP_3); + + $('#submitDialogBtn').click(function() { + // Dispatch the ballot to the server + sendBallotsToServer(selection, selectedBallot, otherBallot); + }); + + if(!dialogOpen) { + modalDialog.modal('toggle'); + dialogOpen = true; + } +} + +function sendBallotsToServer(selection, selectedBallot, otherBallot) { $.ajaxSetup({ beforeSend: function(xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { @@ -416,14 +556,15 @@ function sendBallotsToServer(selection, alt) { var pollNum = $('#poll-num').text(); var ballotID = encodeURIComponent(btoa(JSON.stringify({voterID: voterID, eventID: eventID, pollNum: pollNum}))); + // TODO: Generate a SK rather than using a static one. UUID generated server side and then injected JS side? var SK = "temporary"; - var encAlt = sjcl.encrypt(SK, JSON.stringify(alt)); - selection = JSON.stringify(selection); + var encAlt = sjcl.encrypt(SK, JSON.stringify(otherBallot)); + let selectedBallotAsStr = JSON.stringify(selectedBallot); $.ajax({ type : "POST", url : window.location, - data : { handle: ballotID, encBallot: encAlt, ballot: selection }, + data : { handle: ballotID, encBallot: encAlt, ballot: selectedBallotAsStr, selection: selection }, success : function(){ onAfterBallotSend(ballotID, SK); } @@ -432,40 +573,64 @@ function sendBallotsToServer(selection, alt) { // Called once the ballot has been sent to the back-end and dialog has closed function onAfterBallotSend(ballotID, SK) { - let titleText = 'Vote Successfully Received'; - let bodyText = "Thank you for voting! Your secret key is '"+SK+"'. Make sure to scan this QR code with your phone before closing this window."; - - if(POLL_NUM !== POLL_COUNT) { - bodyText += " You can vote on the next poll by closing down this dialog and clicking 'Next Poll'."; - } - // With one ballot selected, we can display a QR code of the ballot ID var modalDialog = $('#modalDialog'); var title = modalDialog.find('.modal-title'); var body = modalDialog.find('.modal-body'); - title.text(titleText); body.empty(); - var p = document.createElement("p"); - p.innerHTML = bodyText; - body.append(p); + let titleText = 'Vote Successfully Received'; + title.text(titleText); - // Generate the body of the dialog which displays the unselected ballot QR code + // Add the first section: Instructions on next steps + let instructions1Txt = "Thank you for voting! Please note down the ballot identifier by scanning " + + "this QR code using the DEMOS2 mobile application: "; + + var instructions1P = document.createElement("p"); + instructions1P.innerHTML = instructions1Txt; + body.append(instructions1P); + + // Add the second section: QR code that contains the ballot identifier var QRCodeImg = document.createElement('img'); QRCodeImg.setAttribute('class', 'QR-code'); new QRCode(QRCodeImg, ballotID); body.append(QRCodeImg); - var closeButton = $('#close-button'); - closeButton.removeClass('btn-danger'); - closeButton.addClass('btn-success'); - closeButton.text("Close"); - if(POLL_NUM == POLL_COUNT) { - $('#next-button').hide(); + // Add the third section: instructions on Ballot ID and SK + let instructions2Div = document.createElement('div'); + instructions2Div.setAttribute('class', 'containerMarginTop'); + + let instructions2Txt = "You will also be emailed the ballot identifier. However, you will need to note down the following " + + "secret in order to later verify your ballot was recorded as cast: "; + let instructions2P = document.createElement('p'); + instructions2P.innerHTML = instructions2Txt; + instructions2Div.append(instructions2P); + body.append(instructions2Div); + + // Add the fourth section: SK plain text + let SKContainerDiv = document.createElement('div'); + SKContainerDiv.setAttribute("class", "containerMarginTop"); + + let SKDiv = document.createElement('div'); + SKDiv.setAttribute("class", "skDIV"); + + let SKP = document.createElement('p'); + SKP.innerHTML = SK; + SKDiv.append(SKP); + + SKContainerDiv.append(SKDiv); + body.append(SKContainerDiv); + + // Conditional fifth section: Instructions on how to vote on the next poll for the event + if(POLL_NUM !== POLL_COUNT) { + let instructions3Txt = "You can vote on the next poll by closing down this dialog and clicking 'Next Poll'."; + let instructions3P = document.createElement('p'); + instructions3P.innerHTML = instructions3Txt; + body.append(instructions3P); } - modalDialog.modal('show'); + updateDialogButtons(DIALOG_BTN_STATES.VOTE_SUCCESS); } $('#modalDialog').on('hide.bs.modal', function (e) { @@ -474,7 +639,7 @@ $('#modalDialog').on('hide.bs.modal', function (e) { if(titleText.indexOf("Received") > -1) { // Update page to reflect the fact that a vote has taken place location.reload(); - } else { + } else if (titleText.indexOf("Error") === -1) { // Reset poll voting to allow user to vote again progressBar.setAttribute("style", "width: 0%;"); $('#gen-ballots-btn').toggleClass("hidden");