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.
|
||||
|
||||
## 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.
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
class RPCMixnetParam(EmbeddedObject):
|
||||
permutation = IntField()
|
||||
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):
|
||||
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)
|
||||
|
@ -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/<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'])
|
||||
@using_election
|
||||
def election_api_cast_vote(election):
|
||||
|
@ -24,8 +24,14 @@
|
||||
<ul>
|
||||
{% for task in election.workflow.tasks %}
|
||||
{% 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 %}
|
||||
{% endfor %}
|
||||
</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 %}
|
||||
|
@ -32,7 +32,7 @@
|
||||
<div class="ui secondary pointing menu" id="election-tab-menu">
|
||||
{% include eosweb.core.main.model_view_map[election.__class__]['tabs'] %}
|
||||
{% 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 %}
|
||||
</div>
|
||||
<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>
|
||||
{% 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>
|
||||
|
||||
{% 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>
|
||||
{% 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 %}
|
||||
<h3>{{ loop.index }}. {{ question.prompt }}</h2>
|
||||
|
Loading…
Reference in New Issue
Block a user