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):
|
||||
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):
|
||||
answers = EmbeddedObjectListField()
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"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.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)
|
||||
|
||||
question = ApprovalQuestion(prompt='Chairman', choices=['John Doe', 'Andrew Citizen'], min_choices=0, max_choices=1)
|
||||
|
@ -27,6 +27,12 @@ model_view_map = {
|
||||
Election: {
|
||||
'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: {
|
||||
'tabs': 'election/psr/tabs.html'
|
||||
}
|
||||
|
@ -36,3 +36,60 @@
|
||||
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>
|
||||
|
||||
<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 class="grouped fields">
|
||||
@ -34,7 +34,7 @@
|
||||
</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>
|
||||
<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>
|
||||
@ -52,7 +52,7 @@
|
||||
|
||||
// Fill in ballot with previous selections
|
||||
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);
|
||||
}
|
||||
choicesChanged();
|
||||
@ -61,7 +61,7 @@
|
||||
function saveSelections() {
|
||||
selections = [];
|
||||
$("#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}));
|
||||
booth.answers[booth.questionNum] = eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(answer);
|
||||
|
@ -25,13 +25,13 @@
|
||||
{% else %}
|
||||
<div class="item">
|
||||
<i class="warning circle icon"></i>
|
||||
<div class="content">No candidates selected</div>
|
||||
<div class="content">No choices selected</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% 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 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>
|
||||
{% 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 head %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='bower_components/dragula.js/dist/dragula.min.css') }}" type="text/css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="ui container" id="booth-content">
|
||||
<div class="ui active text loader">Loading voting booth. Please wait.</div>
|
||||
@ -28,8 +33,12 @@
|
||||
|
||||
{% 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 src="{{ url_for('static', filename='bower_components/dragula.js/dist/dragula.min.js') }}"></script>
|
||||
|
||||
<script>
|
||||
var templates = {};
|
||||
var election = null;
|
||||
|
@ -16,6 +16,8 @@
|
||||
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">
|
||||
{% for choice in question.choices %}
|
||||
<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