Merge pull request #21 from vincentmdealmeida/Fixes

Fixed a number of bugs that arose from merging the code from Bens for…
This commit is contained in:
Vincent 2018-08-31 19:42:33 +01:00 committed by GitHub
commit 59d3b26e95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 281 additions and 75 deletions

View file

@ -211,7 +211,7 @@ class Ballot(models.Model):
class EncBallot(models.Model): class EncBallot(models.Model):
handle = models.CharField(primary_key=True, default=uuid.uuid4, editable=False, max_length=255) 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 # Implements the new binary encoding scheme

View file

@ -212,6 +212,22 @@ def email_voters_vote_url(voters, event):
voter.send_email(email_subject, email_body) 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()) Updates the EID of an event to contain 2 event IDs: a human readable one (hr) and a crypto one (GP from param())
''' '''

View file

@ -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 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 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 from .utils.EventModelAdaptor import EventModelAdaptor
@ -220,7 +221,9 @@ def event_vote(request, event_id, poll_id):
ballot.selection = selection ballot.selection = selection
ballot.save() 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: if next_poll_uuid:
return HttpResponseRedirect(reverse('polls:event-vote', kwargs={'event_id': event.uuid, return HttpResponseRedirect(reverse('polls:event-vote', kwargs={'event_id': event.uuid,

View file

@ -34,8 +34,6 @@
{% endif %} {% endif %}
</span> </span>
<br/> <br/>
<span><strong>Number of polls for this event:</strong> {{ poll_count }}</span>
<br/>
<br/> <br/>
<span><strong>Instructions:</strong> <span><strong>Instructions:</strong>
You will be shown each poll for this event one by one where you will need to make a selection for the current 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 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Options</strong></div> <div class="panel-heading"><strong>Options</strong></div>
<div class="panel panel-body"> <div class="panel panel-body">
{% comment %}<select class="radio-inline select form-control" id="poll-options" name="options">
{% load custom_filters_tags %}
<option value="{{ -1|get_ballot_value:object.options.all.count }}">Please Select...</option>
{% for option in object.options.all %}
<option value="{{forloop.counter|get_ballot_value:object.options.all.count}}">{{ option.choice_text }}</option>
{% endfor %}
</select>{% endcomment %}
{% for option in object.options.all %} {% for option in object.options.all %}
<div class="checkbox"> <div class="checkbox">
{% load custom_filters_tags %} {% load custom_filters_tags %}
<label><input type="checkbox" value="{{forloop.counter|get_ballot_value:object.options.all.count}}">{{ option.choice_text }}</label> <label id="{{forloop.counter|get_ballot_value:object.options.all.count}}">
<input type="checkbox" value="{{forloop.counter|get_ballot_value:object.options.all.count}}">{{ option.choice_text }}
</label>
</div> </div>
{% endfor %} {% endfor %}
<hr/> <hr/>
<div id="ballot-gen-progress-area"> <div id="ballot-gen-progress-area">
<button id="gen-ballots-btn" class="btn btn-primary">Generate Ballots</button> <button id="gen-ballots-btn" class="btn btn-primary">Begin Voting</button>
<!-- Progress bar which is used during encryption --> <!-- Progress bar which is used during encryption -->
<h4 id="progress-bar-description" class="hidden">Generating Ballots...</h4> <h4 id="progress-bar-description" class="hidden">Generating 2 Digital Ballots. Please wait...</h4>
<div id="progress-bar-container" class="progress hidden"> <div id="progress-bar-container" class="progress hidden">
<div id="progress-bar" class="progress-bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"> <div id="progress-bar" class="progress-bar progress-bar-striped" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
<span class="sr-only">70% Complete</span> <span class="sr-only">70% Complete</span>
</div> </div>
</div> </div>
@ -124,8 +117,11 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button id="cancelVoteBtn" type="button" class="btn btn-danger" data-dismiss="modal">Cancel</button> <button id="nextDialogBtn" type="button" class="btn btn-primary">Next</button>
<button id="cancelDialogBtn" type="button" class="btn btn-danger" data-dismiss="modal">Cancel</button>
<button id="closeDialogBtn" type="button" class="btn btn-primary hidden" data-dismiss="modal">Close</button> <button id="closeDialogBtn" type="button" class="btn btn-primary hidden" data-dismiss="modal">Close</button>
<button id="startOverDialogBtn" type="button" class="btn btn-danger hidden" data-dismiss="modal">Start Over</button>
<button id="submitDialogBtn" type="button" class="btn btn-success hidden">Submit</button>
</div> </div>
</div> </div>

View file

@ -209,3 +209,29 @@ input[type="file"] {
width: 54%; width: 54%;
margin: 0 auto; 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;
}

View file

@ -1,4 +1,11 @@
var dialogOpen = false; 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) { function showDialogWithText(titleTxt, bodyTxt) {
var modalDialog = $('#modalDialog'); var modalDialog = $('#modalDialog');
@ -18,6 +25,47 @@ function showDialogWithText(titleTxt, bodyTxt) {
} }
} }
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 // This should stop people ticking more than the maximum permitted
function updateCheckboxInteractivity() { function updateCheckboxInteractivity() {
var inputs = $("label input[type=checkbox]"); var inputs = $("label input[type=checkbox]");
@ -65,10 +113,6 @@ function isVotingInputValid() {
valid = false; valid = false;
} }
if(selectedCount < MAX_SELECTIONS) {
valid = false;
}
// This will highlight when people haven't selected enough options // This will highlight when people haven't selected enough options
if(!valid) { if(!valid) {
@ -86,6 +130,7 @@ function isVotingInputValid() {
let titleTxt = 'Voting Error'; let titleTxt = 'Voting Error';
showDialogWithText(titleTxt, errText); showDialogWithText(titleTxt, errText);
updateDialogButtons(DIALOG_BTN_STATES.VOTE_ERROR);
return; return;
} }
@ -259,6 +304,18 @@ function SHA256Hash(bytes, toStr) {
} }
function generateBallots() { 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 // Generate Ballot A and Ballot B to be displayed to the user
// This fn starts the process // This fn starts the process
var ballotA = generateBallot(); var ballotA = generateBallot();
@ -271,11 +328,12 @@ function generateBallots() {
var ballotB = generateBallot(); var ballotB = generateBallot();
progressBar.setAttribute("style", "width: 100%;"); progressBar.setAttribute("style", "width: 100%;");
showFirstQRCode(ballotA, ballotB); showFirstQRCode(ballotA, ballotB, selectedOption);
}, 150); }, 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 ballots = new Array(ballotA, ballotB);
var ballotHashes = new Array(2); var ballotHashes = new Array(2);
@ -287,14 +345,19 @@ function showFirstQRCode(ballotA, ballotB) {
var modalDialog = $('#modalDialog'); var modalDialog = $('#modalDialog');
var title = modalDialog.find('.modal-title'); var title = modalDialog.find('.modal-title');
var body = modalDialog.find('.modal-body'); var body = modalDialog.find('.modal-body');
var footer = modalDialog.find('.modal-footer');
body.empty(); 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'); var QRCodeImg = document.createElement('img');
QRCodeImg.setAttribute('class', 'QR-code'); QRCodeImg.setAttribute('class', 'QR-code');
QRCodeImg.setAttribute('id', "qr-img");
new QRCode(QRCodeImg, ballotHashes[0] + ';' + ballotHashes[1]); 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); body.append(hashGroupDiv);
var closeButton = $('close-button'); // Prepare the appropriate dialog buttons
closeButton.removeClass('btn-success'); updateDialogButtons(DIALOG_BTN_STATES.STEP_1);
closeButton.addClass('btn-danger');
closeButton.text("Close without submitting vote");
var nextButton = document.createElement('button'); if(!dialogOpen) {
nextButton.setAttribute('type', 'button'); modalDialog.modal('toggle');
nextButton.setAttribute('id', 'next-button'); dialogOpen = true;
nextButton.setAttribute('class', 'btn btn-default'); }
nextButton.innerHTML = "Next";
footer.prepend(nextButton); $('#nextDialogBtn').click(function(e) {
showBallotChoiceDialog(ballots, ballotHashes, selectedOption, modalDialog);
modalDialog.modal('show');
$('#next-button').click(function(e) {
showBallotChoiceDialog(ballots);
}); });
} }
function showBallotChoiceDialog(ballots) { // Called in stage 2 of 3 in the voting process
function showBallotChoiceDialog(ballots, ballotHashes, selectedOption, dialog) {
// Display the ballot choice dialog // Display the ballot choice dialog
var modalDialog = $('#modalDialog'); var title = dialog.find('.modal-title');
var title = modalDialog.find('.modal-title'); var body = dialog.find('.modal-body');
var body = modalDialog.find('.modal-body');
body.empty(); 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 // Generate the body of the dialog which consists of a button for A and for B
var choiceGroupDiv = document.createElement('div'); var choiceGroupDiv = document.createElement('div');
@ -363,21 +420,104 @@ function showBallotChoiceDialog(ballots) {
btnChoiceB.innerHTML = 'B'; btnChoiceB.innerHTML = 'B';
choiceGroupDiv.append(btnChoiceB); 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 // Register callback functions for the selection of either A or B
$('#choice-A').click(function(e) { $('#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) { $('#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({ $.ajaxSetup({
beforeSend: function(xhr, settings) { beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
@ -416,14 +556,15 @@ function sendBallotsToServer(selection, alt) {
var pollNum = $('#poll-num').text(); var pollNum = $('#poll-num').text();
var ballotID = encodeURIComponent(btoa(JSON.stringify({voterID: voterID, eventID: eventID, pollNum: pollNum}))); 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 SK = "temporary";
var encAlt = sjcl.encrypt(SK, JSON.stringify(alt)); var encAlt = sjcl.encrypt(SK, JSON.stringify(otherBallot));
selection = JSON.stringify(selection); let selectedBallotAsStr = JSON.stringify(selectedBallot);
$.ajax({ $.ajax({
type : "POST", type : "POST",
url : window.location, url : window.location,
data : { handle: ballotID, encBallot: encAlt, ballot: selection }, data : { handle: ballotID, encBallot: encAlt, ballot: selectedBallotAsStr, selection: selection },
success : function(){ success : function(){
onAfterBallotSend(ballotID, SK); 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 // Called once the ballot has been sent to the back-end and dialog has closed
function onAfterBallotSend(ballotID, SK) { 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 // With one ballot selected, we can display a QR code of the ballot ID
var modalDialog = $('#modalDialog'); var modalDialog = $('#modalDialog');
var title = modalDialog.find('.modal-title'); var title = modalDialog.find('.modal-title');
var body = modalDialog.find('.modal-body'); var body = modalDialog.find('.modal-body');
title.text(titleText);
body.empty(); body.empty();
var p = document.createElement("p"); let titleText = 'Vote Successfully Received';
p.innerHTML = bodyText; title.text(titleText);
body.append(p);
// 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'); var QRCodeImg = document.createElement('img');
QRCodeImg.setAttribute('class', 'QR-code'); QRCodeImg.setAttribute('class', 'QR-code');
new QRCode(QRCodeImg, ballotID); new QRCode(QRCodeImg, ballotID);
body.append(QRCodeImg); body.append(QRCodeImg);
var closeButton = $('#close-button'); // Add the third section: instructions on Ballot ID and SK
closeButton.removeClass('btn-danger'); let instructions2Div = document.createElement('div');
closeButton.addClass('btn-success'); instructions2Div.setAttribute('class', 'containerMarginTop');
closeButton.text("Close");
if(POLL_NUM == POLL_COUNT) { let instructions2Txt = "You will also be emailed the ballot identifier. However, you will need to note down the following " +
$('#next-button').hide(); "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) { $('#modalDialog').on('hide.bs.modal', function (e) {
@ -474,7 +639,7 @@ $('#modalDialog').on('hide.bs.modal', function (e) {
if(titleText.indexOf("Received") > -1) { if(titleText.indexOf("Received") > -1) {
// Update page to reflect the fact that a vote has taken place // Update page to reflect the fact that a vote has taken place
location.reload(); location.reload();
} else { } else if (titleText.indexOf("Error") === -1) {
// Reset poll voting to allow user to vote again // Reset poll voting to allow user to vote again
progressBar.setAttribute("style", "width: 0%;"); progressBar.setAttribute("style", "width: 0%;");
$('#gen-ballots-btn').toggleClass("hidden"); $('#gen-ballots-btn').toggleClass("hidden");