Implement ballot counting

This commit is contained in:
RunasSudo 2017-12-16 21:17:17 +10:30
parent 25a9a27434
commit 764660f317
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
12 changed files with 184 additions and 17 deletions

View File

@ -205,6 +205,14 @@ class RawResult(Result):
combined.sort(key=lambda x: x[1], reverse=True)
return combined
class MultipleResult(Result):
results = EmbeddedObjectListField()
class STVResult(Result):
elected = ListField(IntField())
log = StringField()
random = BlobField()
class Election(TopLevelObject):
_id = UUIDField()
workflow = EmbeddedObjectField(Workflow) # Once saved, we don't care what kind of workflow it is

62
eos/base/tasks.py Normal file
View 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

View File

@ -21,27 +21,27 @@ def writeBLT(election, q_num, seats, withdrawn=[]):
electionLines = []
electionLines.append('{} {}\n'.format(len(flat_choices), seats))
electionLines.append('{} {}'.format(len(flat_choices), seats))
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()
for answer, count in result:
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:
electionLines.append('{} 0\n'.format(count))
electionLines.append('{} 0'.format(count))
electionLines.append('0\n')
electionLines.append('0')
for candidate in flat_choices:
if candidate.party:
electionLines.append("'{}{}'\n".format(candidate.name, candidate.party))
electionLines.append("'{}{}'".format(candidate.name, candidate.party))
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

View File

@ -242,6 +242,9 @@ class PSRElection(Election):
# Verify decryption proofs
for q_num in range(len(self.questions)):
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)):
# Input and output blocks:
plaintexts = raw_result.plaintexts[answer_num]

View File

@ -21,6 +21,7 @@ import timeago
from eos.core.objects import *
from eos.core.tasks import *
from eos.base.election import *
from eos.base.tasks import *
from eos.base.workflow import *
from eos.psr.crypto import *
from eos.psr.election import *
@ -153,6 +154,26 @@ def verify_election(electionid):
election.verify()
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
def inject_globals():
return {'eos': eos, 'eosweb': eosweb, 'SHA256': eos.core.hashing.SHA256}
@ -313,8 +334,7 @@ def election_api_cast_vote(election):
@using_election
def election_api_export_question(election, q_num, format):
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(''.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('\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.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
return resp

View File

@ -20,7 +20,7 @@ from eos.psr.election import *
model_view_map = {
ApprovalQuestion: {
'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_review': 'question/approval/selections_review.html'
},
@ -29,7 +29,8 @@ model_view_map = {
},
PreferentialQuestion: {
'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_review': 'question/preferential/selections_review.html'
},

View File

@ -45,6 +45,11 @@
{{ super() }}
<script>
function initTab() {
$('.tabular.menu .item').tab();
$('.ui.accordion').accordion();
}
$(".election-tab-ajax").click(function() {
var linkEl = $(this);
history.pushState({}, "", linkEl.attr("href"));
@ -56,8 +61,12 @@
$("#election-tab-content").load(linkEl.attr("href") + " #election-tab-content", function() {
linkEl.find(".loader").removeClass("active");
initTab();
});
return false;
});
initTab();
</script>
{% endblock %}

View File

@ -92,8 +92,24 @@
{% endif %}
{% for question in election.questions %}
<h3>{{ loop.index }}. {{ question.prompt }}</h2>
{% include eosweb.core.main.model_view_map[question.__class__]['result_raw'] %}
<h3>{{ loop.index }}. {{ question.prompt }}</h3>
{% 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 %}
{% endif %}

View File

@ -17,7 +17,7 @@
#}
<table class="ui celled table">
{% for answer, num in election.results[loop.index0].count() %}
{% for answer, num in result.count() %}
<tr>
<td class="fourteen wide">{{ question.pretty_answer(answer) }}</td>
<td class="two wide">{{ num }}</td>

View File

@ -16,10 +16,10 @@
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">
{% for answer, num in election.results[loop.index0].count() %}
{% for answer, num in result.count() %}
<tr>
<td class="fourteen wide">{{ question.pretty_answer(answer) }}</td>
<td class="two wide">{{ num }}</td>

View 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>

View File

@ -15,6 +15,7 @@ psutil==5.4.1
psycopg2==2.7.3.2
PyExecJS==1.4.1
pymongo==3.5.1
pyRCV==0.3
pytz==2017.3
requests==2.18.4
requests-oauthlib==0.8.0