diff --git a/eos/base/election.py b/eos/base/election.py
index 7c22723..cb56e4c 100644
--- a/eos/base/election.py
+++ b/eos/base/election.py
@@ -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
diff --git a/eos/base/tasks.py b/eos/base/tasks.py
new file mode 100644
index 0000000..62b4c7f
--- /dev/null
+++ b/eos/base/tasks.py
@@ -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 .
+
+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
diff --git a/eos/base/util/blt.py b/eos/base/util/blt.py
index 4228205..b278bbb 100644
--- a/eos/base/util/blt.py
+++ b/eos/base/util/blt.py
@@ -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
diff --git a/eos/psr/election.py b/eos/psr/election.py
index 0e859ec..cb5e854 100644
--- a/eos/psr/election.py
+++ b/eos/psr/election.py
@@ -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]
diff --git a/eosweb/core/main.py b/eosweb/core/main.py
index e2bf47a..36bbd3d 100644
--- a/eosweb/core/main.py
+++ b/eosweb/core/main.py
@@ -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
diff --git a/eosweb/core/modelview.py b/eosweb/core/modelview.py
index 9616f6f..937a38b 100644
--- a/eosweb/core/modelview.py
+++ b/eosweb/core/modelview.py
@@ -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'
},
diff --git a/eosweb/core/templates/election/base.html b/eosweb/core/templates/election/base.html
index 3abf7fc..3694e55 100644
--- a/eosweb/core/templates/election/base.html
+++ b/eosweb/core/templates/election/base.html
@@ -45,6 +45,11 @@
{{ super() }}
{% endblock %}
diff --git a/eosweb/core/templates/election/view/view.html b/eosweb/core/templates/election/view/view.html
index 387e524..4f6156b 100644
--- a/eosweb/core/templates/election/view/view.html
+++ b/eosweb/core/templates/election/view/view.html
@@ -92,8 +92,24 @@
{% endif %}
{% for question in election.questions %}
-
{{ loop.index }}. {{ question.prompt }}
- {% include eosweb.core.main.model_view_map[question.__class__]['result_raw'] %}
+ {{ loop.index }}. {{ question.prompt }}
+ {% set q_num = loop.index0 %}
+ {% set result = election.results[q_num] %}
+ {% if result._name == 'eos.base.election.MultipleResult' %}
+ {% set result1 = result %}
+
+ {% for result in result1.results %}
+
+ {% include eosweb.core.main.model_view_map[question.__class__]['result_' + result._name] %}
+
+ {% endfor %}
+ {% else %}
+ {% include eosweb.core.main.model_view_map[question.__class__]['result_' + result._name] %}
+ {% endif %}
{% endfor %}
{% endif %}
diff --git a/eosweb/core/templates/question/approval/result_raw.html b/eosweb/core/templates/question/approval/result_raw.html
index 0b95044..7a6b050 100644
--- a/eosweb/core/templates/question/approval/result_raw.html
+++ b/eosweb/core/templates/question/approval/result_raw.html
@@ -17,7 +17,7 @@
#}
- {% for answer, num in election.results[loop.index0].count() %}
+ {% for answer, num in result.count() %}
{{ question.pretty_answer(answer) }} |
{{ num }} |
diff --git a/eosweb/core/templates/question/preferential/result_raw.html b/eosweb/core/templates/question/preferential/result_raw.html
index 8629157..bd222b1 100644
--- a/eosweb/core/templates/question/preferential/result_raw.html
+++ b/eosweb/core/templates/question/preferential/result_raw.html
@@ -16,10 +16,10 @@
along with this program. If not, see .
#}
- Export as OpenSTV BLT
+ Export as OpenSTV BLT
- {% for answer, num in election.results[loop.index0].count() %}
+ {% for answer, num in result.count() %}
{{ question.pretty_answer(answer) }} |
{{ num }} |
diff --git a/eosweb/core/templates/question/preferential/result_stv.html b/eosweb/core/templates/question/preferential/result_stv.html
new file mode 100644
index 0000000..99834f5
--- /dev/null
+++ b/eosweb/core/templates/question/preferential/result_stv.html
@@ -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 .
+#}
+
+
+
+
+ Candidate |
+ Elected |
+
+
+
+ {% set flat_choices = question.flatten_choices() %}
+ {% for elected in result.elected %}
+
+ {{ flat_choices[elected].name }}{% if flat_choices[elected].party %} – {{ flat_choices[elected].party }}{% endif %} |
+ {{ loop.index }} |
+
+ {% endfor %}
+
+
+
+
diff --git a/requirements.txt b/requirements.txt
index 729dbae..3f50a54 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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