From 453443d22ac90b322369a22bda3c95bc5dec4eae Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Thu, 23 Nov 2017 23:10:57 +1100 Subject: [PATCH] :tada: We did it, Reddit! Voting booth now functional at a basic level --- eos/base/election.py | 8 +- eos/base/tests.py | 3 +- eos/base/workflow.py | 4 +- eos/core/objects/__init__.py | 3 + eos/psr/election.py | 4 +- eos/psr/tests.py | 7 +- eosweb/core/main.py | 30 ++++- eosweb/core/modelview.py | 3 +- eosweb/core/static/js/booth_worker.js | 15 ++- eosweb/core/static/nunjucks/booth/audit.html | 28 +++++ eosweb/core/static/nunjucks/booth/base.html | 14 ++- eosweb/core/static/nunjucks/booth/cast.html | 103 ++++++++++++++++++ .../core/static/nunjucks/booth/complete.html | 37 +++++++ .../core/static/nunjucks/booth/encrypt.html | 52 +++++++++ eosweb/core/static/nunjucks/booth/review.html | 41 +++++++ .../question/approval/selections_make.html | 7 +- .../question/approval/selections_review.html | 37 +++++++ eosweb/core/templates/election/ballots.html | 4 +- eosweb/core/templates/election/booth.html | 58 ++++++++-- 19 files changed, 424 insertions(+), 34 deletions(-) create mode 100644 eosweb/core/static/nunjucks/booth/audit.html create mode 100644 eosweb/core/static/nunjucks/booth/cast.html create mode 100644 eosweb/core/static/nunjucks/booth/complete.html create mode 100644 eosweb/core/static/nunjucks/booth/encrypt.html create mode 100644 eosweb/core/static/nunjucks/booth/review.html create mode 100644 eosweb/core/static/nunjucks/question/approval/selections_review.html diff --git a/eos/base/election.py b/eos/base/election.py index b47702b..85c637e 100644 --- a/eos/base/election.py +++ b/eos/base/election.py @@ -30,13 +30,17 @@ class NullEncryptedAnswer(EncryptedAnswer): return self.answer class Ballot(EmbeddedObject): - _id = UUIDField() + #_id = UUIDField() encrypted_answers = EmbeddedObjectListField() +class Vote(EmbeddedObject): + ballot = EmbeddedObjectField() + cast_at = StringField() + class Voter(EmbeddedObject): _id = UUIDField() name = StringField() - ballots = EmbeddedObjectListField() + votes = EmbeddedObjectListField() class EmailVoter(Voter): email = StringField() diff --git a/eos/base/tests.py b/eos/base/tests.py index 143df2c..03a6436 100644 --- a/eos/base/tests.py +++ b/eos/base/tests.py @@ -94,7 +94,8 @@ class ElectionTestCase(EosTestCase): answer = ApprovalAnswer(choices=VOTES[i][j]) encrypted_answer = NullEncryptedAnswer(answer=answer) ballot.encrypted_answers.append(encrypted_answer) - election.voters[i].ballots.append(ballot) + vote = Vote(ballot=ballot) + election.voters[i].votes.append(vote) election.save() diff --git a/eos/base/workflow.py b/eos/base/workflow.py index 8d7ac8d..d5697a4 100644 --- a/eos/base/workflow.py +++ b/eos/base/workflow.py @@ -135,7 +135,9 @@ class TaskDecryptVotes(WorkflowTask): election.results.append(EosObject.objects['eos.base.election.RawResult']()) for voter in election.voters: - for ballot in voter.ballots: + if len(voter.votes) > 0: + vote = voter.votes[-1] + ballot = vote.ballot for i in range(len(ballot.encrypted_answers)): answer = ballot.encrypted_answers[i].decrypt() election.results[i].answers.append(answer) diff --git a/eos/core/objects/__init__.py b/eos/core/objects/__init__.py index 0d57434..a00910b 100644 --- a/eos/core/objects/__init__.py +++ b/eos/core/objects/__init__.py @@ -302,6 +302,9 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType): @classmethod def deserialise(cls, value): + if value is None: + return None + attrs = {} for attr, val in cls._fields.items(): json_attr = attr[3:] if attr.startswith('py_') else attr diff --git a/eos/psr/election.py b/eos/psr/election.py index 3eb6f39..bda3dbf 100644 --- a/eos/psr/election.py +++ b/eos/psr/election.py @@ -84,7 +84,9 @@ class MixingTrustee(Trustee): # Use the raw ballots from voters orig_answers = [] for voter in self.recurse_parents(Election).voters: - for ballot in voter.ballots: + if len(voter.votes) > 0: + vote = voter.votes[-1] + ballot = vote.ballot orig_answers.append(ballot.encrypted_answers[question_num]) return orig_answers diff --git a/eos/psr/tests.py b/eos/psr/tests.py index ded41d6..7a47986 100644 --- a/eos/psr/tests.py +++ b/eos/psr/tests.py @@ -265,7 +265,8 @@ class ElectionTestCase(EosTestCase): answer = ApprovalAnswer(choices=VOTES[i][j]) encrypted_answer = BlockEncryptedAnswer.encrypt(election.sk.public_key, answer) ballot.encrypted_answers.append(encrypted_answer) - election.voters[i].ballots.append(ballot) + vote = Vote(ballot=ballot) + election.voters[i].votes.append(vote) election.save() @@ -287,8 +288,8 @@ class ElectionTestCase(EosTestCase): else: orig_answers = [] for voter in election.voters: - for ballot in voter.ballots: - orig_answers.append(ballot.encrypted_answers[i]) + ballot = voter.votes[-1].ballot + orig_answers.append(ballot.encrypted_answers[i]) shuffled_answers, commitments = election.mixing_trustees[j].mixnets[i].shuffle(orig_answers) election.mixing_trustees[j].mixed_questions.append(EosList(shuffled_answers)) election.mixing_trustees[j].commitments.append(EosList(commitments)) diff --git a/eosweb/core/main.py b/eosweb/core/main.py index df5d3d8..def8809 100644 --- a/eosweb/core/main.py +++ b/eosweb/core/main.py @@ -26,7 +26,10 @@ from eos.psr.workflow import * import eos.core.hashing import eosweb +from datetime import datetime + import functools +import json app = flask.Flask(__name__) @@ -123,7 +126,32 @@ def election_view_ballots(election): def election_view_trustees(election): return flask.render_template('election/trustees.html', election=election) - +@app.route('/election//cast_ballot', methods=['POST']) +@using_election +def election_api_cast_vote(election): + data = json.loads(flask.request.data) + + voter = None + for election_voter in election.voters: + if election_voter.name == data['auth']['username']: + voter = election_voter + break + + if voter is None: + # User is not authenticated + return flask.Response('Invalid credentials', 403) + + # Cast the vote + ballot = EosObject.deserialise_and_unwrap(data['ballot']) + vote = Vote(ballot=ballot, cast_at=datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')) + voter.votes.append(vote) + + election.save() + + return flask.Response(json.dumps({ + 'voter': EosObject.serialise_and_wrap(voter), + 'vote': EosObject.serialise_and_wrap(vote) + }), mimetype='application/json') # === Model-Views === diff --git a/eosweb/core/modelview.py b/eosweb/core/modelview.py index 05d6637..c88e0e5 100644 --- a/eosweb/core/modelview.py +++ b/eosweb/core/modelview.py @@ -28,6 +28,7 @@ model_view_map = { 'tabs': 'election/psr/tabs.html' }, ApprovalQuestion: { - 'selections_make': 'question/approval/selections_make.html' + 'selections_make': 'question/approval/selections_make.html', + 'selections_review': 'question/approval/selections_review.html' } } diff --git a/eosweb/core/static/js/booth_worker.js b/eosweb/core/static/js/booth_worker.js index 12cc2a4..815f11e 100644 --- a/eosweb/core/static/js/booth_worker.js +++ b/eosweb/core/static/js/booth_worker.js @@ -19,10 +19,15 @@ 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() }); +function generateEncryptedVote(election, answers) { + encrypted_answers = []; + for (var answer_json of answers) { + answer = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(answer_json, null); + encrypted_answer = eosjs.eos.psr.election.__all__.BlockEncryptedAnswer.encrypt(election.public_key, answer); + encrypted_answers.push(eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(encrypted_answer, null)); + } - postMessage(eos_js.eos_core.libobjects.__all__.EosObject.serialise_and_wrap(encryptedVote, null)); + postMessage(encrypted_answers); } onmessage = function(msg) { @@ -34,9 +39,9 @@ onmessage = function(msg) { } if (msg.data.action === "generateEncryptedVote") { - msg.data.election = eosjs.eos.core.libobjects.__all__.EosObject.deserialise_and_unwrap(msg.data.election, null); + msg.data.election = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(msg.data.election, null); - generateEncryptedVote(msg.data.election, msg.data.selections); + generateEncryptedVote(msg.data.election, msg.data.answers); } else { throw "Unknown action: " + msg.data.action; } diff --git a/eosweb/core/static/nunjucks/booth/audit.html b/eosweb/core/static/nunjucks/booth/audit.html new file mode 100644 index 0000000..a2fd8a7 --- /dev/null +++ b/eosweb/core/static/nunjucks/booth/audit.html @@ -0,0 +1,28 @@ +{% 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 . +#} + +{% block content %} + TODO +{% endblock %} + +{% block buttons %} + +{% endblock %} diff --git a/eosweb/core/static/nunjucks/booth/base.html b/eosweb/core/static/nunjucks/booth/base.html index d2e997d..7dcfdf2 100644 --- a/eosweb/core/static/nunjucks/booth/base.html +++ b/eosweb/core/static/nunjucks/booth/base.html @@ -29,17 +29,19 @@ {% set menuindex = 3 %} {% elif template == 'booth/audit.html' %} {% set menuindex = 4 %} -{% elif template == 'booth/complete.html' %} +{% elif template == 'booth/cast.html' %} {% set menuindex = 5 %} +{% elif template == 'booth/complete.html' %} + {% set menuindex = 6 %} {% endif %}
diff --git a/eosweb/core/static/nunjucks/booth/cast.html b/eosweb/core/static/nunjucks/booth/cast.html new file mode 100644 index 0000000..4709c84 --- /dev/null +++ b/eosweb/core/static/nunjucks/booth/cast.html @@ -0,0 +1,103 @@ +{% 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 . +#} + +{% block content %} +
+

Your vote has not yet been cast. If you have not already done so, please make a note of your ballot fingerprint, {{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}.

+

This election requires you to log in to vote. Please enter your name below, then click ‘Cast ballot’ to cast your ballot.

+
+
+ + +
+
+ +
Error
+

The log in details you entered are not valid for this election. Please check your username and password and try again. If the issue persists, contact your election administrator.

+
+
+ + +
+ +{% endblock %} + +{% block buttons %} + +{% endblock %} + +{% block after %} + +{% endblock %} diff --git a/eosweb/core/static/nunjucks/booth/complete.html b/eosweb/core/static/nunjucks/booth/complete.html new file mode 100644 index 0000000..e3563e2 --- /dev/null +++ b/eosweb/core/static/nunjucks/booth/complete.html @@ -0,0 +1,37 @@ +{% 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 . +#} + +{% block content %} +

Your vote has been successfully cast. The following is your ‘smart ballot tracker’. Please retain a copy of your smart ballot tracker – you can use it to verify that your vote has been counted correctly. You may print this page as a receipt if you wish.

+ +
+
Smart ballot tracker
+

This smart ballot tracker confirms that {{ voter.py_name }} cast a vote in the election {{ election.py_name }} at {{ vote.cast_at }}.

+

Ballot fingerprint: {{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(vote.ballot).hash_as_b64() }}

+
+ +

Please check that the ballot fingerprint above matches the ballot fingerprint you recorded earlier.

+ +

To confirm that your ballot was cast correctly, please go to the ‘Voters and ballots’ page for the election or click ‘Finish’, and confirm that the above ballot fingerprint appears next to your name.

+{% endblock %} + +{% block buttons %} + Finish +{% endblock %} diff --git a/eosweb/core/static/nunjucks/booth/encrypt.html b/eosweb/core/static/nunjucks/booth/encrypt.html new file mode 100644 index 0000000..1f9ac1f --- /dev/null +++ b/eosweb/core/static/nunjucks/booth/encrypt.html @@ -0,0 +1,52 @@ +{# + 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 . +#} + +
Preparing your ballot. Please wait.
+ + diff --git a/eosweb/core/static/nunjucks/booth/review.html b/eosweb/core/static/nunjucks/booth/review.html new file mode 100644 index 0000000..24b8fe1 --- /dev/null +++ b/eosweb/core/static/nunjucks/booth/review.html @@ -0,0 +1,41 @@ +{% 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 . +#} + +{% block content %} +

Your vote has not yet been cast. Your selections are shown below. Please review your selections and ensure you are happy with them before continuing.

+ + {% for question in election.questions %} +

{{ loop.index }}. {{ question.prompt }}

+ {% include templates[selection_model_view_map[election.questions[loop.index0]._name]["selections_review"]] %} + {% endfor %} + +

If you are happy with your selections, then make a note of your ballot fingerprint, {{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}.

+

Click ‘Continue’, and you will be able to log into cast your vote.

+{% endblock %} + +{% block buttons %} + +{% endblock %} + +{% block after %} +
+

If you would like to audit your ballot, click here. Auditing your ballot is an optional step you can take to check that your vote has been prepared correctly. You do not need to audit your ballot in order to cast a vote.

+{% endblock %} diff --git a/eosweb/core/static/nunjucks/question/approval/selections_make.html b/eosweb/core/static/nunjucks/question/approval/selections_make.html index 432c74b..62b5ad2 100644 --- a/eosweb/core/static/nunjucks/question/approval/selections_make.html +++ b/eosweb/core/static/nunjucks/question/approval/selections_make.html @@ -51,8 +51,8 @@ } // Fill in ballot with previous selections - if (booth.selections[booth.questionNum]) { - for (var selection of booth.selections[booth.questionNum]) { + if (booth.answers[booth.questionNum]) { + for (var selection of booth.answers[booth.questionNum].value.choices) { // Answer already serialised $("#question-choice-" + selection).prop("checked", true); } choicesChanged(); @@ -63,6 +63,7 @@ $("#question-choices input:checked").each(function(i, el) { selections.push(parseInt(el.id.substring("question-choice-".length))) }); - booth.selections[booth.questionNum] = selections; + answer = eosjs.eos.base.election.__all__.ApprovalAnswer(eosjs.__kwargtrans__({choices: selections})); + booth.answers[booth.questionNum] = eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(answer); } diff --git a/eosweb/core/static/nunjucks/question/approval/selections_review.html b/eosweb/core/static/nunjucks/question/approval/selections_review.html new file mode 100644 index 0000000..fcb7444 --- /dev/null +++ b/eosweb/core/static/nunjucks/question/approval/selections_review.html @@ -0,0 +1,37 @@ +{# + 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 . +#} + +
+ {% for choice in booth.answers[loop.index0].value.choices %} +
+ +
{{ question.choices[choice] }}
+
+ {% else %} +
+ +
No candidates selected
+
+ {% endfor %} +
+ +{% if booth.answers[loop.index0].value.choices.length < question.max_choices %} +
+

You have selected fewer than the maximum allowed number of candidates. If this was not your intention, please click the ‘Back’ button below now, and correct your selections.

+
+{% endif %} diff --git a/eosweb/core/templates/election/ballots.html b/eosweb/core/templates/election/ballots.html index 64ce0e6..7fb4438 100644 --- a/eosweb/core/templates/election/ballots.html +++ b/eosweb/core/templates/election/ballots.html @@ -30,8 +30,8 @@ {% for voter in election.voters %} {{ voter.name }} - {% if voter.ballot|length > 0 %} - {{ SHA256().update_obj(voter.ballot[-1]).hash_as_b64() }} + {% if voter.votes|length > 0 %} + {{ SHA256().update_obj(voter.votes[-1].ballot).hash_as_b64() }} {% else %} {% endif %} diff --git a/eosweb/core/templates/election/booth.html b/eosweb/core/templates/election/booth.html index e3e83a2..cfcc0a1 100644 --- a/eosweb/core/templates/election/booth.html +++ b/eosweb/core/templates/election/booth.html @@ -42,7 +42,7 @@ function resetBooth() { booth = { "questionNum": 0, - "selections": [], + "answers": [], }; } resetBooth(); @@ -71,6 +71,7 @@ // Load templates for the question types for (var question of election.questions) { templates[selection_model_view_map[question._name]['selections_make']] = null; + templates[selection_model_view_map[question._name]['selections_review']] = null; } loadTemplates(); @@ -137,10 +138,11 @@ "templates": templates, "template": template, "election_base_url": "{{ url_for('election_api_json', election_id=election._id) }}", - "static_base_url": "{{ url_for('static', filename='nunjucks') }}", + "static_base_url": "{{ url_for('static', filename='') }}", "election": election, "booth": booth, "eosjs": eosjs, + "selection_model_view_map": selection_model_view_map }, opts); $(destination).html(templates[template].render(opts)); } catch (err) { @@ -149,13 +151,19 @@ } } - function nextTemplate() { - currentBoothTask++; + function nextTemplate(num) { + if (!num) { + num = 1; + } + currentBoothTask += num; boothTasks[currentBoothTask].activate(true); } - function prevTemplate() { - currentBoothTask--; + function prevTemplate(num) { + if (!num) { + num = 1; + } + currentBoothTask -= num; boothTasks[currentBoothTask].activate(false); } @@ -163,18 +171,52 @@ // TODO: Make modular boothTasks.append({ - activate: function(shouldInit) { + activate: function(fromLeft) { showTemplate('booth/welcome.html'); } }); templates['booth/base.html'] = null; templates['booth/welcome.html'] = null; boothTasks.append({ - activate: function(shouldInit) { + activate: function(fromLeft) { showTemplate('booth/selections.html'); } }); templates['booth/selections.html'] = null; + boothTasks.append({ + activate: function(fromLeft) { + if (fromLeft) { + showTemplate('booth/encrypt.html'); + } else { + prevTemplate(); + } + } + }); + templates['booth/encrypt.html'] = null; + boothTasks.append({ + activate: function(fromLeft) { + showTemplate('booth/review.html', {ballot: booth.ballot}); + } + }); + templates['booth/review.html'] = null; + boothTasks.append({ + activate: function(fromLeft) { + showTemplate('booth/audit.html', {ballot: booth.ballot}); + } + }); + templates['booth/audit.html'] = null; + boothTasks.append({ + activate: function(fromLeft) { + showTemplate('booth/cast.html', {ballot: booth.ballot}); + } + }); + templates['booth/cast.html'] = null; + boothTasks.append({ + activate: function(fromLeft) { + showTemplate('booth/complete.html', {voter: booth.voter, vote: booth.vote}); + } + }); + templates['booth/complete.html'] = null; // === END BOOTH TASKS ===