Implement ballot counting
This commit is contained in:
parent
25a9a27434
commit
764660f317
@ -205,6 +205,14 @@ class RawResult(Result):
|
|||||||
combined.sort(key=lambda x: x[1], reverse=True)
|
combined.sort(key=lambda x: x[1], reverse=True)
|
||||||
return combined
|
return combined
|
||||||
|
|
||||||
|
class MultipleResult(Result):
|
||||||
|
results = EmbeddedObjectListField()
|
||||||
|
|
||||||
|
class STVResult(Result):
|
||||||
|
elected = ListField(IntField())
|
||||||
|
log = StringField()
|
||||||
|
random = BlobField()
|
||||||
|
|
||||||
class Election(TopLevelObject):
|
class Election(TopLevelObject):
|
||||||
_id = UUIDField()
|
_id = UUIDField()
|
||||||
workflow = EmbeddedObjectField(Workflow) # Once saved, we don't care what kind of workflow it is
|
workflow = EmbeddedObjectField(Workflow) # Once saved, we don't care what kind of workflow it is
|
||||||
|
62
eos/base/tasks.py
Normal file
62
eos/base/tasks.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# 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/>.
|
||||||
|
|
||||||
|
from eos.core.objects import *
|
||||||
|
from eos.core.tasks import *
|
||||||
|
from eos.base.election import *
|
||||||
|
|
||||||
|
import eos.base.util.blt
|
||||||
|
|
||||||
|
import pyRCV.stv
|
||||||
|
import pyRCV.utils.blt
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
class QuietSTVCounter(pyRCV.stv.STVCounter):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.randdata = base64.b64decode(self.args['randjson']['result']['random']['data'][0])
|
||||||
|
|
||||||
|
self.output = []
|
||||||
|
|
||||||
|
def log(self, string, *args):
|
||||||
|
self.output.append(string.format(*args))
|
||||||
|
|
||||||
|
class TaskTallySTV(Task):
|
||||||
|
election_id = UUIDField()
|
||||||
|
q_num = IntField()
|
||||||
|
random = BlobField()
|
||||||
|
num_seats = IntField()
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
election = Election.get_by_id(self.election_id)
|
||||||
|
|
||||||
|
# Count the ballots
|
||||||
|
blt = eos.base.util.blt.writeBLT(election, self.q_num, self.num_seats)
|
||||||
|
ballots, candidates, seats = pyRCV.utils.blt.readBLT(blt)
|
||||||
|
counter = QuietSTVCounter(ballots, candidates, seats=seats, ties=['backwards', 'random'], randjson=self.random, verbose=True, quota='gt-hb')
|
||||||
|
elected, exhausted = counter.countVotes()
|
||||||
|
|
||||||
|
election.results[self.q_num] = MultipleResult(results=[election.results[self.q_num]])
|
||||||
|
result = STVResult(elected=[candidates.index(x) for x in elected], log='\n'.join(counter.output), random=self.random)
|
||||||
|
|
||||||
|
election.results[self.q_num].results.append(result)
|
||||||
|
election.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self):
|
||||||
|
election = Election.get_by_id(self.election_id)
|
||||||
|
return 'Tally STV question – ' + election.questions[self.q_num].prompt + ' – ' + election.name
|
@ -21,27 +21,27 @@ def writeBLT(election, q_num, seats, withdrawn=[]):
|
|||||||
|
|
||||||
electionLines = []
|
electionLines = []
|
||||||
|
|
||||||
electionLines.append('{} {}\n'.format(len(flat_choices), seats))
|
electionLines.append('{} {}'.format(len(flat_choices), seats))
|
||||||
|
|
||||||
if len(withdrawn) > 0:
|
if len(withdrawn) > 0:
|
||||||
electionLines.append(' '.join(['-{}'.format(flat_choices.index(candidate) + 1) for candidate in withdrawn]) + '\n')
|
electionLines.append(' '.join(['-{}'.format(flat_choices.index(candidate) + 1) for candidate in withdrawn]))
|
||||||
|
|
||||||
result = election.results[q_num].count()
|
result = election.results[q_num].count()
|
||||||
|
|
||||||
for answer, count in result:
|
for answer, count in result:
|
||||||
if answer.choices:
|
if answer.choices:
|
||||||
electionLines.append('{} {} 0\n'.format(count, ' '.join(str(x + 1) for x in answer.choices)))
|
electionLines.append('{} {} 0'.format(count, ' '.join(str(x + 1) for x in answer.choices)))
|
||||||
else:
|
else:
|
||||||
electionLines.append('{} 0\n'.format(count))
|
electionLines.append('{} 0'.format(count))
|
||||||
|
|
||||||
electionLines.append('0\n')
|
electionLines.append('0')
|
||||||
|
|
||||||
for candidate in flat_choices:
|
for candidate in flat_choices:
|
||||||
if candidate.party:
|
if candidate.party:
|
||||||
electionLines.append("'{} – {}'\n".format(candidate.name, candidate.party))
|
electionLines.append("'{} – {}'".format(candidate.name, candidate.party))
|
||||||
else:
|
else:
|
||||||
electionLines.append("'{}'\n".format(candidate.name))
|
electionLines.append("'{}'".format(candidate.name))
|
||||||
|
|
||||||
electionLines.append("'{} – {}'\n".format(election.name, question.prompt))
|
electionLines.append("'{} – {}'".format(election.name, question.prompt))
|
||||||
|
|
||||||
return electionLines
|
return electionLines
|
||||||
|
@ -242,6 +242,9 @@ class PSRElection(Election):
|
|||||||
# Verify decryption proofs
|
# Verify decryption proofs
|
||||||
for q_num in range(len(self.questions)):
|
for q_num in range(len(self.questions)):
|
||||||
raw_result = self.results[q_num]
|
raw_result = self.results[q_num]
|
||||||
|
if isinstance(raw_result, MultipleResult):
|
||||||
|
raw_result = next(result for result in raw_result.results if isinstance(result, RawResult))
|
||||||
|
|
||||||
for answer_num in range(len(raw_result.plaintexts)):
|
for answer_num in range(len(raw_result.plaintexts)):
|
||||||
# Input and output blocks:
|
# Input and output blocks:
|
||||||
plaintexts = raw_result.plaintexts[answer_num]
|
plaintexts = raw_result.plaintexts[answer_num]
|
||||||
|
@ -21,6 +21,7 @@ import timeago
|
|||||||
from eos.core.objects import *
|
from eos.core.objects import *
|
||||||
from eos.core.tasks import *
|
from eos.core.tasks import *
|
||||||
from eos.base.election import *
|
from eos.base.election import *
|
||||||
|
from eos.base.tasks import *
|
||||||
from eos.base.workflow import *
|
from eos.base.workflow import *
|
||||||
from eos.psr.crypto import *
|
from eos.psr.crypto import *
|
||||||
from eos.psr.election import *
|
from eos.psr.election import *
|
||||||
@ -153,6 +154,26 @@ def verify_election(electionid):
|
|||||||
election.verify()
|
election.verify()
|
||||||
print('The election has passed validation')
|
print('The election has passed validation')
|
||||||
|
|
||||||
|
@app.cli.command('tally_stv')
|
||||||
|
@click.option('--electionid', default=None)
|
||||||
|
@click.option('--qnum', default=0)
|
||||||
|
@click.option('--randfile', default=None)
|
||||||
|
def tally_stv_election(electionid, qnum, randfile):
|
||||||
|
election = Election.get_by_id(electionid)
|
||||||
|
|
||||||
|
with open(randfile, 'r') as f:
|
||||||
|
dat = json.load(f)
|
||||||
|
task = TaskTallySTV(
|
||||||
|
election_id=election._id,
|
||||||
|
q_num=qnum,
|
||||||
|
random=dat,
|
||||||
|
num_seats=7,
|
||||||
|
status=Task.Status.READY,
|
||||||
|
run_strategy=EosObject.lookup(app.config['TASK_RUN_STRATEGY'])()
|
||||||
|
)
|
||||||
|
task.save()
|
||||||
|
task.run()
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_globals():
|
def inject_globals():
|
||||||
return {'eos': eos, 'eosweb': eosweb, 'SHA256': eos.core.hashing.SHA256}
|
return {'eos': eos, 'eosweb': eosweb, 'SHA256': eos.core.hashing.SHA256}
|
||||||
@ -313,8 +334,7 @@ def election_api_cast_vote(election):
|
|||||||
@using_election
|
@using_election
|
||||||
def election_api_export_question(election, q_num, format):
|
def election_api_export_question(election, q_num, format):
|
||||||
import eos.base.util.blt
|
import eos.base.util.blt
|
||||||
#return flask.Response(''.join(eos.base.util.blt.writeBLT(election, q_num, 2)), mimetype='text/plain')
|
resp = flask.send_file(io.BytesIO('\n'.join(eos.base.util.blt.writeBLT(election, q_num, 2)).encode('utf-8')), mimetype='text/plain; charset=utf-8', attachment_filename='{}.blt'.format(q_num), as_attachment=True)
|
||||||
resp = flask.send_file(io.BytesIO(''.join(eos.base.util.blt.writeBLT(election, q_num, 2)).encode('utf-8')), mimetype='text/plain; charset=utf-8', attachment_filename='{}.blt'.format(q_num), as_attachment=True)
|
|
||||||
resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ from eos.psr.election import *
|
|||||||
model_view_map = {
|
model_view_map = {
|
||||||
ApprovalQuestion: {
|
ApprovalQuestion: {
|
||||||
'view': 'question/approval/view.html',
|
'view': 'question/approval/view.html',
|
||||||
'result_raw': 'question/approval/result_raw.html',
|
'result_eos.base.election.RawResult': 'question/approval/result_raw.html',
|
||||||
'selections_make': 'question/approval/selections_make.html',
|
'selections_make': 'question/approval/selections_make.html',
|
||||||
'selections_review': 'question/approval/selections_review.html'
|
'selections_review': 'question/approval/selections_review.html'
|
||||||
},
|
},
|
||||||
@ -29,7 +29,8 @@ model_view_map = {
|
|||||||
},
|
},
|
||||||
PreferentialQuestion: {
|
PreferentialQuestion: {
|
||||||
'view': 'question/preferential/view.html',
|
'view': 'question/preferential/view.html',
|
||||||
'result_raw': 'question/preferential/result_raw.html',
|
'result_eos.base.election.RawResult': 'question/preferential/result_raw.html',
|
||||||
|
'result_eos.base.election.STVResult': 'question/preferential/result_stv.html',
|
||||||
'selections_make': 'question/preferential/selections_make.html',
|
'selections_make': 'question/preferential/selections_make.html',
|
||||||
'selections_review': 'question/preferential/selections_review.html'
|
'selections_review': 'question/preferential/selections_review.html'
|
||||||
},
|
},
|
||||||
|
@ -45,6 +45,11 @@
|
|||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function initTab() {
|
||||||
|
$('.tabular.menu .item').tab();
|
||||||
|
$('.ui.accordion').accordion();
|
||||||
|
}
|
||||||
|
|
||||||
$(".election-tab-ajax").click(function() {
|
$(".election-tab-ajax").click(function() {
|
||||||
var linkEl = $(this);
|
var linkEl = $(this);
|
||||||
history.pushState({}, "", linkEl.attr("href"));
|
history.pushState({}, "", linkEl.attr("href"));
|
||||||
@ -56,8 +61,12 @@
|
|||||||
|
|
||||||
$("#election-tab-content").load(linkEl.attr("href") + " #election-tab-content", function() {
|
$("#election-tab-content").load(linkEl.attr("href") + " #election-tab-content", function() {
|
||||||
linkEl.find(".loader").removeClass("active");
|
linkEl.find(".loader").removeClass("active");
|
||||||
|
|
||||||
|
initTab();
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
initTab();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -92,8 +92,24 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for question in election.questions %}
|
{% for question in election.questions %}
|
||||||
<h3>{{ loop.index }}. {{ question.prompt }}</h2>
|
<h3>{{ loop.index }}. {{ question.prompt }}</h3>
|
||||||
{% include eosweb.core.main.model_view_map[question.__class__]['result_raw'] %}
|
{% set q_num = loop.index0 %}
|
||||||
|
{% set result = election.results[q_num] %}
|
||||||
|
{% if result._name == 'eos.base.election.MultipleResult' %}
|
||||||
|
{% set result1 = result %}
|
||||||
|
<div class="ui top attached tabular menu">
|
||||||
|
{% for result in result1.results %}
|
||||||
|
<div class="{% if loop.index0 == 0 %}active {% endif %}item" data-tab="result-{{ q_num }}-{{ loop.index0 }}">{{ result.__class__.__name__.replace('Result', '') }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% for result in result1.results %}
|
||||||
|
<div class="ui bottom attached{% if loop.index0 == 0 %} active{% endif %} tab segment" data-tab="result-{{ q_num }}-{{ loop.index0 }}">
|
||||||
|
{% include eosweb.core.main.model_view_map[question.__class__]['result_' + result._name] %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{% include eosweb.core.main.model_view_map[question.__class__]['result_' + result._name] %}
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
#}
|
#}
|
||||||
|
|
||||||
<table class="ui celled table">
|
<table class="ui celled table">
|
||||||
{% for answer, num in election.results[loop.index0].count() %}
|
{% for answer, num in result.count() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fourteen wide">{{ question.pretty_answer(answer) }}</td>
|
<td class="fourteen wide">{{ question.pretty_answer(answer) }}</td>
|
||||||
<td class="two wide">{{ num }}</td>
|
<td class="two wide">{{ num }}</td>
|
||||||
|
@ -16,10 +16,10 @@
|
|||||||
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><a href="{{ url_for('election_api_export_question', election_id=election._id, q_num=loop.index0, format='blt') }}" class="mini ui labeled icon button"><i class="download icon"></i> Export as OpenSTV BLT</a></p>
|
<p><a href="{{ url_for('election_api_export_question', election_id=election._id, q_num=q_num, format='blt') }}" class="mini ui labeled icon button"><i class="download icon"></i> Export as OpenSTV BLT</a></p>
|
||||||
|
|
||||||
<table class="ui celled table">
|
<table class="ui celled table">
|
||||||
{% for answer, num in election.results[loop.index0].count() %}
|
{% for answer, num in result.count() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fourteen wide">{{ question.pretty_answer(answer) }}</td>
|
<td class="fourteen wide">{{ question.pretty_answer(answer) }}</td>
|
||||||
<td class="two wide">{{ num }}</td>
|
<td class="two wide">{{ num }}</td>
|
||||||
|
47
eosweb/core/templates/question/preferential/result_stv.html
Normal file
47
eosweb/core/templates/question/preferential/result_stv.html
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{#
|
||||||
|
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">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="fourteen wide">Candidate</th>
|
||||||
|
<th class="two wide">Elected</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% set flat_choices = question.flatten_choices() %}
|
||||||
|
{% for elected in result.elected %}
|
||||||
|
<tr>
|
||||||
|
<td class="fourteen wide">{{ flat_choices[elected].name }}{% if flat_choices[elected].party %} – {{ flat_choices[elected].party }}{% endif %}</td>
|
||||||
|
<td class="two wide">{{ loop.index }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="ui accordion">
|
||||||
|
<div class="title">
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
Count log
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="ui form">
|
||||||
|
<textarea style="font-family: monospace;" rows="20">{{ result.log }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -15,6 +15,7 @@ psutil==5.4.1
|
|||||||
psycopg2==2.7.3.2
|
psycopg2==2.7.3.2
|
||||||
PyExecJS==1.4.1
|
PyExecJS==1.4.1
|
||||||
pymongo==3.5.1
|
pymongo==3.5.1
|
||||||
|
pyRCV==0.3
|
||||||
pytz==2017.3
|
pytz==2017.3
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
requests-oauthlib==0.8.0
|
requests-oauthlib==0.8.0
|
||||||
|
Loading…
Reference in New Issue
Block a user