🎉 Up to counting elections
This commit is contained in:
parent
81807ca7f8
commit
6af132bb5f
@ -89,10 +89,10 @@ class ListField(Field):
|
|||||||
self.element_field = element_field
|
self.element_field = element_field
|
||||||
|
|
||||||
def serialise(self, value, for_hash=False, should_protect=False):
|
def serialise(self, value, for_hash=False, should_protect=False):
|
||||||
return [self.element_field.serialise(x, for_hash, should_protect) for x in value]
|
return [self.element_field.serialise(x, for_hash, should_protect) for x in (value.impl if isinstance(value, EosList) else value)]
|
||||||
|
|
||||||
def deserialise(self, value):
|
def deserialise(self, value):
|
||||||
return [self.element_field.deserialise(x) for x in value]
|
return EosList([self.element_field.deserialise(x) for x in value])
|
||||||
|
|
||||||
class EmbeddedObjectListField(Field):
|
class EmbeddedObjectListField(Field):
|
||||||
def __init__(self, object_type=None, *args, **kwargs):
|
def __init__(self, object_type=None, *args, **kwargs):
|
||||||
@ -100,9 +100,8 @@ class EmbeddedObjectListField(Field):
|
|||||||
self.object_type = object_type
|
self.object_type = object_type
|
||||||
|
|
||||||
def serialise(self, value, for_hash=False, should_protect=False):
|
def serialise(self, value, for_hash=False, should_protect=False):
|
||||||
#return [EosObject.serialise_and_wrap(x, self.object_type, for_hash, should_protect) for x in value]
|
# TNYI: Doesn't know how to deal with iterators like EosList
|
||||||
# TNYI: Doesn't know how to deal with iterators like this
|
return [EosObject.serialise_and_wrap(x, self.object_type, for_hash, should_protect) for x in (value.impl if isinstance(value, EosList) else value)]
|
||||||
return [EosObject.serialise_and_wrap(x, self.object_type, for_hash, should_protect) for x in value.impl]
|
|
||||||
|
|
||||||
def deserialise(self, value):
|
def deserialise(self, value):
|
||||||
return EosList([EosObject.deserialise_and_unwrap(x, self.object_type) for x in value])
|
return EosList([EosObject.deserialise_and_unwrap(x, self.object_type) for x in value])
|
||||||
@ -192,10 +191,15 @@ class EosList(EosObject):
|
|||||||
def post_init(self):
|
def post_init(self):
|
||||||
for i in range(len(self.impl)):
|
for i in range(len(self.impl)):
|
||||||
val = self.impl[i]
|
val = self.impl[i]
|
||||||
|
# Check if object has writeable attributes
|
||||||
|
if is_python and hasattr(val, '__dict__'):
|
||||||
val._instance = (self, i)
|
val._instance = (self, i)
|
||||||
if not val._inited:
|
if not val._inited:
|
||||||
val.post_init()
|
val.post_init()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<EosList {}>'.format(repr(self.impl))
|
||||||
|
|
||||||
# Lists in JS are implemented as native Arrays, so no cheating here :(
|
# Lists in JS are implemented as native Arrays, so no cheating here :(
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self.impl)
|
return len(self.impl)
|
||||||
@ -209,6 +213,16 @@ class EosList(EosObject):
|
|||||||
def __contains__(self, val):
|
def __contains__(self, val):
|
||||||
return val in self.impl
|
return val in self.impl
|
||||||
|
|
||||||
|
# For sorting, etc.
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, EosList):
|
||||||
|
other = other.impl
|
||||||
|
return self.impl == other
|
||||||
|
def __lt__(self, other):
|
||||||
|
if isinstance(other, EosList):
|
||||||
|
other = other.impl
|
||||||
|
return self.impl < other
|
||||||
|
|
||||||
def append(self, value):
|
def append(self, value):
|
||||||
if isinstance(value, EosObject):
|
if isinstance(value, EosObject):
|
||||||
value._instance = (self, len(self))
|
value._instance = (self, len(self))
|
||||||
|
@ -21,6 +21,8 @@ from eos.base.election import *
|
|||||||
from eos.psr.bitstream import *
|
from eos.psr.bitstream import *
|
||||||
from eos.psr.crypto import *
|
from eos.psr.crypto import *
|
||||||
|
|
||||||
|
from eos.core.objects import __pragma__
|
||||||
|
|
||||||
class BlockEncryptedAnswer(EncryptedAnswer):
|
class BlockEncryptedAnswer(EncryptedAnswer):
|
||||||
blocks = EmbeddedObjectListField()
|
blocks = EmbeddedObjectListField()
|
||||||
|
|
||||||
@ -55,14 +57,10 @@ class MixChallengeResponse(EmbeddedObject):
|
|||||||
rand = EmbeddedObjectField(BigInt)
|
rand = EmbeddedObjectField(BigInt)
|
||||||
|
|
||||||
class MixingTrustee(Trustee):
|
class MixingTrustee(Trustee):
|
||||||
mixed_questions = ListField(EmbeddedObjectListField(BlockEncryptedAnswer))
|
mixed_questions = ListField(EmbeddedObjectListField(BlockEncryptedAnswer), is_hashed=False)
|
||||||
commitments = ListField(EmbeddedObjectListField(BigInt))
|
commitments = ListField(EmbeddedObjectListField(BigInt), is_hashed=False)
|
||||||
challenge = EmbeddedObjectListField(BigInt)
|
challenge = EmbeddedObjectListField(BigInt, is_hashed=False)
|
||||||
response = ListField(EmbeddedObjectListField(MixChallengeResponse))
|
response = ListField(EmbeddedObjectListField(MixChallengeResponse), is_hashed=False)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self.mixnets = [] # TODO: Remove this stuff
|
|
||||||
|
|
||||||
def compute_challenge(self, question_num):
|
def compute_challenge(self, question_num):
|
||||||
if self._instance[1] % 2 == 1:
|
if self._instance[1] % 2 == 1:
|
||||||
@ -102,12 +100,14 @@ class MixingTrustee(Trustee):
|
|||||||
challenge_bs = InfiniteHashBitStream(challenge)
|
challenge_bs = InfiniteHashBitStream(challenge)
|
||||||
|
|
||||||
# Check each challenge response
|
# Check each challenge response
|
||||||
responses_iter = iter(self.response[question_num])
|
|
||||||
for k in range(len(self.mixed_questions[question_num])):
|
for k in range(len(self.mixed_questions[question_num])):
|
||||||
|
response = self.response[question_num][k]
|
||||||
|
|
||||||
challenge_bit = challenge_bs.read(1)
|
challenge_bit = challenge_bs.read(1)
|
||||||
should_reveal = ((self._instance[1] % 2) == (challenge_bit % 2))
|
should_reveal = ((self._instance[1] % 2) == (challenge_bit % 2))
|
||||||
if should_reveal:
|
if should_reveal:
|
||||||
response = next(responses_iter)
|
if response is None:
|
||||||
|
raise Exception('Response inconsistent with challenge')
|
||||||
|
|
||||||
# Check the commitment matches
|
# Check the commitment matches
|
||||||
if self.commitments[question_num][k] != SHA256().update_obj(response).hash_as_bigint():
|
if self.commitments[question_num][k] != SHA256().update_obj(response).hash_as_bigint():
|
||||||
@ -132,11 +132,17 @@ class MixingTrustee(Trustee):
|
|||||||
raise Exception('Reencryption not consistent with challenge response')
|
raise Exception('Reencryption not consistent with challenge response')
|
||||||
if claimed_blocks[k].delta != reencrypted_block.delta:
|
if claimed_blocks[k].delta != reencrypted_block.delta:
|
||||||
raise Exception('Reencryption not consistent with challenge response')
|
raise Exception('Reencryption not consistent with challenge response')
|
||||||
|
else:
|
||||||
|
if response is not None:
|
||||||
|
raise Exception('Response inconsistent with challenge')
|
||||||
|
|
||||||
# Check the responses are consistent with a permutation
|
# Check the responses are consistent with a permutation
|
||||||
challenge_indexes = []
|
challenge_indexes = []
|
||||||
response_indexes = []
|
response_indexes = []
|
||||||
for response in self.response[question_num]:
|
for response in self.response[question_num]:
|
||||||
|
if response is None:
|
||||||
|
continue
|
||||||
|
|
||||||
if response.challenge_index in challenge_indexes:
|
if response.challenge_index in challenge_indexes:
|
||||||
raise Exception('Response not consistent with a permutation')
|
raise Exception('Response not consistent with a permutation')
|
||||||
if response.response_index in response_indexes:
|
if response.response_index in response_indexes:
|
||||||
@ -153,10 +159,63 @@ class MixingTrustee(Trustee):
|
|||||||
raise Exception('Duplicate ciphertexts in output')
|
raise Exception('Duplicate ciphertexts in output')
|
||||||
blocks.append(block)
|
blocks.append(block)
|
||||||
|
|
||||||
|
def mix_votes(self, question=0):
|
||||||
|
return False
|
||||||
|
def prove_mixes(self, question=0):
|
||||||
|
return False
|
||||||
|
|
||||||
|
class InternalMixingTrustee(MixingTrustee):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.mixnets = []
|
||||||
|
|
||||||
|
def mix_votes(self, question=0):
|
||||||
|
__pragma__('skip')
|
||||||
|
from eos.psr.mixnet import RPCMixnet
|
||||||
|
__pragma__('noskip')
|
||||||
|
|
||||||
|
election = self.recurse_parents('eos.base.election.Election')
|
||||||
|
index = self._instance[1]
|
||||||
|
|
||||||
|
self.mixnets.append(RPCMixnet(index))
|
||||||
|
if index > 0:
|
||||||
|
orig_answers = election.mixing_trustees[index - 1].mixed_questions[question]
|
||||||
|
else:
|
||||||
|
orig_answers = []
|
||||||
|
for voter in election.voters:
|
||||||
|
if len(voter.votes) > 0:
|
||||||
|
ballot = voter.votes[-1].ballot
|
||||||
|
orig_answers.append(ballot.encrypted_answers[question])
|
||||||
|
shuffled_answers, commitments = self.mixnets[question].shuffle(orig_answers)
|
||||||
|
self.mixed_questions.append(EosList(shuffled_answers))
|
||||||
|
self.commitments.append(EosList(commitments))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def prove_mixes(self, question=0):
|
||||||
|
election = self.recurse_parents('eos.base.election.Election')
|
||||||
|
index = self._instance[1]
|
||||||
|
|
||||||
|
self.challenge.append(self.compute_challenge(question))
|
||||||
|
challenge_bs = InfiniteHashBitStream(self.challenge[question])
|
||||||
|
|
||||||
|
self.response.append(EosList())
|
||||||
|
|
||||||
|
for k in range(len(self.mixed_questions[question])):
|
||||||
|
challenge_bit = challenge_bs.read(1)
|
||||||
|
should_reveal = ((index % 2) == (challenge_bit % 2))
|
||||||
|
if should_reveal:
|
||||||
|
response = self.mixnets[question].challenge(k)
|
||||||
|
self.response[question].append(response)
|
||||||
|
else:
|
||||||
|
self.response[question].append(None)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
class PSRElection(Election):
|
class PSRElection(Election):
|
||||||
_db_name = Election._name
|
_db_name = Election._name
|
||||||
|
|
||||||
sk = EmbeddedObjectField(SEGPrivateKey, is_protected=True) # TODO: Threshold
|
sk = EmbeddedObjectField(SEGPrivateKey, is_protected=True) # TODO: Threshold
|
||||||
|
|
||||||
public_key = EmbeddedObjectField(SEGPublicKey)
|
public_key = EmbeddedObjectField(SEGPublicKey)
|
||||||
mixing_trustees = EmbeddedObjectListField(MixingTrustee)
|
mixing_trustees = EmbeddedObjectListField()
|
||||||
|
@ -235,7 +235,7 @@ class ElectionTestCase(EosTestCase):
|
|||||||
election.voters.append(voter)
|
election.voters.append(voter)
|
||||||
|
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
mixing_trustee = MixingTrustee()
|
mixing_trustee = InternalMixingTrustee()
|
||||||
election.mixing_trustees.append(mixing_trustee)
|
election.mixing_trustees.append(mixing_trustee)
|
||||||
|
|
||||||
election.sk = EGPrivateKey.generate()
|
election.sk = EGPrivateKey.generate()
|
||||||
@ -275,60 +275,24 @@ class ElectionTestCase(EosTestCase):
|
|||||||
election.save()
|
election.save()
|
||||||
|
|
||||||
# Mix votes
|
# Mix votes
|
||||||
election.workflow.get_task('eos.psr.workflow.TaskMixVotes').enter()
|
self.do_task_assert(election, 'eos.psr.workflow.TaskMixVotes', 'eos.psr.workflow.TaskProveMixes')
|
||||||
election.save()
|
|
||||||
|
|
||||||
# Do the mix
|
|
||||||
for i in range(len(election.questions)):
|
|
||||||
for j in range(len(election.mixing_trustees)):
|
|
||||||
# Wouldn't happen server-side IRL
|
|
||||||
election.mixing_trustees[j].mixnets.append(RPCMixnet(j))
|
|
||||||
if j > 0:
|
|
||||||
orig_answers = election.mixing_trustees[j - 1].mixed_questions[i]
|
|
||||||
else:
|
|
||||||
orig_answers = []
|
|
||||||
for voter in election.voters:
|
|
||||||
ballot = voter.votes[-1].ballot
|
|
||||||
orig_answers.append(ballot.encrypted_answers[i])
|
|
||||||
shuffled_answers, commitments = election.mixing_trustees[j].mixnets[i].shuffle(orig_answers)
|
|
||||||
election.mixing_trustees[j].mixed_questions.append(EosList(shuffled_answers))
|
|
||||||
election.mixing_trustees[j].commitments.append(EosList(commitments))
|
|
||||||
|
|
||||||
election.workflow.get_task('eos.psr.workflow.TaskMixVotes').exit()
|
|
||||||
election.save()
|
election.save()
|
||||||
|
|
||||||
# Prove mixes
|
# Prove mixes
|
||||||
election.workflow.get_task('eos.psr.workflow.TaskProveMixes').enter()
|
self.do_task_assert(election, 'eos.psr.workflow.TaskProveMixes', 'eos.base.workflow.TaskDecryptVotes')
|
||||||
election.save()
|
election.save()
|
||||||
|
|
||||||
# Record challenge responses
|
# Verify mixes
|
||||||
for i in range(len(election.questions)):
|
for i in range(len(election.questions)):
|
||||||
for j in range(len(election.mixing_trustees)):
|
for j in range(len(election.mixing_trustees)):
|
||||||
trustee = election.mixing_trustees[j]
|
election.mixing_trustees[j].verify(i)
|
||||||
trustee.challenge.append(trustee.compute_challenge(i))
|
|
||||||
challenge_bs = InfiniteHashBitStream(trustee.challenge[i])
|
|
||||||
|
|
||||||
trustee.response.append(EosList())
|
|
||||||
|
|
||||||
for k in range(len(trustee.mixed_questions[i])):
|
|
||||||
challenge_bit = challenge_bs.read(1)
|
|
||||||
should_reveal = ((j % 2) == (challenge_bit % 2))
|
|
||||||
if should_reveal:
|
|
||||||
response = trustee.mixnets[i].challenge(k)
|
|
||||||
trustee.response[i].append(response)
|
|
||||||
|
|
||||||
# Verify challenge response
|
|
||||||
trustee.verify(i)
|
|
||||||
|
|
||||||
election.workflow.get_task('eos.psr.workflow.TaskProveMixes').exit()
|
|
||||||
election.save()
|
|
||||||
|
|
||||||
# Decrypt votes, for realsies
|
# Decrypt votes, for realsies
|
||||||
self.do_task_assert(election, 'eos.base.workflow.TaskDecryptVotes', 'eos.base.workflow.TaskReleaseResults')
|
self.do_task_assert(election, 'eos.base.workflow.TaskDecryptVotes', 'eos.base.workflow.TaskReleaseResults')
|
||||||
election.save()
|
election.save()
|
||||||
|
|
||||||
# Check result
|
# Check result
|
||||||
RESULTS = [[voter[i] for voter in VOTES] for i in range(len(election.questions))]
|
RESULTS = [[EosList(voter[i]) for voter in VOTES] for i in range(len(election.questions))]
|
||||||
for i in range(len(RESULTS)):
|
for i in range(len(RESULTS)):
|
||||||
votes1 = RESULTS[i]
|
votes1 = RESULTS[i]
|
||||||
votes2 = [x.choices for x in election.results[i].answers]
|
votes2 = [x.choices for x in election.results[i].answers]
|
||||||
|
@ -25,15 +25,37 @@ class TaskMixVotes(WorkflowTask):
|
|||||||
depends_on = ['eos.base.workflow.TaskCloseVoting']
|
depends_on = ['eos.base.workflow.TaskCloseVoting']
|
||||||
|
|
||||||
def on_enter(self):
|
def on_enter(self):
|
||||||
# Do not automatically exit this task
|
election = self.recurse_parents('eos.base.election.Election')
|
||||||
pass
|
|
||||||
|
should_exit = True
|
||||||
|
|
||||||
|
for i in range(len(election.questions)):
|
||||||
|
for j in range(len(election.mixing_trustees)):
|
||||||
|
success = election.mixing_trustees[j].mix_votes(i)
|
||||||
|
if not success:
|
||||||
|
should_exit = False
|
||||||
|
break # out of inner loop - further mixing required by hand for this question
|
||||||
|
|
||||||
|
if should_exit:
|
||||||
|
self.exit()
|
||||||
|
|
||||||
class TaskProveMixes(WorkflowTask):
|
class TaskProveMixes(WorkflowTask):
|
||||||
depends_on = ['eos.psr.workflow.TaskMixVotes']
|
depends_on = ['eos.psr.workflow.TaskMixVotes']
|
||||||
|
|
||||||
def on_enter(self):
|
def on_enter(self):
|
||||||
# Do not automatically exit this task
|
election = self.recurse_parents('eos.base.election.Election')
|
||||||
pass
|
|
||||||
|
should_exit = True
|
||||||
|
|
||||||
|
for i in range(len(election.questions)):
|
||||||
|
for j in range(len(election.mixing_trustees)):
|
||||||
|
success = election.mixing_trustees[j].prove_mixes(i)
|
||||||
|
if not success:
|
||||||
|
should_exit = False
|
||||||
|
break # out of inner loop - further mixing required by hand for this question
|
||||||
|
|
||||||
|
if should_exit:
|
||||||
|
self.exit()
|
||||||
|
|
||||||
class TaskDecryptVotes(eos.base.workflow.TaskDecryptVotes):
|
class TaskDecryptVotes(eos.base.workflow.TaskDecryptVotes):
|
||||||
depends_on = ['eos.psr.workflow.TaskProveMixes']
|
depends_on = ['eos.psr.workflow.TaskProveMixes']
|
||||||
|
@ -58,7 +58,8 @@ def setup_test_election():
|
|||||||
election.voters.append(Voter(name='Bob'))
|
election.voters.append(Voter(name='Bob'))
|
||||||
election.voters.append(Voter(name='Charlie'))
|
election.voters.append(Voter(name='Charlie'))
|
||||||
|
|
||||||
election.mixing_trustees.append(MixingTrustee(name='Eos Voting'))
|
election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting'))
|
||||||
|
election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting'))
|
||||||
|
|
||||||
election.sk = EGPrivateKey.generate()
|
election.sk = EGPrivateKey.generate()
|
||||||
election.public_key = election.sk.public_key
|
election.public_key = election.sk.public_key
|
||||||
@ -79,6 +80,28 @@ def setup_test_election():
|
|||||||
|
|
||||||
election.save()
|
election.save()
|
||||||
|
|
||||||
|
@app.cli.command('close_test_election')
|
||||||
|
def close_test_election():
|
||||||
|
election = Election.get_all()[0]
|
||||||
|
election.workflow.get_task('eos.base.workflow.TaskCloseVoting').enter()
|
||||||
|
|
||||||
|
election.save()
|
||||||
|
|
||||||
|
@app.cli.command('count_test_election')
|
||||||
|
def count_test_election():
|
||||||
|
election = Election.get_all()[0]
|
||||||
|
|
||||||
|
# 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.context_processor
|
@app.context_processor
|
||||||
def inject_globals():
|
def inject_globals():
|
||||||
return {'eos': eos, 'eosweb': eosweb, 'SHA256': eos.core.hashing.SHA256}
|
return {'eos': eos, 'eosweb': eosweb, 'SHA256': eos.core.hashing.SHA256}
|
||||||
@ -129,6 +152,10 @@ def election_view_trustees(election):
|
|||||||
@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):
|
||||||
|
if election.workflow.get_task('eos.base.workflow.TaskOpenVoting').status >= WorkflowTask.Status.EXITED or election.workflow.get_task('eos.base.workflow.TaskCloseVoting').status <= WorkflowTask.Status.READY:
|
||||||
|
# Voting is not yet open or has closed
|
||||||
|
return flask.Response('Voting is not yet open or has closed', 405)
|
||||||
|
|
||||||
data = json.loads(flask.request.data)
|
data = json.loads(flask.request.data)
|
||||||
|
|
||||||
voter = None
|
voter = None
|
||||||
|
@ -19,16 +19,15 @@ from eos.psr.election import *
|
|||||||
|
|
||||||
model_view_map = {
|
model_view_map = {
|
||||||
ApprovalQuestion: {
|
ApprovalQuestion: {
|
||||||
'view': 'question/approval/view.html'
|
'view': 'question/approval/view.html',
|
||||||
|
'result_raw': 'question/approval/result_raw.html',
|
||||||
|
'selections_make': 'question/approval/selections_make.html',
|
||||||
|
'selections_review': 'question/approval/selections_review.html'
|
||||||
},
|
},
|
||||||
Election: {
|
Election: {
|
||||||
'tabs': 'election/core/tabs.html'
|
'tabs': 'election/core/tabs.html'
|
||||||
},
|
},
|
||||||
PSRElection: {
|
PSRElection: {
|
||||||
'tabs': 'election/psr/tabs.html'
|
'tabs': 'election/psr/tabs.html'
|
||||||
},
|
|
||||||
ApprovalQuestion: {
|
|
||||||
'selections_make': 'question/approval/selections_make.html',
|
|
||||||
'selections_review': 'question/approval/selections_review.html'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,9 @@
|
|||||||
{% if election.workflow.get_task('eos.base.workflow.TaskConfigureElection').status == Status.EXITED %}
|
{% if election.workflow.get_task('eos.base.workflow.TaskConfigureElection').status == Status.EXITED %}
|
||||||
{% if election.workflow.get_task('eos.base.workflow.TaskOpenVoting').status == Status.EXITED %}
|
{% if election.workflow.get_task('eos.base.workflow.TaskOpenVoting').status == Status.EXITED %}
|
||||||
{% if election.workflow.get_task('eos.base.workflow.TaskCloseVoting').status == Status.EXITED %}
|
{% if election.workflow.get_task('eos.base.workflow.TaskCloseVoting').status == Status.EXITED %}
|
||||||
|
{% if election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status < Status.EXITED %}
|
||||||
<p><button class="ui huge button">Voting in this election has closed</button></p>
|
<p><button class="ui huge button">Voting in this election has closed</button></p>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p><a href="{{ url_for('election_booth', election_id=election._id) }}" class="ui huge primary button">Click here to vote in this election</a></p>
|
<p><a href="{{ url_for('election_booth', election_id=election._id) }}" class="ui huge primary button">Click here to vote in this election</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -52,4 +54,13 @@
|
|||||||
|
|
||||||
<p>The administrator of this election has not yet finished setting the election parameters. The details of the election may change at any time.</p>
|
<p>The administrator of this election has not yet finished setting the election parameters. The details of the election may change at any time.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status == Status.EXITED %}
|
||||||
|
<h2>Results</h2>
|
||||||
|
|
||||||
|
{% for question in election.questions %}
|
||||||
|
<h3>{{ loop.index }}. {{ question.prompt }}</h2>
|
||||||
|
{% include eosweb.core.main.model_view_map[question.__class__]['result_raw'] %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
27
eosweb/core/templates/question/approval/result_raw.html
Normal file
27
eosweb/core/templates/question/approval/result_raw.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{#
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
#}
|
||||||
|
|
||||||
|
<ul class="ui list">
|
||||||
|
{% for answer in election.results[loop.index0].answers %}
|
||||||
|
<li>
|
||||||
|
{% for choice in answer.choices %}
|
||||||
|
{{ question.choices[choice] }},
|
||||||
|
{% endfor %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
Loading…
Reference in New Issue
Block a user