diff --git a/HOWTO.md b/HOWTO.md index 5baa4f8..01e726c 100644 --- a/HOWTO.md +++ b/HOWTO.md @@ -126,20 +126,6 @@ You should now be able to see the election in the web interface. Exit the Flask shell by pressing Ctrl+D. -## Closing and counting an election +## Administrating an election -To close an election, helper commands are available, so use of the Flask shell is not required. Locally, run: - - FLASK_APP=eosweb EOSWEB_SETTINGS=$PWD/local_settings.py python -m flask close_election - -On Heroku, run: - - heroku run FLASK_APP=eosweb python -m flask close_election - -This will close the first election found. If there are multiple elections, you can select which one to close by passing the `--electionid` flag, for example: - - FLASK_APP=eosweb EOSWEB_SETTINGS=$PWD/local_settings.py python -m flask close_election --electionid 01234567-89ab-cdef-ghij-klmnopqrstuv - -Repeat this process, but substitute `count_election` for `close_election` to count the ballots and release the results. - -You should now be able to see the results in the web interface. +Provided that you are logged in using an administrator account (defined using the `ADMINS` option in *local_settings.py*), you will be able to further administer the election from the ‘Administrate this election’ tab in the web interface. diff --git a/eos/base/election.py b/eos/base/election.py index b4c750a..a10672d 100644 --- a/eos/base/election.py +++ b/eos/base/election.py @@ -109,24 +109,24 @@ class Question(EmbeddedObject): class Result(EmbeddedObject): pass -class ApprovalQuestion(Question): +class ListChoiceQuestion(Question): choices = ListField(StringField()) min_choices = IntField() max_choices = IntField() def pretty_answer(self, answer): + if len(answer.choices) == 0: + return '(blank votes)' return ', '.join([self.choices[choice] for choice in answer.choices]) +class ApprovalQuestion(ListChoiceQuestion): + pass + 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 PreferentialQuestion(ListChoiceQuestion): + pass class PreferentialAnswer(Answer): choices = ListField(IntField()) diff --git a/eos/base/workflow.py b/eos/base/workflow.py index e39a9b4..620fada 100644 --- a/eos/base/workflow.py +++ b/eos/base/workflow.py @@ -117,7 +117,7 @@ class Workflow(EmbeddedObject): # ============== class TaskConfigureElection(WorkflowTask): - label = 'Configure the election and freeze the election' + label = 'Freeze the election' #def on_enter(self): # self.status = WorkflowTask.Status.COMPLETE diff --git a/eos/psr/election.py b/eos/psr/election.py index 8b4504b..42cc16c 100644 --- a/eos/psr/election.py +++ b/eos/psr/election.py @@ -175,9 +175,7 @@ class MixingTrustee(Trustee): return False class InternalMixingTrustee(MixingTrustee): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.mixnets = [] + mixnets = EmbeddedObjectListField(is_protected=True) def mix_votes(self, question=0): __pragma__('skip') @@ -187,7 +185,7 @@ class InternalMixingTrustee(MixingTrustee): election = self.recurse_parents('eos.base.election.Election') index = self._instance[1] - self.mixnets.append(RPCMixnet(index)) + self.mixnets.append(RPCMixnet(mix_order=index)) if index > 0: orig_answers = election.mixing_trustees[index - 1].mixed_questions[question] else: diff --git a/eos/psr/mixnet.py b/eos/psr/mixnet.py index 0057902..169c961 100644 --- a/eos/psr/mixnet.py +++ b/eos/psr/mixnet.py @@ -19,13 +19,19 @@ from eos.core.objects import * from eos.core.hashing import * from eos.psr.election import * -class RPCMixnet: - def __init__(self, mix_order): - self.mix_order = mix_order - - self.is_left = (self.mix_order % 2 == 0) - - self.params = [] +class RPCMixnetParam(EmbeddedObject): + permutation = IntField() + reencryption = EmbeddedObjectListField(BigInt) + rand_a = EmbeddedObjectField(BigInt) + rand_b = EmbeddedObjectField(BigInt) + +class RPCMixnet(EmbeddedObject): + mix_order = IntField() + params = EmbeddedObjectListField(RPCMixnetParam) + + @property + def is_left(self): + return (self.mix_order % 2 == 0) def random_permutation(self, n): permutation = list(range(n)) @@ -57,21 +63,21 @@ class RPCMixnet: # And shuffle it to the new position shuffled_answers[permutations[i]] = BlockEncryptedAnswer(blocks=shuffled_blocks) # Record the parameters - permutations_and_reenc.append([permutations[i], block_reencryptions, block.public_key.group.random_Zq_element(), block.public_key.group.random_Zq_element()]) + permutations_and_reenc.append(RPCMixnetParam(permutation=permutations[i], reencryption=block_reencryptions, rand_a=block.public_key.group.random_Zq_element(), rand_b=block.public_key.group.random_Zq_element())) commitments = [] if self.is_left: for i in range(len(permutations_and_reenc)): val = permutations_and_reenc[i] - val_obj = MixChallengeResponse(challenge_index=i, response_index=val[0], reenc=val[1], rand=val[2]) + val_obj = MixChallengeResponse(challenge_index=i, response_index=val.permutation, reenc=val.reencryption, rand=val.rand_a) commitments.append(SHA256().update_obj(val_obj).hash_as_bigint()) else: for i in range(len(permutations_and_reenc)): # Find the answer that went to 'i' - idx = next(idx for idx in range(len(permutations_and_reenc)) if permutations_and_reenc[idx][0] == i) + idx = next(idx for idx in range(len(permutations_and_reenc)) if permutations_and_reenc[idx].permutation == i) val = permutations_and_reenc[idx] - val_obj = MixChallengeResponse(challenge_index=i, response_index=idx, reenc=val[1], rand=val[3]) + val_obj = MixChallengeResponse(challenge_index=i, response_index=idx, reenc=val.reencryption, rand=val.rand_b) commitments.append(SHA256().update_obj(val_obj).hash_as_bigint()) self.params = permutations_and_reenc @@ -80,8 +86,8 @@ class RPCMixnet: def challenge(self, i): if self.is_left: val = self.params[i] - return MixChallengeResponse(challenge_index=i, response_index=val[0], reenc=val[1], rand=val[2]) + return MixChallengeResponse(challenge_index=i, response_index=val.permutation, reenc=val.reencryption, rand=val.rand_a) else: - idx = next(idx for idx in range(len(self.params)) if self.params[idx][0] == i) + idx = next(idx for idx in range(len(self.params)) if self.params[idx].permutation == i) val = self.params[idx] - return MixChallengeResponse(challenge_index=i, response_index=idx, reenc=val[1], rand=val[3]) + return MixChallengeResponse(challenge_index=i, response_index=idx, reenc=val.reencryption, rand=val.rand_b) diff --git a/eosweb/core/main.py b/eosweb/core/main.py index 7e4eb51..2703ed1 100644 --- a/eosweb/core/main.py +++ b/eosweb/core/main.py @@ -131,37 +131,6 @@ def setup_test_election(): election.save() -@app.cli.command('close_election') -@click.option('--electionid', default=None) -def close_election(electionid): - if electionid is None: - election = Election.get_all()[0] - else: - election = Election.get_by_id(electionid) - - election.workflow.get_task('eos.base.workflow.TaskCloseVoting').enter() - - election.save() - -@app.cli.command('count_election') -@click.option('--electionid', default=None) -def count_election(electionid): - if electionid is None: - election = Election.get_all()[0] - else: - election = Election.get_by_id(electionid) - - # Mix votes - election.workflow.get_task('eos.psr.workflow.TaskMixVotes').enter() - # Prove mixes - election.workflow.get_task('eos.psr.workflow.TaskProveMixes').enter() - # Decrypt votes, for realsies - election.workflow.get_task('eos.psr.workflow.TaskDecryptVotes').enter() - # Release result - election.workflow.get_task('eos.base.workflow.TaskReleaseResults').enter() - - election.save() - @app.cli.command('verify_election') @click.option('--electionid', default=None) def verify_election(electionid): @@ -185,16 +154,16 @@ def index(): def using_election(func): @functools.wraps(func) - def wrapped(election_id): + def wrapped(election_id, **kwargs): election = Election.get_by_id(election_id) - return func(election) + return func(election, **kwargs) return wrapped def election_admin(func): @functools.wraps(func) - def wrapped(election): + def wrapped(election, **kwargs): if 'user' in flask.session and flask.session['user'].is_admin(): - return func(election) + return func(election, **kwargs) else: return flask.Response('Administrator credentials required', 403) return wrapped @@ -238,6 +207,19 @@ def election_view_trustees(election): def election_admin_summary(election): return flask.render_template('election/admin/admin.html', election=election) +@app.route('/election//admin/enter_task') +@using_election +@election_admin +def election_admin_enter_task(election): + task = election.workflow.get_task(flask.request.args['task_name']) + if task.status != WorkflowTask.Status.READY: + return flask.Response('Task is not yet ready or has already exited', 409) + + task.enter() + election.save() + + return flask.redirect(flask.url_for('election_admin_summary', election_id=election._id)) + @app.route('/election//cast_ballot', methods=['POST']) @using_election def election_api_cast_vote(election): diff --git a/eosweb/core/templates/election/admin/admin.html b/eosweb/core/templates/election/admin/admin.html index 7fe9b31..30031cc 100644 --- a/eosweb/core/templates/election/admin/admin.html +++ b/eosweb/core/templates/election/admin/admin.html @@ -24,8 +24,14 @@ + + {% endblock %} diff --git a/eosweb/core/templates/election/base.html b/eosweb/core/templates/election/base.html index c5027b2..3abf7fc 100644 --- a/eosweb/core/templates/election/base.html +++ b/eosweb/core/templates/election/base.html @@ -32,7 +32,7 @@
diff --git a/eosweb/core/templates/election/view/view.html b/eosweb/core/templates/election/view/view.html index 2d18619..f564245 100644 --- a/eosweb/core/templates/election/view/view.html +++ b/eosweb/core/templates/election/view/view.html @@ -55,10 +55,16 @@

The administrator of this {{ election.kind }} has not yet finished setting the election parameters. The details of the {{ election.kind }} may change at any time.

{% endif %} - {% if election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status == Status.EXITED %} + {% if (session.user and session.user.is_admin()) or election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status == Status.EXITED %}

Results

-

Results were released at {{ election.workflow.get_task('eos.base.workflow.TaskReleaseResults').exited_at.strftime('%Y-%m-%d %H:%M:%S') }} UTC.

+ {% if election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status == Status.EXITED %} +

Results were released at {{ election.workflow.get_task('eos.base.workflow.TaskReleaseResults').exited_at.strftime('%Y-%m-%d %H:%M:%S') }} UTC.

+ {% else %} +
+ This is a preview of the election results, shown only to you, the election administrator. To publicly release the results, you must do so from the ‘Administrate this election’ tab. +
+ {% endif %} {% for question in election.questions %}

{{ loop.index }}. {{ question.prompt }}