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):
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

View File

@ -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())
'''

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 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,

View File

@ -34,8 +34,6 @@
{% endif %}
</span>
<br/>
<span><strong>Number of polls for this event:</strong> {{ poll_count }}</span>
<br/>
<br/>
<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
@ -61,26 +59,21 @@
<div class="panel panel-default">
<div class="panel-heading"><strong>Options</strong></div>
<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 %}
<div class="checkbox">
{% 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>
{% endfor %}
<hr/>
<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 -->
<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" 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>
</div>
</div>
@ -124,8 +117,11 @@
</div>
</div>
<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="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>

View File

@ -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;
}

View File

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