Add preferential voting support. Closes #4
This commit is contained in:
parent
5f05450ea4
commit
c2d3b4ab93
@ -101,6 +101,17 @@ class ApprovalQuestion(Question):
|
|||||||
class ApprovalAnswer(Answer):
|
class ApprovalAnswer(Answer):
|
||||||
choices = ListField(IntField())
|
choices = ListField(IntField())
|
||||||
|
|
||||||
|
class PreferentialQuestion(Question):
|
||||||
|
choices = ListField(StringField())
|
||||||
|
min_choices = IntField()
|
||||||
|
max_choices = IntField()
|
||||||
|
|
||||||
|
def pretty_answer(self, answer):
|
||||||
|
return ', '.join([self.choices[choice] for choice in answer.choices])
|
||||||
|
|
||||||
|
class PreferentialAnswer(Answer):
|
||||||
|
choices = ListField(IntField())
|
||||||
|
|
||||||
class RawResult(Result):
|
class RawResult(Result):
|
||||||
answers = EmbeddedObjectListField()
|
answers = EmbeddedObjectListField()
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"semantic": "semantic-ui#^2.2.13",
|
"semantic": "semantic-ui#^2.2.13",
|
||||||
"nunjucks": "^3.0.1"
|
"nunjucks": "^3.0.1",
|
||||||
|
"dragula.js": "dragula#^3.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ 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'], min_choices=0, max_choices=2)
|
question = PreferentialQuestion(prompt='President', choices=['John Smith', 'Joe Bloggs', 'John Q. Public'], min_choices=0, max_choices=3)
|
||||||
election.questions.append(question)
|
election.questions.append(question)
|
||||||
|
|
||||||
question = ApprovalQuestion(prompt='Chairman', choices=['John Doe', 'Andrew Citizen'], min_choices=0, max_choices=1)
|
question = ApprovalQuestion(prompt='Chairman', choices=['John Doe', 'Andrew Citizen'], min_choices=0, max_choices=1)
|
||||||
|
@ -27,6 +27,12 @@ model_view_map = {
|
|||||||
Election: {
|
Election: {
|
||||||
'tabs': 'election/core/tabs.html'
|
'tabs': 'election/core/tabs.html'
|
||||||
},
|
},
|
||||||
|
PreferentialQuestion: {
|
||||||
|
'view': 'question/preferential/view.html',
|
||||||
|
'result_raw': 'question/preferential/result_raw.html',
|
||||||
|
'selections_make': 'question/preferential/selections_make.html',
|
||||||
|
'selections_review': 'question/preferential/selections_review.html'
|
||||||
|
},
|
||||||
PSRElection: {
|
PSRElection: {
|
||||||
'tabs': 'election/psr/tabs.html'
|
'tabs': 'election/psr/tabs.html'
|
||||||
}
|
}
|
||||||
|
@ -36,3 +36,60 @@
|
|||||||
margin-top: 4em;
|
margin-top: 4em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Preferential voting */
|
||||||
|
|
||||||
|
.preferential-choices {
|
||||||
|
padding: 0.5em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferential-choices .dragarea {
|
||||||
|
min-height: 3em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferential-choices .dragarea-hint:first-child:last-child {
|
||||||
|
content: "";
|
||||||
|
width: calc(100% - 1em);
|
||||||
|
height: 3em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: -100;
|
||||||
|
border: 1px dashed #555;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
#question-choices-selected {
|
||||||
|
border: 1px solid #3465a4;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferential-choice {
|
||||||
|
background-color: #eee;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferential-choice:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#question-choices-selected .preferential-choice {
|
||||||
|
background-color: #e6f1fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#question-choices-remaining {
|
||||||
|
border: 1px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferential-choice .number, .preferential-choice .name {
|
||||||
|
padding: 0.5em 0 0.5em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferential-choice .number {
|
||||||
|
width: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<h2>{{ questionNum + 1 }}. {{ election.questions.__getitem__(questionNum).prompt }}</h2>
|
<h2>{{ questionNum + 1 }}. {{ election.questions.__getitem__(questionNum).prompt }}</h2>
|
||||||
|
|
||||||
<p><small>Vote for between {{ election.questions.__getitem__(questionNum).min_choices }} and {{ election.questions.__getitem__(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>
|
<p><small>Vote for between {{ election.questions.__getitem__(questionNum).min_choices }} and {{ election.questions.__getitem__(questionNum).max_choices }} choices. Click the check-boxes to the left of the choices 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 id="question-choices" class="ui form" style="margin-bottom: 1em;">
|
||||||
<div class="grouped fields">
|
<div class="grouped fields">
|
||||||
@ -34,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui hidden message" id="message-max-choices">
|
<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>
|
<p>You have now selected the maximum allowed number of choices. If you wish to change your selections, you must first use the check-boxes to deselect a choice.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
// Fill in ballot with previous selections
|
// Fill in ballot with previous selections
|
||||||
if (booth.answers[booth.questionNum]) {
|
if (booth.answers[booth.questionNum]) {
|
||||||
for (var selection of booth.answers.__getitem__(booth.questionNum).value.choices) { // Answer already serialised
|
for (var selection of booth.answers[booth.questionNum].value.choices) { // Answer already serialised
|
||||||
$("#question-choice-" + selection).prop("checked", true);
|
$("#question-choice-" + selection).prop("checked", true);
|
||||||
}
|
}
|
||||||
choicesChanged();
|
choicesChanged();
|
||||||
@ -61,7 +61,7 @@
|
|||||||
function saveSelections() {
|
function saveSelections() {
|
||||||
selections = [];
|
selections = [];
|
||||||
$("#question-choices input:checked").each(function(i, el) {
|
$("#question-choices input:checked").each(function(i, el) {
|
||||||
selections.push(parseInt(el.id.substring("question-choice-".length)))
|
selections.push(parseInt(el.id.substring("question-choice-".length)));
|
||||||
});
|
});
|
||||||
answer = eosjs.eos.base.election.__all__.ApprovalAnswer(eosjs.__kwargtrans__({choices: 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);
|
booth.answers[booth.questionNum] = eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(answer);
|
||||||
|
@ -25,13 +25,13 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<i class="warning circle icon"></i>
|
<i class="warning circle icon"></i>
|
||||||
<div class="content">No candidates selected</div>
|
<div class="content">No choices selected</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if booth.answers[loop.index0].value.choices.length < question.max_choices %}
|
{% if booth.answers[loop.index0].value.choices.length < question.max_choices %}
|
||||||
<div class="ui warning message">
|
<div class="ui warning message">
|
||||||
<p>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.</p>
|
<p>You have selected fewer than the maximum allowed number of choices. If this was not your intention, please click the ‘Back’ button below now, and correct your selections.</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
{#
|
||||||
|
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.__getitem__(questionNum).prompt }}</h2>
|
||||||
|
|
||||||
|
<p><small>Vote for between {{ election.questions.__getitem__(questionNum).min_choices }} and {{ election.questions.__getitem__(questionNum).max_choices }} choices. Click and drag the choices from the grey box to the blue box in order from most-preferred to least-preferred. It is in your best interests to vote for as many choices as you can.</small></p>
|
||||||
|
|
||||||
|
<div id="question-choices-selected" class="preferential-choices">
|
||||||
|
<div style="color: #3465a4;">Options voted for:</div>
|
||||||
|
<div class="dragarea">
|
||||||
|
<div class="dragarea-hint"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="question-choices-remaining" class="preferential-choices">
|
||||||
|
<div>Options not yet voted for:</div>
|
||||||
|
<div class="dragarea">
|
||||||
|
<div class="dragarea-hint"></div>
|
||||||
|
{% for choice in election.questions.__getitem__(questionNum).choices.impl %}
|
||||||
|
<div class="preferential-choice" data-choiceno="{{ loop.index0 }}">
|
||||||
|
<div class="number"></div>
|
||||||
|
<div class="name">{{ choice }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui hidden message" id="message-max-choices">
|
||||||
|
<p>You have now selected the maximum allowed number of choices. If you wish to change your selections, you must first use the check-boxes to deselect a choice.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function choicesChanged() {
|
||||||
|
// Recalculate numbers
|
||||||
|
$(".preferential-choices .preferential-choice .number").each(function(i, el) {
|
||||||
|
$(el).text("");
|
||||||
|
});
|
||||||
|
$("#question-choices-selected .preferential-choice .number").each(function(i, el) {
|
||||||
|
$(el).text(i + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in ballot with previous selections
|
||||||
|
if (booth.answers[booth.questionNum]) {
|
||||||
|
for (var selection of booth.answers[booth.questionNum].value.choices) { // Answer already serialised
|
||||||
|
$(".preferential-choice[data-choiceno=" + selection + "]").detach().appendTo("#question-choices-selected .dragarea");
|
||||||
|
}
|
||||||
|
choicesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
var dragulaChoices = dragula([document.querySelector("#question-choices-selected .dragarea"), document.querySelector("#question-choices-remaining .dragarea")], { moves: function(el, source, handle, sibling) { return !("dragarea-hint" in el.classList); } });
|
||||||
|
|
||||||
|
dragulaChoices.on("drop", function(el, target, source, sibling) {
|
||||||
|
choicesChanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
function saveSelections() {
|
||||||
|
selections = [];
|
||||||
|
$("#question-choices-selected .preferential-choice").each(function(i, el) {
|
||||||
|
selections.push(parseInt(el.dataset.choiceno));
|
||||||
|
});
|
||||||
|
answer = eosjs.eos.base.election.__all__.PreferentialAnswer(eosjs.__kwargtrans__({choices: selections}));
|
||||||
|
booth.answers[booth.questionNum] = eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(answer);
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,40 @@
|
|||||||
|
{#
|
||||||
|
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/>.
|
||||||
|
#}
|
||||||
|
|
||||||
|
{% if booth.answers[loop.index0].value.choices.length > 0 %}
|
||||||
|
<div class="ui ordered list">
|
||||||
|
{% for choice in booth.answers[loop.index0].value.choices %}
|
||||||
|
<div class="item">
|
||||||
|
<div class="content">{{ question.choices.__getitem__(choice) }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="ui list">
|
||||||
|
<div class="item">
|
||||||
|
<i class="warning circle icon"></i>
|
||||||
|
<div class="content">No choices selected</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if booth.answers[loop.index0].value.choices.length < question.max_choices %}
|
||||||
|
<div class="ui warning message">
|
||||||
|
<p>You have selected fewer than the maximum allowed number of choices. If this was not your intention, please click the ‘Back’ button below now, and correct your selections.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
@ -20,6 +20,11 @@
|
|||||||
|
|
||||||
{% block title %}{{ election.name }} – Voting booth{% endblock %}
|
{% block title %}{{ election.name }} – Voting booth{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='bower_components/dragula.js/dist/dragula.min.css') }}" type="text/css">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui container" id="booth-content">
|
<div class="ui container" id="booth-content">
|
||||||
<div class="ui active text loader">Loading voting booth. Please wait.</div>
|
<div class="ui active text loader">Loading voting booth. Please wait.</div>
|
||||||
@ -28,8 +33,12 @@
|
|||||||
|
|
||||||
{% block basecontent %}
|
{% block basecontent %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='bower_components/nunjucks/browser/nunjucks.min.js') }}"></script>
|
<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 src="{{ url_for('static', filename='js/eosjs.js') }}"></script>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='bower_components/dragula.js/dist/dragula.min.js') }}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var templates = {};
|
var templates = {};
|
||||||
var election = null;
|
var election = null;
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#}
|
#}
|
||||||
|
|
||||||
|
<p><small>Approval voting. Vote for between {{ question.min_choices }} and {{ question.max_choices }} choices.</small></p>
|
||||||
|
|
||||||
<ul class="ui list">
|
<ul class="ui list">
|
||||||
{% for choice in question.choices %}
|
{% for choice in question.choices %}
|
||||||
<li>{{ choice }}</li>
|
<li>{{ choice }}</li>
|
||||||
|
26
eosweb/core/templates/question/preferential/result_raw.html
Normal file
26
eosweb/core/templates/question/preferential/result_raw.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{#
|
||||||
|
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/>.
|
||||||
|
#}
|
||||||
|
|
||||||
|
<table class="ui celled table">
|
||||||
|
{% for answer, num in election.results[loop.index0].count() %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ question.pretty_answer(answer) }}</td>
|
||||||
|
<td>{{ num }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
25
eosweb/core/templates/question/preferential/view.html
Normal file
25
eosweb/core/templates/question/preferential/view.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{#
|
||||||
|
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/>.
|
||||||
|
#}
|
||||||
|
|
||||||
|
<p><small>Preferential voting. Vote for between {{ question.min_choices }} and {{ question.max_choices }} choices.</small></p>
|
||||||
|
|
||||||
|
<ul class="ui list">
|
||||||
|
{% for choice in question.choices %}
|
||||||
|
<li>{{ choice }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
Loading…
Reference in New Issue
Block a user