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");