Basic functionality of the admin pane
This commit is contained in:
parent
95e6a56f81
commit
be4127639b
18
HOWTO.md
18
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.
|
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:
|
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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
@ -109,24 +109,24 @@ class Question(EmbeddedObject):
|
|||||||
class Result(EmbeddedObject):
|
class Result(EmbeddedObject):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class ApprovalQuestion(Question):
|
class ListChoiceQuestion(Question):
|
||||||
choices = ListField(StringField())
|
choices = ListField(StringField())
|
||||||
min_choices = IntField()
|
min_choices = IntField()
|
||||||
max_choices = IntField()
|
max_choices = IntField()
|
||||||
|
|
||||||
def pretty_answer(self, answer):
|
def pretty_answer(self, answer):
|
||||||
|
if len(answer.choices) == 0:
|
||||||
|
return '(blank votes)'
|
||||||
return ', '.join([self.choices[choice] for choice in answer.choices])
|
return ', '.join([self.choices[choice] for choice in answer.choices])
|
||||||
|
|
||||||
|
class ApprovalQuestion(ListChoiceQuestion):
|
||||||
|
pass
|
||||||
|
|
||||||
class ApprovalAnswer(Answer):
|
class ApprovalAnswer(Answer):
|
||||||
choices = ListField(IntField())
|
choices = ListField(IntField())
|
||||||
|
|
||||||
class PreferentialQuestion(Question):
|
class PreferentialQuestion(ListChoiceQuestion):
|
||||||
choices = ListField(StringField())
|
pass
|
||||||
min_choices = IntField()
|
|
||||||
max_choices = IntField()
|
|
||||||
|
|
||||||
def pretty_answer(self, answer):
|
|
||||||
return ', '.join([self.choices[choice] for choice in answer.choices])
|
|
||||||
|
|
||||||
class PreferentialAnswer(Answer):
|
class PreferentialAnswer(Answer):
|
||||||
choices = ListField(IntField())
|
choices = ListField(IntField())
|
||||||
|
@ -117,7 +117,7 @@ class Workflow(EmbeddedObject):
|
|||||||
# ==============
|
# ==============
|
||||||
|
|
||||||
class TaskConfigureElection(WorkflowTask):
|
class TaskConfigureElection(WorkflowTask):
|
||||||
label = 'Configure the election and freeze the election'
|
label = 'Freeze the election'
|
||||||
|
|
||||||
#def on_enter(self):
|
#def on_enter(self):
|
||||||
# self.status = WorkflowTask.Status.COMPLETE
|
# self.status = WorkflowTask.Status.COMPLETE
|
||||||
|
@ -175,9 +175,7 @@ class MixingTrustee(Trustee):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
class InternalMixingTrustee(MixingTrustee):
|
class InternalMixingTrustee(MixingTrustee):
|
||||||
def __init__(self, **kwargs):
|
mixnets = EmbeddedObjectListField(is_protected=True)
|
||||||
super().__init__(**kwargs)
|
|
||||||
self.mixnets = []
|
|
||||||
|
|
||||||
def mix_votes(self, question=0):
|
def mix_votes(self, question=0):
|
||||||
__pragma__('skip')
|
__pragma__('skip')
|
||||||
@ -187,7 +185,7 @@ class InternalMixingTrustee(MixingTrustee):
|
|||||||
election = self.recurse_parents('eos.base.election.Election')
|
election = self.recurse_parents('eos.base.election.Election')
|
||||||
index = self._instance[1]
|
index = self._instance[1]
|
||||||
|
|
||||||
self.mixnets.append(RPCMixnet(index))
|
self.mixnets.append(RPCMixnet(mix_order=index))
|
||||||
if index > 0:
|
if index > 0:
|
||||||
orig_answers = election.mixing_trustees[index - 1].mixed_questions[question]
|
orig_answers = election.mixing_trustees[index - 1].mixed_questions[question]
|
||||||
else:
|
else:
|
||||||
|
@ -19,13 +19,19 @@ from eos.core.objects import *
|
|||||||
from eos.core.hashing import *
|
from eos.core.hashing import *
|
||||||
from eos.psr.election import *
|
from eos.psr.election import *
|
||||||
|
|
||||||
class RPCMixnet:
|
class RPCMixnetParam(EmbeddedObject):
|
||||||
def __init__(self, mix_order):
|
permutation = IntField()
|
||||||
self.mix_order = mix_order
|
reencryption = EmbeddedObjectListField(BigInt)
|
||||||
|
rand_a = EmbeddedObjectField(BigInt)
|
||||||
|
rand_b = EmbeddedObjectField(BigInt)
|
||||||
|
|
||||||
self.is_left = (self.mix_order % 2 == 0)
|
class RPCMixnet(EmbeddedObject):
|
||||||
|
mix_order = IntField()
|
||||||
|
params = EmbeddedObjectListField(RPCMixnetParam)
|
||||||
|
|
||||||
self.params = []
|
@property
|
||||||
|
def is_left(self):
|
||||||
|
return (self.mix_order % 2 == 0)
|
||||||
|
|
||||||
def random_permutation(self, n):
|
def random_permutation(self, n):
|
||||||
permutation = list(range(n))
|
permutation = list(range(n))
|
||||||
@ -57,21 +63,21 @@ class RPCMixnet:
|
|||||||
# And shuffle it to the new position
|
# And shuffle it to the new position
|
||||||
shuffled_answers[permutations[i]] = BlockEncryptedAnswer(blocks=shuffled_blocks)
|
shuffled_answers[permutations[i]] = BlockEncryptedAnswer(blocks=shuffled_blocks)
|
||||||
# Record the parameters
|
# 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 = []
|
commitments = []
|
||||||
if self.is_left:
|
if self.is_left:
|
||||||
for i in range(len(permutations_and_reenc)):
|
for i in range(len(permutations_and_reenc)):
|
||||||
val = permutations_and_reenc[i]
|
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())
|
commitments.append(SHA256().update_obj(val_obj).hash_as_bigint())
|
||||||
else:
|
else:
|
||||||
for i in range(len(permutations_and_reenc)):
|
for i in range(len(permutations_and_reenc)):
|
||||||
# Find the answer that went to 'i'
|
# 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 = 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())
|
commitments.append(SHA256().update_obj(val_obj).hash_as_bigint())
|
||||||
|
|
||||||
self.params = permutations_and_reenc
|
self.params = permutations_and_reenc
|
||||||
@ -80,8 +86,8 @@ class RPCMixnet:
|
|||||||
def challenge(self, i):
|
def challenge(self, i):
|
||||||
if self.is_left:
|
if self.is_left:
|
||||||
val = self.params[i]
|
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:
|
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]
|
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)
|
||||||
|
@ -131,37 +131,6 @@ def setup_test_election():
|
|||||||
|
|
||||||
election.save()
|
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')
|
@app.cli.command('verify_election')
|
||||||
@click.option('--electionid', default=None)
|
@click.option('--electionid', default=None)
|
||||||
def verify_election(electionid):
|
def verify_election(electionid):
|
||||||
@ -185,16 +154,16 @@ def index():
|
|||||||
|
|
||||||
def using_election(func):
|
def using_election(func):
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def wrapped(election_id):
|
def wrapped(election_id, **kwargs):
|
||||||
election = Election.get_by_id(election_id)
|
election = Election.get_by_id(election_id)
|
||||||
return func(election)
|
return func(election, **kwargs)
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
def election_admin(func):
|
def election_admin(func):
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def wrapped(election):
|
def wrapped(election, **kwargs):
|
||||||
if 'user' in flask.session and flask.session['user'].is_admin():
|
if 'user' in flask.session and flask.session['user'].is_admin():
|
||||||
return func(election)
|
return func(election, **kwargs)
|
||||||
else:
|
else:
|
||||||
return flask.Response('Administrator credentials required', 403)
|
return flask.Response('Administrator credentials required', 403)
|
||||||
return wrapped
|
return wrapped
|
||||||
@ -238,6 +207,19 @@ def election_view_trustees(election):
|
|||||||
def election_admin_summary(election):
|
def election_admin_summary(election):
|
||||||
return flask.render_template('election/admin/admin.html', election=election)
|
return flask.render_template('election/admin/admin.html', election=election)
|
||||||
|
|
||||||
|
@app.route('/election/<election_id>/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/<election_id>/cast_ballot', methods=['POST'])
|
@app.route('/election/<election_id>/cast_ballot', methods=['POST'])
|
||||||
@using_election
|
@using_election
|
||||||
def election_api_cast_vote(election):
|
def election_api_cast_vote(election):
|
||||||
|
@ -24,8 +24,14 @@
|
|||||||
<ul>
|
<ul>
|
||||||
{% for task in election.workflow.tasks %}
|
{% for task in election.workflow.tasks %}
|
||||||
{% if task.status == eos.base.workflow.WorkflowTask.Status.READY %}
|
{% if task.status == eos.base.workflow.WorkflowTask.Status.READY %}
|
||||||
<li>{{ task.label }}</li>
|
<li><a href="{{ url_for('election_admin_enter_task', election_id=election._id, task_name=task._name) }}" onclick="return confirmTask(this);">{{ task.label }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function confirmTask(taskLink) {
|
||||||
|
return window.confirm("Are you sure you want to execute the task \"" + taskLink.innerText + "\"? This action is irreversible.");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
<div class="ui secondary pointing menu" id="election-tab-menu">
|
<div class="ui secondary pointing menu" id="election-tab-menu">
|
||||||
{% include eosweb.core.main.model_view_map[election.__class__]['tabs'] %}
|
{% include eosweb.core.main.model_view_map[election.__class__]['tabs'] %}
|
||||||
{% if session.user and session.user.is_admin() %}
|
{% if session.user and session.user.is_admin() %}
|
||||||
<a href="{{ url_for('election_admin_summary', election_id=election._id) }}" class="election-tab-ajax item{% if request.endpoint == 'election_admin' %} active{% endif %} right"><i class="configure icon"></i> Administrate this election</a>
|
<a href="{{ url_for('election_admin_summary', election_id=election._id) }}" class="election-tab-ajax item{% if request.endpoint == 'election_admin_summary' %} active{% endif %} right"><i class="configure icon"></i> Administrate this election</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="ui container" id="election-tab-content">
|
<div class="ui container" id="election-tab-content">
|
||||||
|
@ -55,10 +55,16 @@
|
|||||||
<p>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.</p>
|
<p>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.</p>
|
||||||
{% endif %}
|
{% 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 %}
|
||||||
<h2>Results</h2>
|
<h2>Results</h2>
|
||||||
|
|
||||||
|
{% if election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status == Status.EXITED %}
|
||||||
<p>Results were released at {{ election.workflow.get_task('eos.base.workflow.TaskReleaseResults').exited_at.strftime('%Y-%m-%d %H:%M:%S') }} UTC.</p>
|
<p>Results were released at {{ election.workflow.get_task('eos.base.workflow.TaskReleaseResults').exited_at.strftime('%Y-%m-%d %H:%M:%S') }} UTC.</p>
|
||||||
|
{% else %}
|
||||||
|
<div class="ui warning message">
|
||||||
|
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 <a href="{{ url_for('election_admin_summary', election_id=election._id) }}">‘Administrate this election’</a> tab.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% for question in election.questions %}
|
{% for question in election.questions %}
|
||||||
<h3>{{ loop.index }}. {{ question.prompt }}</h2>
|
<h3>{{ loop.index }}. {{ question.prompt }}</h2>
|
||||||
|
Loading…
Reference in New Issue
Block a user