Get started on voting booth

This commit is contained in:
RunasSudo 2017-11-23 21:07:16 +11:00
parent 700a791e72
commit ca8c0ce614
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
13 changed files with 436 additions and 7 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ __javascript__
__pycache__ __pycache__
refs refs
bower_components bower_components
eosjs.js
\#* \#*
.#* .#*

View File

@ -39,3 +39,6 @@ for f in eos.js_tests; do
# Transcrypt bug # Transcrypt bug
perl -0777 -pi -e 's/property.call \((.*?), \g1.\g1.__impl__(.*?)\)/property.call ($1, $1.__impl__$2)/g' eos/__javascript__/$f.js perl -0777 -pi -e 's/property.call \((.*?), \g1.\g1.__impl__(.*?)\)/property.call ($1, $1.__impl__$2)/g' eos/__javascript__/$f.js
done done
cp eos/__javascript__/eos.js_tests.js eosweb/core/static/js/eosjs.js
perl -0777 -pi -e 's/eosjs_tests/eosjs/g' eosweb/core/static/js/eosjs.js

View File

@ -49,6 +49,8 @@ class Result(EmbeddedObject):
class ApprovalQuestion(Question): class ApprovalQuestion(Question):
choices = ListField(StringField()) choices = ListField(StringField())
min_choices = IntField()
max_choices = IntField()
class ApprovalAnswer(Answer): class ApprovalAnswer(Answer):
choices = ListField(IntField()) choices = ListField(IntField())

View File

@ -16,6 +16,7 @@
"tests" "tests"
], ],
"dependencies": { "dependencies": {
"semantic": "semantic-ui#^2.2.13" "semantic": "semantic-ui#^2.2.13",
"nunjucks": "^3.0.1"
} }
} }

View File

@ -60,10 +60,10 @@ def setup_test_election():
election.sk = EGPrivateKey.generate() election.sk = EGPrivateKey.generate()
election.public_key = election.sk.public_key election.public_key = election.sk.public_key
question = ApprovalQuestion(prompt='President', choices=['John Smith', 'Joe Bloggs', 'John Q. Public']) question = ApprovalQuestion(prompt='President', choices=['John Smith', 'Joe Bloggs', 'John Q. Public'], min_choices=0, max_choices=2)
election.questions.append(question) election.questions.append(question)
question = ApprovalQuestion(prompt='Chairman', choices=['John Doe', 'Andrew Citizen']) question = ApprovalQuestion(prompt='Chairman', choices=['John Doe', 'Andrew Citizen'], min_choices=0, max_choices=1)
election.questions.append(question) election.questions.append(question)
election.save() election.save()
@ -91,6 +91,11 @@ def using_election(func):
return func(election) return func(election)
return wrapped return wrapped
@app.route('/election/<election_id>/')
@using_election
def election_api_json(election):
return flask.Response(EosObject.to_json(EosObject.serialise_and_wrap(election, should_protect=True)), mimetype='application/json')
@app.route('/election/<election_id>/view') @app.route('/election/<election_id>/view')
@using_election @using_election
def election_view(election): def election_view(election):
@ -99,7 +104,9 @@ def election_view(election):
@app.route('/election/<election_id>/booth') @app.route('/election/<election_id>/booth')
@using_election @using_election
def election_booth(election): def election_booth(election):
return flask.render_template('election/booth.html', election=election) selection_model_view_map = EosObject.to_json({key._name: val for key, val in model_view_map.items()}) # ewww
return flask.render_template('election/booth.html', election=election, selection_model_view_map=selection_model_view_map)
@app.route('/election/<election_id>/view/questions') @app.route('/election/<election_id>/view/questions')
@using_election @using_election
@ -116,6 +123,8 @@ def election_view_ballots(election):
def election_view_trustees(election): def election_view_trustees(election):
return flask.render_template('election/trustees.html', election=election) return flask.render_template('election/trustees.html', election=election)
# === Model-Views === # === Model-Views ===
model_view_map = {} model_view_map = {}

