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() %} 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

{{ question.pretty_answer(answer) }} {{ num }}
- {% for answer, num in election.results[loop.index0].count() %} + {% for answer, num in result.count() %} 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 . +#} + +
{{ question.pretty_answer(answer) }} {{ num }}
+ + + + + + + + {% set flat_choices = question.flatten_choices() %} + {% for elected in result.elected %} + + + + + {% endfor %} + +
CandidateElected
{{ flat_choices[elected].name }}{% if flat_choices[elected].party %} – {{ flat_choices[elected].party }}{% endif %}{{ loop.index }}
+ +
+
+ + Count log +
+
+
+ +
+
+
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