Get started on voting booth
This commit is contained in:
parent
700a791e72
commit
ca8c0ce614
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,6 +6,7 @@ __javascript__
|
||||
__pycache__
|
||||
refs
|
||||
bower_components
|
||||
eosjs.js
|
||||
|
||||
\#*
|
||||
.#*
|
||||
|
@ -39,3 +39,6 @@ for f in eos.js_tests; do
|
||||
# Transcrypt bug
|
||||
perl -0777 -pi -e 's/property.call \((.*?), \g1.\g1.__impl__(.*?)\)/property.call ($1, $1.__impl__$2)/g' eos/__javascript__/$f.js
|
||||
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
|
||||
|
@ -49,6 +49,8 @@ class Result(EmbeddedObject):
|
||||
|
||||
class ApprovalQuestion(Question):
|
||||
choices = ListField(StringField())
|
||||
min_choices = IntField()
|
||||
max_choices = IntField()
|
||||
|
||||
class ApprovalAnswer(Answer):
|
||||
choices = ListField(IntField())
|
||||
|
@ -16,6 +16,7 @@
|
||||
"tests"
|
||||
],
|
||||
"dependencies": {
|
||||
"semantic": "semantic-ui#^2.2.13"
|
||||
"semantic": "semantic-ui#^2.2.13",
|
||||
"nunjucks": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -60,10 +60,10 @@ def setup_test_election():
|
||||
election.sk = EGPrivateKey.generate()
|
||||
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)
|
||||
|
||||
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.save()
|
||||
@ -91,6 +91,11 @@ def using_election(func):
|
||||
return func(election)
|
||||
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')
|
||||
@using_election
|
||||
def election_view(election):
|
||||
@ -99,7 +104,9 @@ def election_view(election):
|
||||
@app.route('/election/<election_id>/booth')
|
||||
@using_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')
|
||||
@using_election
|
||||
@ -116,6 +123,8 @@ def election_view_ballots(election):
|
||||
def election_view_trustees(election):
|
||||
return flask.render_template('election/trustees.html', election=election)
|
||||
|
||||
|
||||
|
||||
# === Model-Views ===
|
||||
|
||||
model_view_map = {}
|
||||
|
@ -26,5 +26,8 @@ model_view_map = {
|
||||
},
|
||||
PSRElection: {
|
||||
'tabs': 'election/psr/tabs.html'
|
||||
},
|
||||
ApprovalQuestion: {
|
||||
'selections_make': 'question/approval/selections_make.html'
|
||||
}
|
||||
}
|
||||
|
43
eosweb/core/static/js/booth_worker.js
Normal file
43
eosweb/core/static/js/booth_worker.js
Normal 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;
|
||||
}
|
||||
}
|
53
eosweb/core/static/nunjucks/booth/base.html
Normal file
53
eosweb/core/static/nunjucks/booth/base.html
Normal 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 %}
|
58
eosweb/core/static/nunjucks/booth/selections.html
Normal file
58
eosweb/core/static/nunjucks/booth/selections.html
Normal 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 %}
|
32
eosweb/core/static/nunjucks/booth/welcome.html
Normal file
32
eosweb/core/static/nunjucks/booth/welcome.html
Normal 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 %}
|
@ -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>
|
@ -21,7 +21,163 @@
|
||||
{% block title %}{{ election.name }} – Voting booth{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ election.name }}</h1>
|
||||
|
||||
<p><small><b>Election fingerprint:</b> <span class="hash">{{ SHA256().update_obj(election).hash_as_b64() }}</span></small></p>
|
||||
<div class="ui container" id="booth-content">
|
||||
<div class="ui active text loader">Loading voting booth. Please wait.</div>
|
||||
</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 %}
|
||||
|
Loading…
Reference in New Issue
Block a user