View File

@ -26,5 +26,8 @@ model_view_map = {
}, },
PSRElection: { PSRElection: {
'tabs': 'election/psr/tabs.html' 'tabs': 'election/psr/tabs.html'
},
ApprovalQuestion: {
'selections_make': 'question/approval/selections_make.html'
} }
} }

View File

@ -0,0 +1,43 @@
/*
Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
window = self; // Workaround for libraries
isLibrariesLoaded = false;
function generateEncryptedVote(election, selections) {
encryptedVote = eos_js.eos_core.objects.__all__.PlaintextVote({ "choices": selections, "election_uuid": election.id, "election_hash": election.hash() });
postMessage(eos_js.eos_core.libobjects.__all__.EosObject.serialise_and_wrap(encryptedVote, null));
}
onmessage = function(msg) {
if (!isLibrariesLoaded) {
importScripts(
msg.data.static_base_url + "js/eosjs.js"
);
isLibrariesLoaded = true;
}
if (msg.data.action === "generateEncryptedVote") {
msg.data.election = eosjs.eos.core.libobjects.__all__.EosObject.deserialise_and_unwrap(msg.data.election, null);
generateEncryptedVote(msg.data.election, msg.data.selections);
} else {
throw "Unknown action: " + msg.data.action;
}
}

View File

@ -0,0 +1,53 @@
{#
Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
<h1>{{ election.py_name }}</h1>
<p><small><b>Election fingerprint:</b> <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(election).hash_as_b64() }}</span></small></p>
{# Convert the template name to a numerical index for comparison #}
{% if template == 'booth/welcome.html' %}
{% set menuindex = 1 %}
{% elif template == 'booth/selections.html' %}
{% set menuindex = 2 %}
{% elif template == 'booth/review.html' %}
{% set menuindex = 3 %}
{% elif template == 'booth/audit.html' %}
{% set menuindex = 4 %}
{% elif template == 'booth/complete.html' %}
{% set menuindex = 5 %}
{% endif %}
<div class="ui secondary pointing menu" id="election-tab-menu">
{# oh dear god #}
<a href="#" class="ui{% if menuindex >= 1 %} active{% endif %} item"{% if menuindex != 1 %} style="color: #767676;{% if menuindex > 1 %} font-weight: 400;{% endif %}"{% endif %}>1. Welcome</a>
<a href="#" class="ui{% if menuindex >= 2 %} active{% endif %} item"{% if menuindex != 2 %} style="color: #767676;{% if menuindex > 2 %} font-weight: 400;{% endif %}"{% endif %}>2. Make selections</a>
<a href="#" class="ui{% if menuindex >= 3 %} active{% endif %} item"{% if menuindex != 3 %} style="color: #767676;{% if menuindex > 3 %} font-weight: 400;{% endif %}"{% endif %}>3. Review selections</a>
<a href="#" class="ui{% if menuindex >= 4 %} active{% endif %} item"{% if menuindex != 4 %} style="color: #767676;{% if menuindex > 4 %} font-weight: 400;{% endif %}"{% endif %}>4. Audit ballot</a>
<a href="#" class="ui{% if menuindex >= 5 %} active{% endif %} item"{% if menuindex != 5 %} style="color: #767676;{% if menuindex > 5 %} font-weight: 400;{% endif %}"{% endif %}>5. Cast ballot</a>
</div>
<div class="ui container">
{% block content %}{% endblock %}
</div>
<div class="ui container" style="margin-top: 1em;">
{% block buttons %}{% endblock %}
</div>
{% block after %}{% endblock %}

View File

@ -0,0 +1,58 @@
{% extends templates['booth/base.html'] %}
{#
Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% block content %}
<div id="question-box" class="ui container"></div>
{% endblock %}
{% block buttons %}
<button class="ui left floated button" onclick="previousQuestion();">Back</a>
<button class="ui right floated primary button" onclick="nextQuestion();">Continue</button>
{% endblock %}
{% block after %}
<script>
function saveSelections() {
boothError("Question template unable to save selections");
}
showTemplate(selection_model_view_map[election.questions[booth.questionNum]._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
function previousQuestion() {
saveSelections();
if (booth.questionNum == 0) {
prevTemplate();
} else {
booth.questionNum--;
showTemplate(selection_model_view_map[election.questions[booth.questionNum]._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
}
}
function nextQuestion() {
saveSelections();
if (booth.questionNum == election.questions.length - 1) {
nextTemplate();
} else {
booth.questionNum++;
showTemplate(selection_model_view_map[election.questions[booth.questionNum]._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
}
}
</script>
{% endblock %}

View File

@ -0,0 +1,32 @@
{% extends templates['booth/base.html'] %}
{#
Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% block content %}
<p>Welcome to the {{ election.py_name }} voting booth.</p>
<p>Follow the on-screen directions to prepare and cast your ballot in this election. The bar above will show your progress. Please note that your ballot will <b>not be cast until</b> you complete the final ‘Cast ballot’ stage and receive a <b>‘smart ballot tracker’</b>.</p>
<p>If at any point you wish to return to a previous screen, click the ‘Back’ button below.</p>
<p>If you wish, you may disconnect your internet connection now while preparing your ballot, however you must re-connect your internet connection before logging in to cast your ballot.</p>
<p>Please click the blue ‘Continue’ button below to continue.</p>
{% endblock %}
{% block buttons %}
<a href="{{ election_base_url }}/view" class="ui left floated button">Back</a>
<button class="ui right floated primary button" onclick="nextTemplate();">Continue</button>
{% endblock %}

View File

@ -0,0 +1,68 @@
{#
Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
<h2>{{ questionNum + 1 }}. {{ election.questions[questionNum].prompt }}</h2>
<p><small>Vote for between {{ election.questions[questionNum].min_choices }} and {{ election.questions[questionNum].max_choices }} candidates. Click the check-boxes to the left of the candidates' names to make your selection, then click the ‘Continue’ button. If you make a mistake, click the check-boxes again to clear your selection.</small></p>
<div id="question-choices" class="ui form" style="margin-bottom: 1em;">
<div class="grouped fields">
{% for choice in election.questions[questionNum].choices %}
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="question-choice-{{ loop.index0 }}" onchange="choicesChanged();">
<label for="question-choice-{{ loop.index0 }}">{{ choice }}</label>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="ui hidden message" id="message-max-choices">
<p>You have now selected the maximum allowed number of candidates. If you wish to change your selections, you must first use the check-boxes to deselect a candidate.</p>
</div>
<script>
function choicesChanged() {
var numChoices = $("#question-choices input:checked").length;
if (numChoices >= election.questions[booth.questionNum].max_choices) {
// Prevent making any more selections
$("#question-choices input:not(:checked)").prop("disabled", true);
$("#message-max-choices").removeClass("hidden");
} else {
$("#question-choices input").prop("disabled", false);
$("#message-max-choices").addClass("hidden");
}
}
// Fill in ballot with previous selections
if (booth.selections[booth.questionNum]) {
for (var selection of booth.selections[booth.questionNum]) {
$("#question-choice-" + selection).prop("checked", true);
}
choicesChanged();
}
function saveSelections() {
selections = [];
$("#question-choices input:checked").each(function(i, el) {
selections.push(parseInt(el.id.substring("question-choice-".length)))
});
booth.selections[booth.questionNum] = selections;
}
</script>

View File

@ -21,7 +21,163 @@
{% block title %}{{ election.name }} – Voting booth{% endblock %} {% block title %}{{ election.name }} – Voting booth{% endblock %}
{% block content %} {% block content %}
<h1>{{ election.name }}</h1> <div class="ui container" id="booth-content">
<div class="ui active text loader">Loading voting booth. Please wait.</div>
<p><small><b>Election fingerprint:</b> <span class="hash">{{ SHA256().update_obj(election).hash_as_b64() }}</span></small></p> </div>
{% endblock %}
{% block basecontent %}
{{ super() }}
<script src="{{ url_for('static', filename='bower_components/nunjucks/browser/nunjucks.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/eosjs.js') }}"></script>
<script>
var templates = {};
var election = null;
var booth = null;
var boothWorker = null;
var boothTasks = [];
var currentBoothTask = 0;
var selection_model_view_map = {{ selection_model_view_map|safe }}; {# :rooWut: #}
function resetBooth() {
booth = {
"questionNum": 0,
"selections": [],
};
}
resetBooth();
function loadElection() {
$.ajax({ url: "{{ url_for('election_api_json', election_id=election._id) }}", dataType: "text" })
.done(function(data) {
try {
election = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.__all__.EosObject.from_json(data), null);
boothWorker = new Worker("{{ url_for('static', filename='js/booth_worker.js') }}");
electionLoaded();
} catch (err) {
loadError(err);
throw err;
}
})
.fail(function(xhr, status, err) {
loadError(err);
throw err;
});
}
function electionLoaded() {
// Load templates for the question types
for (var question of election.questions) {
templates[selection_model_view_map[question._name]['selections_make']] = null;
}
loadTemplates();
}
function loadTemplates() {
// Load all the templates
var templateUrls = Object.keys(templates);
var numTemplatesLoaded = 0;
for (var templateUrl of templateUrls) {
(function(templateUrl) {
$.ajax({
url: "{{ url_for('static', filename='nunjucks') }}/" + templateUrl,
dataType: "text",
cache: {% if eosweb.app.debug %}false{% else %}true{% endif %}
})
.done(function(data) {
try {
templates[templateUrl] = nunjucks.compile(data);
numTemplatesLoaded += 1;
if (numTemplatesLoaded == templateUrls.length) {
// All templates loaded. Show voting booth
boothTasks[0].activate(true);
}
} catch (err) {
loadError(err);
throw err;
}
})
.fail(function(xhr, status, err) {
loadError(err);
throw err;
});
})(templateUrl);
}
}
function loadError(err) {
var techDetails = '';
if (err) {
techDetails = '<p>Technical details: ' + err + '</p>';
}
$("#booth-content").html('<div class="ui error message"><p>We were unable to load the voting booth for this election. Please try again. If this problem persists, contact the election administrator.</p>' + techDetails + '</div>');
}
function boothError(err) {
resetBooth();
var techDetails = '';
if (err) {
techDetails = '<p>Technical details: ' + err + '</p>';
}
$("#booth-content").html('<div class="ui error message"><p>We were unable to display the next page of the voting booth. For your security, your ballot selections have been cleared. Please try again. If this problem persists, contact the election administrator.</p>' + techDetails + '</div>');
}
function showTemplate(template, opts, destination) {
try {
if (!destination) {
destination = "#booth-content";
}
if (!opts) {
opts = {};
}
opts = $.extend({
"templates": templates,
"template": template,
"election_base_url": "{{ url_for('election_api_json', election_id=election._id) }}",
"static_base_url": "{{ url_for('static', filename='nunjucks') }}",
"election": election,
"booth": booth,
"eosjs": eosjs,
}, opts);
$(destination).html(templates[template].render(opts));
} catch (err) {
boothError(err);
throw err;
}
}
function nextTemplate() {
currentBoothTask++;
boothTasks[currentBoothTask].activate(true);
}
function prevTemplate() {
currentBoothTask--;
boothTasks[currentBoothTask].activate(false);
}
// === BOOTH TASKS ===
// TODO: Make modular
boothTasks.append({
activate: function(shouldInit) {
showTemplate('booth/welcome.html');
}
});
templates['booth/base.html'] = null;
templates['booth/welcome.html'] = null;
boothTasks.append({
activate: function(shouldInit) {
showTemplate('booth/selections.html');
}
});
templates['booth/selections.html'] = null;
// === END BOOTH TASKS ===
loadElection();
</script>
{% endblock %} {% endblock %}