Compare commits

..

No commits in common. "master" and "devel-count" have entirely different histories.

84 changed files with 602 additions and 10525 deletions

View File

@ -1,3 +0,0 @@
{
"presets": ["@babel/preset-env"]
}

3
.gitignore vendored
View File

@ -2,10 +2,9 @@
/.python-version /.python-version
/htmlcov /htmlcov
/venv /venv
__target__ __javascript__
__pycache__ __pycache__
refs refs
node_modules
\#* \#*
.#* .#*

View File

@ -12,10 +12,6 @@ Install the Python dependencies. (If doing this in a virtualenv, add the virtual
cd /path/to/Eos cd /path/to/Eos
pip install -r requirements.txt pip install -r requirements.txt
Install the node dependencies to build the JavaScript code.
npm install @babel/core @babel/cli @babel/preset-env babelify browserify
Build the JavaScript code. Build the JavaScript code.
./build_js.sh ./build_js.sh

View File

@ -1,6 +1,6 @@
# Eos: Modular verifiable elections # Eos: Modular verifiable elections
Work in progress – Both API and GUI are sufficiently complete to have seen experimental use Work in progress – An API for elections and cryptography is more or less complete, focus is now on building a functional user interface
## Comparison with competitors ## Comparison with competitors
@ -16,9 +16,18 @@ Eye Candy | No | Yes!
## Cryptographic details and references ## Cryptographic details and references
Eos aims to be implementation-agnostic with respect to cryptographic details, with the included *eos.psr* package providing an example implementation. Eos aims to be implementation-agnostic with respect to cryptographic details. The included *eos.psr* package provides an example implementation with the following particulars:
For details of the implementation, refer to the [*Eos Voting Technical Report*](https://drive.google.com/open?id=1jjM5hkIBSZ8LryI12yPsuWv32Id7VjTC). * ElGamal encryption
* MENEZES, Alfred J., Paul C. VAN OORSCHOT and Scott A. VANSTONE. *Handbook of Applied Cryptography*. CRC Press, 2001. Fifth printing. ISBN 978-0-8493-8523-0. Available from: http://cacr.uwaterloo.ca/hac/
* Distributed threshold ElGamal due to **P**edersen (1991)
* PEDERSEN, Torben Pryds. ‘A Threshold Cryptosystem without a Trusted Party’. In: D.W. Davies, ed. *Advances in Cryptology — EUROCRYPT '91*. Berlin: Springer, 1991. pp. 522–526. Lecture Notes in Computer Science, vol. 547. ISBN 978-3-540-46416-7. Available from: https://doi.org/10.1007/3-540-46416-6_47
* **S**igned ElGamal due to Schnorr and Jakobsson (2000)
* SCHNORR, Claus Peter and Markus JAKOBSSON. ‘Security of Signed ElGamal Encryption’. In: T. OKAMOTO, ed. *Advances in Cryptology – ASIACRYPT 2000*. Berlin: Springer-Verlag, 2000. pp. 73–89. Lecture Notes in Computer Science, vol. 1976. ISBN 978-3-540-44448-0. Available from: https://doi.org/10.1007/3-540-44448-3_7
* **R**andomised partial checking (RPC) due to Jakobsson, Juels and Rivest (2002)
* JAKOBSSON, Markus, Ari JUELS and Ronald L. RIVEST. ‘Making Mix Nets Robust For Electronic Voting By Randomized Partial Checking’. In: *Proceedings of the 11th USENIX Security Symposium*. pp. 339–353. Berkeley: USENIX Association, 2002. Available from: https://www.usenix.org/event/sec02/full_papers/jakobsson/jakobsson.pdf
* Taking note of points raised by Khazaei and Wikström (2013)
* KHAZAEI, Shahram and Douglas WIKSTRÖM. ‘Randomized Partial Checking Revisited’. In: E. DAWSON, ed. *Topics in Cryptology – CT-RSA 2013*. Berlin: Springer-Verlag, 2013. pp. 115–128. Lecture Notes in Computer Science, vol. 7779. ISBN 978-3-642-36095-4. Available from: https://doi.org/10.1007/978-3-642-36095-4_8
## Mother of all disclaimers ## Mother of all disclaimers

View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-2019 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -20,14 +20,21 @@ FLAGS="-k -mc -o"
#for f in eos.js eos.js_tests; do #for f in eos.js eos.js_tests; do
for f in eos.js_tests; do for f in eos.js_tests; do
transcrypt -b -n $FLAGS $f.py || exit 1 transcrypt -b -n $FLAGS $f.py || exit 1
# Javascript identifiers cannot contain dots
perl -0777 -pi -e 's/eos.js/eosjs/g' eos/__javascript__/$f.js
# __pragma__ sometimes stops working???
perl -0777 -pi -e "s/__pragma__ \('.*?'\)//gs" eos/__javascript__/$f.js
# Transcrypt by default suppresses stack traces for some reason??
perl -0777 -pi -e 's/__except0__.__cause__ = null;//g' eos/__javascript__/$f.js
# Fix handling of properties, Transcrypt bug #407
perl -0777 -pi -e 's/var __get__ = function \(self, func, quotedFuncName\) \{/var __get__ = function (self, func, quotedFuncName) { if(typeof(func) != "function"){return func;}/g' eos/__javascript__/$f.js
perl -0777 -pi -e 's/property.call \((.*?), \g1.\g1.__impl__(.*?)\)/property.call ($1, $1.__impl__$2)/g' eos/__javascript__/$f.js
perl -0777 -pi -e 's/property.call \((.*?), \g1.\g1.__implpy_(.*?)\)/property.call ($1, $1.__impl__$2)/g' eos/__javascript__/$f.js
done done
# Transcrypt syntax errors cp eos/__javascript__/eos.js_tests.js eosweb/core/static/js/eosjs.js
perl -0777 -pi -e 's/import \{, /import \{/g' __target__/eos*.js perl -0777 -pi -e 's/eosjs_tests/eosjs/g' eosweb/core/static/js/eosjs.js
# Add export
echo >> __target__/eos.js_tests.js
echo 'export {eos, __kwargtrans__};' >> __target__/eos.js_tests.js
# Convert to ES5
./node_modules/.bin/browserify -t babelify -r ./__target__/eos.js_tests.js:eosjs > eosweb/core/static/js/eosjs.js

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-2021 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -30,9 +30,6 @@ class NullEncryptedAnswer(EncryptedAnswer):
def decrypt(self): def decrypt(self):
return None, self.answer return None, self.answer
def deaudit(self):
return self
class Ballot(EmbeddedObject): class Ballot(EmbeddedObject):
#_id = UUIDField() #_id = UUIDField()
encrypted_answers = EmbeddedObjectListField() encrypted_answers = EmbeddedObjectListField()
@ -49,11 +46,8 @@ class Ballot(EmbeddedObject):
return Ballot(encrypted_answers=encrypted_answers_deaudit, election_id=self.election_id, election_hash=self.election_hash) return Ballot(encrypted_answers=encrypted_answers_deaudit, election_id=self.election_id, election_hash=self.election_hash)
class Vote(TopLevelObject): class Vote(EmbeddedObject):
_ver = StringField(default='0.6') _ver = StringField(default='0.5')
_id = UUIDField()
voter_id = UUIDField()
ballot = EmbeddedObjectField() ballot = EmbeddedObjectField()
cast_at = DateTimeField() cast_at = DateTimeField()
@ -63,10 +57,8 @@ class Vote(TopLevelObject):
cast_fingerprint = BlobField(is_protected=True) cast_fingerprint = BlobField(is_protected=True)
class Voter(EmbeddedObject): class Voter(EmbeddedObject):
_ver = StringField(default='0.6')
_id = UUIDField() _id = UUIDField()
votes = RelatedObjectListField(related_type=Vote, object_type=None, this_field='_id', related_field='voter_id') votes = EmbeddedObjectListField()
class User(EmbeddedObject): class User(EmbeddedObject):
admins = [] admins = []
@ -80,9 +72,6 @@ class User(EmbeddedObject):
return True return True
return False return False
def __getstate__(self):
return {k: v for k, v in self.__dict__.items() if k != '_instance'}
def generate_password(): def generate_password():
if is_python: if is_python:
#__pragma__('skip') #__pragma__('skip')
@ -102,6 +91,18 @@ class EmailUser(User):
return False return False
return self.email.lower() == other.email.lower() and self.password == other.password return self.email.lower() == other.email.lower() and self.password == other.password
def send_email(self, host, port, username, password, from_email, content):
#__pragma__('skip')
import smtplib
#__pragma__('noskip')
with smtplib.SMTP(host, port) as smtp:
if username is not None:
smtp.login(username, password)
smtp.sendmail(from_email, [self.email], content)
def email_password(self, host, port, username, password, from_email):
self.send_email(host, port, username, password, from_email, 'Subject: Registered to vote in {1}\nFrom: {4}\nTo: {2}\n\nDear {0},\n\nYou are registered to vote in the election {1}. Your log in details are as follows:\n\nEmail: {2}\nPassword: {3}'.format(self.name, self.recurse_parents(Election).name, self.email, self.password, from_email))
class UserVoter(Voter): class UserVoter(Voter):
user = EmbeddedObjectField() user = EmbeddedObjectField()
@ -110,15 +111,14 @@ class UserVoter(Voter):
return self.user.name return self.user.name
class Question(EmbeddedObject): class Question(EmbeddedObject):
_ver = StringField(default='0.7')
prompt = StringField() prompt = StringField()
description = StringField()
class Result(EmbeddedObject): class Result(EmbeddedObject):
pass pass
class ListChoiceQuestion(Question): class ListChoiceQuestion(Question):
_ver = StringField(default='0.5')
choices = EmbeddedObjectListField() choices = EmbeddedObjectListField()
min_choices = IntField() min_choices = IntField()
max_choices = IntField() max_choices = IntField()
@ -214,8 +214,6 @@ class STVResult(Result):
random = BlobField() random = BlobField()
class Election(TopLevelObject): class Election(TopLevelObject):
_ver = StringField(default='0.9')
_id = UUIDField() _id = UUIDField()
workflow = EmbeddedObjectField(Workflow) # Once saved, we don't care what kind of workflow it is workflow = EmbeddedObjectField(Workflow) # Once saved, we don't care what kind of workflow it is
name = StringField() name = StringField()
@ -224,13 +222,6 @@ class Election(TopLevelObject):
questions = EmbeddedObjectListField() questions = EmbeddedObjectListField()
results = EmbeddedObjectListField(is_hashed=False) results = EmbeddedObjectListField(is_hashed=False)
is_voters_public = BooleanField(is_hashed=False, default=False)
is_votes_public = BooleanField(is_hashed=False, default=False)
def can_audit(self):
"""Can prepared votes be audited?"""
return False
def verify(self): def verify(self):
#__pragma__('skip') #__pragma__('skip')
from eos.core.hashing import SHA256 from eos.core.hashing import SHA256
@ -238,7 +229,7 @@ class Election(TopLevelObject):
election_hash = SHA256().update_obj(self).hash_as_b64() election_hash = SHA256().update_obj(self).hash_as_b64()
for voter in self.voters: for voter in self.voters:
for vote in voter.votes.get_all(): for vote in voter.votes:
if vote.ballot.election_id != self._id: if vote.ballot.election_id != self._id:
raise Exception('Invalid election ID on ballot') raise Exception('Invalid election ID on ballot')
if vote.ballot.election_hash != election_hash: if vote.ballot.election_hash != election_hash:

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-2021 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -26,25 +26,25 @@ class ElectionTestCase(EosTestCase):
cls.db_connect_and_reset() cls.db_connect_and_reset()
def do_task_assert(self, election, task, next_task): def do_task_assert(self, election, task, next_task):
self.assertEqual(election.workflow.get_task(task).status, WorkflowTaskStatus.READY) self.assertEqual(election.workflow.get_task(task).status, WorkflowTask.Status.READY)
if next_task is not None: if next_task is not None:
self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTaskStatus.NOT_READY) self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTask.Status.NOT_READY)
election.workflow.get_task(task).enter() election.workflow.get_task(task).enter()
self.assertEqual(election.workflow.get_task(task).status, WorkflowTaskStatus.EXITED) self.assertEqual(election.workflow.get_task(task).status, WorkflowTask.Status.EXITED)
if next_task is not None: if next_task is not None:
self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTaskStatus.READY) self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTask.Status.READY)
@py_only @py_only
def test_run_election(self): def test_run_election(self):
# Set up election # Set up election
election = Election() election = Election()
election.workflow = BaseWorkflow() election.workflow = WorkflowBase()
# Check _instance # Check _instance
self.assertEqual(election.workflow._instance, (election, 'workflow')) self.assertEqual(election.workflow._instance, (election, 'workflow'))
# Check workflow behaviour # Check workflow behaviour
self.assertEqual(election.workflow.get_task('eos.base.workflow.TaskConfigureElection').status, WorkflowTaskStatus.READY) self.assertEqual(election.workflow.get_task('eos.base.workflow.TaskConfigureElection').status, WorkflowTask.Status.READY)
self.assertEqual(election.workflow.get_task('does.not.exist'), None) self.assertEqual(election.workflow.get_task('does.not.exist'), None)
# Set election details # Set election details
@ -95,10 +95,10 @@ class ElectionTestCase(EosTestCase):
answer = ApprovalAnswer(choices=VOTES[i][j]) answer = ApprovalAnswer(choices=VOTES[i][j])
encrypted_answer = NullEncryptedAnswer(answer=answer) encrypted_answer = NullEncryptedAnswer(answer=answer)
ballot.encrypted_answers.append(encrypted_answer) ballot.encrypted_answers.append(encrypted_answer)
vote = Vote(voter_id=election.voters[i]._id, ballot=ballot, cast_at=DateTimeField.now()) vote = Vote(ballot=ballot, cast_at=DateTimeField.now())
vote.save() election.voters[i].votes.append(vote)
#election.save() election.save()
# Close voting # Close voting
self.do_task_assert(election, 'eos.base.workflow.TaskCloseVoting', 'eos.base.workflow.TaskDecryptVotes') self.do_task_assert(election, 'eos.base.workflow.TaskCloseVoting', 'eos.base.workflow.TaskDecryptVotes')

View File

@ -1,6 +1,6 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# pyRCV - Preferential voting counting # pyRCV - Preferential voting counting
# Copyright © 2016-2021 RunasSudo (Yingtong Li) # Copyright © 2016–2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -41,10 +41,10 @@ def writeBLT(election, q_num, seats, withdrawn=[]):
for candidate in flat_choices: for candidate in flat_choices:
if candidate.party: if candidate.party:
electionLines.append('"{}{}"'.format(candidate.name, candidate.party)) electionLines.append("'{}{}'".format(candidate.name, candidate.party))
else: else:
electionLines.append('"{}"'.format(candidate.name)) electionLines.append("'{}'".format(candidate.name))
electionLines.append('"{}{}"'.format(election.name, question.prompt)) electionLines.append("'{}{}'".format(election.name, question.prompt))
return electionLines return electionLines

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-2021 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -17,7 +17,8 @@
from eos.core.objects import * from eos.core.objects import *
from eos.core.tasks import * from eos.core.tasks import *
class WorkflowTaskStatus(EosEnum): class WorkflowTask(EmbeddedObject):
class Status:
UNKNOWN = 0 UNKNOWN = 0
NOT_READY = 10 NOT_READY = 10
READY = 20 READY = 20
@ -25,11 +26,10 @@ class WorkflowTaskStatus(EosEnum):
#COMPLETE = 40 #COMPLETE = 40
EXITED = 50 EXITED = 50
class WorkflowTask(EmbeddedObject):
depends_on = [] depends_on = []
provides = [] provides = []
status = EnumField(WorkflowTaskStatus, is_hashed=False, default=WorkflowTaskStatus.UNKNOWN) status = IntField(default=0, is_hashed=False)
exited_at = DateTimeField(is_hashed=False) exited_at = DateTimeField(is_hashed=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -40,8 +40,8 @@ class WorkflowTask(EmbeddedObject):
self.workflow = self.recurse_parents(Workflow) self.workflow = self.recurse_parents(Workflow)
if self.status == WorkflowTaskStatus.UNKNOWN: if self.status == WorkflowTask.Status.UNKNOWN:
self.status = WorkflowTaskStatus.READY if self.are_dependencies_met() else WorkflowTaskStatus.NOT_READY self.status = WorkflowTask.Status.READY if self.are_dependencies_met() else WorkflowTask.Status.NOT_READY
self.listeners = { self.listeners = {
'enter': [], 'enter': [],
@ -51,20 +51,15 @@ class WorkflowTask(EmbeddedObject):
# Helpers # Helpers
def on_dependency_exit(): def on_dependency_exit():
self.status = WorkflowTaskStatus.READY if self.are_dependencies_met() else WorkflowTaskStatus.NOT_READY self.status = WorkflowTask.Status.READY if self.are_dependencies_met() else WorkflowTask.Status.NOT_READY
for depends_on_desc in self.depends_on: for depends_on_desc in self.depends_on:
for depends_on_task in self.workflow.get_tasks(depends_on_desc): for depends_on_task in self.workflow.get_tasks(depends_on_desc):
depends_on_task.listeners['exit'].append(on_dependency_exit) depends_on_task.listeners['exit'].append(on_dependency_exit)
def are_dependencies_met(self): def are_dependencies_met(self):
for depends_on_desc in self.depends_on: for depends_on_desc in self.depends_on:
depends_on_tasks = list(self.workflow.get_tasks(depends_on_desc)) for depends_on_task in self.workflow.get_tasks(depends_on_desc):
if depends_on_task.status is not WorkflowTask.Status.EXITED:
if len(depends_on_tasks) == 0:
return False
for depends_on_task in depends_on_tasks:
if depends_on_task.status is not WorkflowTaskStatus.EXITED:
return False return False
return True return True
@ -76,10 +71,10 @@ class WorkflowTask(EmbeddedObject):
self.exit() self.exit()
def enter(self): def enter(self):
if self.status is not WorkflowTaskStatus.READY: if self.status is not WorkflowTask.Status.READY:
raise Exception('Attempted to enter a task when not ready') raise Exception('Attempted to enter a task when not ready')
self.status = WorkflowTaskStatus.ENTERED self.status = WorkflowTask.Status.ENTERED
self.fire_event('enter') self.fire_event('enter')
self.on_enter() self.on_enter()
@ -91,10 +86,10 @@ class WorkflowTask(EmbeddedObject):
self.exited_at = DateTimeField.now() self.exited_at = DateTimeField.now()
def exit(self): def exit(self):
if self.status is not WorkflowTaskStatus.ENTERED: if self.status is not WorkflowTask.Status.ENTERED:
raise Exception('Attempted to exit a task when not entered') raise Exception('Attempted to exit a task when not entered')
self.status = WorkflowTaskStatus.EXITED self.status = WorkflowTask.Status.EXITED
self.fire_event('exit') self.fire_event('exit')
self.on_exit() self.on_exit()
@ -151,7 +146,7 @@ class TaskConfigureElection(WorkflowTask):
label = 'Freeze the election' label = 'Freeze the election'
#def on_enter(self): #def on_enter(self):
# self.status = WorkflowTaskStatus.COMPLETE # self.status = WorkflowTask.Status.COMPLETE
class TaskOpenVoting(WorkflowTask): class TaskOpenVoting(WorkflowTask):
label = 'Open voting' label = 'Open voting'
@ -172,8 +167,8 @@ class TaskDecryptVotes(WorkflowTask):
election.results.append(EosObject.lookup('eos.base.election.RawResult')()) election.results.append(EosObject.lookup('eos.base.election.RawResult')())
for voter in election.voters: for voter in election.voters:
if len(voter.votes.get_all()) > 0: if len(voter.votes) > 0:
vote = voter.votes.get_all()[-1] vote = voter.votes[-1]
ballot = vote.ballot ballot = vote.ballot
for q_num in range(len(ballot.encrypted_answers)): for q_num in range(len(ballot.encrypted_answers)):
plaintexts, answer = ballot.encrypted_answers[q_num].decrypt() plaintexts, answer = ballot.encrypted_answers[q_num].decrypt()
@ -189,9 +184,7 @@ class TaskReleaseResults(WorkflowTask):
# Concrete workflows # Concrete workflows
# ================== # ==================
class BaseWorkflow(Workflow): class WorkflowBase(Workflow):
"""Base workflow, with no encryption"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from eos.core.objects import * from eos.core.objects import EosObject
import random import random
@ -125,7 +125,7 @@ class BigInt(EosObject):
def nbits(self): def nbits(self):
return self.impl.bitLength() return self.impl.bitLength()
def serialise(self, options=SerialiseOptions.DEFAULT): def serialise(self, for_hash=False, should_protect=False):
return str(self) return str(self)
@classmethod @classmethod

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from eos.core.objects import * from eos.core.objects import EosObject
import math import math
@ -46,7 +46,7 @@ class BigInt(EosObject):
def nbits(self): def nbits(self):
return math.ceil(math.log2(self.impl)) if self.impl > 0 else 0 return math.ceil(math.log2(self.impl)) if self.impl > 0 else 0
def serialise(self, options=SerialiseOptions.DEFAULT): def serialise(self, for_hash=False, should_protect=False):
return str(self) return str(self)
@classmethod @classmethod

View File

@ -27,18 +27,12 @@ class DBProvider:
def get_all(self, collection): def get_all(self, collection):
raise Exception('Not implemented') raise Exception('Not implemented')
def get_all_by_fields(self, collection, fields):
raise Exception('Not implemented')
def get_by_id(self, collection, _id): def get_by_id(self, collection, _id):
raise Exception('Not implemented') raise Exception('Not implemented')
def update_by_id(self, collection, _id, value): def update_by_id(self, collection, _id, value):
raise Exception('Not implemented') raise Exception('Not implemented')
def delete_by_id(self, collection, _id):
raise Exception('Not implemented')
def reset_db(self): def reset_db(self):
raise Exception('Not implemented') raise Exception('Not implemented')
@ -49,18 +43,12 @@ class DummyProvider(DBProvider):
def get_all(self, collection): def get_all(self, collection):
pass pass
def get_all_by_fields(self, collection, fields):
pass
def get_by_id(self, collection, _id): def get_by_id(self, collection, _id):
pass pass
def update_by_id(self, collection, _id, value): def update_by_id(self, collection, _id, value):
pass pass
def delete_by_id(self, collection, _id):
pass
def reset_db(self): def reset_db(self):
pass pass

View File

@ -26,25 +26,12 @@ class MongoDBProvider(eos.core.db.DBProvider):
def get_all(self, collection): def get_all(self, collection):
return self.db[collection].find() return self.db[collection].find()
def get_all_by_fields(self, collection, fields):
query = {}
if '_id' in fields:
query['_id'] = fields.pop('_id')
if 'type' in fields:
query['type'] = fields.pop('type')
for field in fields:
query['value.' + field] = fields[field]
return self.db[collection].find(query)
def get_by_id(self, collection, _id): def get_by_id(self, collection, _id):
return self.db[collection].find_one(_id) return self.db[collection].find_one(_id)
def update_by_id(self, collection, _id, value): def update_by_id(self, collection, _id, value):
self.db[collection].replace_one({'_id': _id}, value, upsert=True) self.db[collection].replace_one({'_id': _id}, value, upsert=True)
def delete_by_id(self, collection, _id):
self.db[collection].delete_one({'_id': _id})
def reset_db(self): def reset_db(self):
self.client.drop_database(self.db_name) self.client.drop_database(self.db_name)

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-18 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -34,24 +34,6 @@ class PostgreSQLDBProvider(eos.core.db.DBProvider):
self.cur.execute(SQL('SELECT data FROM {}').format(Identifier(table))) self.cur.execute(SQL('SELECT data FROM {}').format(Identifier(table)))
return [x[0] for x in self.cur.fetchall()] return [x[0] for x in self.cur.fetchall()]
def get_all_by_fields(self, table, fields):
def does_match(val):
if '_id' in fields and val['_id'] != fields.pop('_id'):
return False
if 'type' in fields and val['type'] != fields.pop('type'):
return False
for field in fields:
if val['value'][field] != fields[field]:
return False
return True
# TODO: Make this much better
result = []
for val in self.get_all(table):
if does_match(val):
result.append(val)
return result
def get_by_id(self, table, _id): def get_by_id(self, table, _id):
self.create_table(table) self.create_table(table)
self.cur.execute(SQL('SELECT data FROM {} WHERE _id = %s').format(Identifier(table)), (_id,)) self.cur.execute(SQL('SELECT data FROM {} WHERE _id = %s').format(Identifier(table)), (_id,))
@ -62,11 +44,6 @@ class PostgreSQLDBProvider(eos.core.db.DBProvider):
self.cur.execute(SQL('INSERT INTO {} (_id, data) VALUES (%s, %s) ON CONFLICT (_id) DO UPDATE SET data = excluded.data').format(Identifier(table)), (_id, psycopg2.extras.Json(value))) self.cur.execute(SQL('INSERT INTO {} (_id, data) VALUES (%s, %s) ON CONFLICT (_id) DO UPDATE SET data = excluded.data').format(Identifier(table)), (_id, psycopg2.extras.Json(value)))
self.conn.commit() self.conn.commit()
def delete_by_id(self, table, _id):
self.create_table(table)
self.cur.execute(SQL('DELETE FROM {} WHERE _id = %s').format(Identifier(table)), (_id))
self.conn.commit()
def reset_db(self): def reset_db(self):
self.cur.execute('DROP SCHEMA public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public') self.cur.execute('DROP SCHEMA public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public')
self.conn.commit() self.conn.commit()

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-18 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -71,22 +71,19 @@ class SHA256:
def update_obj(self, *values): def update_obj(self, *values):
for value in values: for value in values:
self.update_text(EosObject.to_json(EosObject.serialise_and_wrap(value, None, SerialiseOptions(for_hash=True)))) self.update_text(EosObject.to_json(EosObject.serialise_and_wrap(value, None, True)))
return self return self
def update_obj_raw(self, *values): def update_obj_raw(self, *values):
for value in values: for value in values:
self.update_text(EosObject.to_json(EosObject.serialise_and_wrap(value, None, SerialiseOptions(for_hash=False)))) self.update_text(EosObject.to_json(EosObject.serialise_and_wrap(value, None, False)))
return self return self
def hash_as_b64(self, short=False): def hash_as_b64(self):
if is_python: if is_python:
b64 = base64.b64encode(self.impl.digest()).decode('utf-8') return base64.b64encode(self.impl.digest()).decode('utf-8')
else: else:
b64 = self.impl.getHash('B64') return self.impl.getHash('B64')
if short:
return b64[:10]
return b64
def hash_as_hex(self): def hash_as_hex(self):
if is_python: if is_python:

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-2019 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -75,30 +75,8 @@ class Field:
self.is_protected = kwargs['is_protected'] if 'is_protected' in kwargs else False self.is_protected = kwargs['is_protected'] if 'is_protected' in kwargs else False
self.is_hashed = kwargs['is_hashed'] if 'is_hashed' in kwargs else not self.is_protected self.is_hashed = kwargs['is_hashed'] if 'is_hashed' in kwargs else not self.is_protected
def object_get(self, obj):
return obj._field_values[self.real_name]
def object_set(self, obj, value):
obj._field_values[self.real_name] = value
if isinstance(value, EosObject):
value._instance = (obj, self.real_name)
if not value._inited:
value.post_init()
def object_init(self, obj, value):
self.object_set(obj, value)
class SerialiseOptions:
def __init__(self, for_hash=False, should_protect=False, combine_related=False):
self.for_hash = for_hash
self.should_protect = should_protect
self.combine_related = combine_related
SerialiseOptions.DEFAULT = SerialiseOptions()
class PrimitiveField(Field): class PrimitiveField(Field):
def serialise(self, value, options=SerialiseOptions.DEFAULT): def serialise(self, value, for_hash=False, should_protect=False):
return value return value
def deserialise(self, value): def deserialise(self, value):
@ -115,8 +93,8 @@ class EmbeddedObjectField(Field):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.object_type = object_type self.object_type = object_type
def serialise(self, value, options=SerialiseOptions.DEFAULT): def serialise(self, value, for_hash=False, should_protect=False):
return EosObject.serialise_and_wrap(value, self.object_type, options) return EosObject.serialise_and_wrap(value, self.object_type, for_hash, should_protect)
def deserialise(self, value): def deserialise(self, value):
return EosObject.deserialise_and_unwrap(value, self.object_type) return EosObject.deserialise_and_unwrap(value, self.object_type)
@ -126,8 +104,8 @@ class ListField(Field):
super().__init__(default=EosList, *args, **kwargs) super().__init__(default=EosList, *args, **kwargs)
self.element_field = element_field self.element_field = element_field
def serialise(self, value, options=SerialiseOptions.DEFAULT): def serialise(self, value, for_hash=False, should_protect=False):
return [self.element_field.serialise(x, options) for x in (value.impl if isinstance(value, EosList) else 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 EosList([self.element_field.deserialise(x) for x in value]) return EosList([self.element_field.deserialise(x) for x in value])
@ -137,68 +115,29 @@ class EmbeddedObjectListField(Field):
super().__init__(default=EosList, *args, **kwargs) super().__init__(default=EosList, *args, **kwargs)
self.object_type = object_type self.object_type = object_type
def serialise(self, value, options=SerialiseOptions.DEFAULT): def serialise(self, value, for_hash=False, should_protect=False):
# TNYI: Doesn't know how to deal with iterators like EosList # TNYI: Doesn't know how to deal with iterators like EosList
if value is None: if value is None:
return None return None
return [EosObject.serialise_and_wrap(x, self.object_type, options) 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 if isinstance(value, EosList) else value)]
def deserialise(self, value): def deserialise(self, value):
if value is None: if value is None:
return None return None
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])
class RelatedObjectListManager: if is_python:
def __init__(self, field, obj): class UUIDField(Field):
self.field = field
self.obj = obj
def get_all(self):
query = {self.field.related_field: getattr(self.obj, self.field.this_field)}
return self.field.related_type.get_all_by_fields(**query)
class RelatedObjectListField(Field):
def __init__(self, object_type=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.related_type = kwargs['related_type']
self.object_type = kwargs['object_type'] if 'object_type' in kwargs else None
self.this_field = kwargs['this_field'] if 'this_field' in kwargs else '_id'
self.related_field = kwargs['related_field']
def object_get(self, obj):
return RelatedObjectListManager(self, obj)
def object_set(self, obj, value):
raise Exception('Cannot directly set related field')
def object_init(self, obj, value):
pass
def serialise(self, value, options=SerialiseOptions.DEFAULT):
if not options.combine_related:
return None
return EmbeddedObjectListField(object_type=self.object_type).serialise(value.get_all(), options)
def deserialise(self, value):
if value is None:
return None
return EosList([EosObject.deserialise_and_unwrap(x, self.object_type) for x in value])
class UUIDField(Field):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if is_python:
super().__init__(default=uuid.uuid4, *args, **kwargs) super().__init__(default=uuid.uuid4, *args, **kwargs)
else:
super().__init__(*args, **kwargs)
def serialise(self, value, options=SerialiseOptions.DEFAULT): def serialise(self, value, for_hash=False, should_protect=False):
return str(value) return str(value)
def deserialise(self, value): def deserialise(self, value):
if is_python:
return uuid.UUID(value) return uuid.UUID(value)
else: else:
return value UUIDField = PrimitiveField
class DateTimeField(Field): class DateTimeField(Field):
def pad(self, number): def pad(self, number):
@ -206,7 +145,7 @@ class DateTimeField(Field):
return '0' + str(number) return '0' + str(number)
return str(number) return str(number)
def serialise(self, value, options=SerialiseOptions.DEFAULT): def serialise(self, value, for_hash=False, should_protect=False):
if value is None: if value is None:
return None return None
@ -276,13 +215,12 @@ class EosObject(metaclass=EosObjectType):
return EosObject.objects[name] return EosObject.objects[name]
@staticmethod @staticmethod
def serialise_and_wrap(value, object_type=None, options=SerialiseOptions.DEFAULT): def serialise_and_wrap(value, object_type=None, for_hash=False, should_protect=False):
if object_type: if object_type:
if value: if value:
return value.serialise(options) return value.serialise(for_hash, should_protect)
if value:
return {'type': value._name, 'value': (value.serialise(options) if value else None)}
return None return None
return {'type': value._name, 'value': (value.serialise(for_hash, should_protect) if value else None)}
@staticmethod @staticmethod
def deserialise_and_unwrap(value, object_type=None): def deserialise_and_unwrap(value, object_type=None):
@ -362,16 +300,7 @@ class DocumentObjectType(EosObjectType):
fields = {} fields = {}
if hasattr(cls, '_fields'): if hasattr(cls, '_fields'):
fields = cls._fields.copy() if is_python else Object.create(cls._fields) fields = cls._fields.copy() if is_python else Object.create(cls._fields)
for attr in list(dir(cls)):
if is_python:
attrs = list(dir(cls))
else:
# We want the raw Javascript name for getOwnPropertyDescriptor
__pragma__('jsiter')
attrs = [x for x in cls]
__pragma__('nojsiter')
for attr in attrs:
if not is_python: if not is_python:
# We must skip things with getters or else they will be called here (too soon) # We must skip things with getters or else they will be called here (too soon)
if Object.getOwnPropertyDescriptor(cls, attr).js_get: if Object.getOwnPropertyDescriptor(cls, attr).js_get:
@ -398,9 +327,14 @@ class DocumentObjectType(EosObjectType):
if is_python: if is_python:
def make_property(name, field): def make_property(name, field):
def field_getter(self): def field_getter(self):
return field.object_get(self) return self._field_values[name]
def field_setter(self, value): def field_setter(self, value):
field.object_set(self, value) self._field_values[name] = value
if isinstance(value, EosObject):
value._instance = (self, name)
if not value._inited:
value.post_init()
return property(field_getter, field_setter) return property(field_getter, field_setter)
for attr, val in fields.items(): for attr, val in fields.items():
@ -419,8 +353,6 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__() super().__init__()
self._json = None
self._field_values = {} self._field_values = {}
# Different to Python # Different to Python
@ -430,11 +362,15 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType):
pass pass
else: else:
def make_property(name, field): def make_property(name, field):
# TNYI: Transcrypt doesn't pass self
def field_getter(): def field_getter():
return field.object_get(self) return self._field_values[name]
def field_setter(value): def field_setter(value):
field.object_set(self, value) self._field_values[name] = value
if isinstance(value, EosObject):
value._instance = (self, name)
if not value._inited:
value.post_init()
return (field_getter, field_setter) return (field_getter, field_setter)
prop = make_property(val.real_name, val) prop = make_property(val.real_name, val)
# TNYI: No support for property() # TNYI: No support for property()
@ -450,19 +386,15 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType):
}) })
if val.internal_name in kwargs: if val.internal_name in kwargs:
val.object_init(self, kwargs[val.internal_name]) setattr(self, val.real_name, kwargs[val.internal_name])
else: else:
default = val.default default = val.default
if default is not None and callable(default): if default is not None and callable(default):
default = default() default = default()
val.object_init(self, default) setattr(self, val.real_name, default)
def serialise(self, options=SerialiseOptions.DEFAULT): def serialise(self, for_hash=False, should_protect=False):
if self._ver != self._fields['_ver'].default: return {val.real_name: val.serialise(getattr(self, val.real_name), for_hash, should_protect) for attr, val in self._fields.items() if ((val.is_hashed or not for_hash) and (not should_protect or not val.is_protected))}
# Different version, use stored JSON
return self._json
return {val.real_name: val.serialise(getattr(self, val.real_name), options) for attr, val in self._fields.items() if ((val.is_hashed or not options.for_hash) and (not options.should_protect or not val.is_protected))}
@classmethod @classmethod
def deserialise(cls, value): def deserialise(cls, value):
@ -473,11 +405,7 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType):
for attr, val in cls._fields.items(): for attr, val in cls._fields.items():
if attr in value: if attr in value:
attrs[val.internal_name] = val.deserialise(value[val.real_name]) attrs[val.internal_name] = val.deserialise(value[val.real_name])
inst = cls(**attrs) return cls(**attrs)
inst._json = value
return inst
class TopLevelObjectType(DocumentObjectType): class TopLevelObjectType(DocumentObjectType):
def __new__(meta, name, bases, attrs): def __new__(meta, name, bases, attrs):
@ -499,26 +427,14 @@ class TopLevelObjectType(DocumentObjectType):
class TopLevelObject(DocumentObject, metaclass=TopLevelObjectType): class TopLevelObject(DocumentObject, metaclass=TopLevelObjectType):
def save(self): def save(self):
if self._ver != self._fields['_ver'].default: #res = db[self._name].replace_one({'_id': self.serialise()['_id']}, self.serialise(), upsert=True)
# Different version, unable to save #res = dbinfo.db[self._db_name].replace_one({'_id': self._fields['_id'].serialise(self._id)}, EosObject.serialise_and_wrap(self), upsert=True)
raise Exception('Attempted to save older vesion object')
dbinfo.provider.update_by_id(self._db_name, self._fields['_id'].serialise(self._id), EosObject.serialise_and_wrap(self)) dbinfo.provider.update_by_id(self._db_name, self._fields['_id'].serialise(self._id), EosObject.serialise_and_wrap(self))
def delete(self):
dbinfo.provider.delete_by_id(self._db_name, self._fields['_id'].serialise(self._id))
@classmethod @classmethod
def get_all(cls): def get_all(cls):
return [EosObject.deserialise_and_unwrap(x) for x in dbinfo.provider.get_all(cls._db_name)] return [EosObject.deserialise_and_unwrap(x) for x in dbinfo.provider.get_all(cls._db_name)]
@classmethod
def get_all_by_fields(cls, **fields):
for field in fields:
if not isinstance(fields[field], str):
fields[field] = str(fields[field])
return [EosObject.deserialise_and_unwrap(x) for x in dbinfo.provider.get_all_by_fields(cls._db_name, fields)]
@classmethod @classmethod
def get_by_id(cls, _id): def get_by_id(cls, _id):
if not isinstance(_id, str): if not isinstance(_id, str):
@ -527,61 +443,3 @@ class TopLevelObject(DocumentObject, metaclass=TopLevelObjectType):
class EmbeddedObject(DocumentObject): class EmbeddedObject(DocumentObject):
pass pass
# Enums
# =====
class EosEnumType(EosObjectType):
def __new__(meta, name, bases, attrs):
cls = EosObjectType.__new__(meta, name, bases, attrs)
cls._values = {}
for attr in list(dir(cls)):
val = getattr(cls, attr);
if isinstance(val, int):
instance = cls(attr, val)
setattr(cls, attr, instance)
cls._values[val] = instance
return cls
class EosEnum(EosObject, metaclass=EosEnumType):
def __init__(self, name, value):
super().__init__()
self.name = name
self.value = value
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return self.value == other.value
def __ne__(self, other):
if not isinstance(other, self.__class__):
return True
return self.value != other.value
def __gt__(self, other):
if not isinstance(other, self.__class__):
raise TypeError
return self.value > other.value
def __lt__(self, other):
if not isinstance(other, self.__class__):
raise TypeError
return self.value < other.value
def __ge__(self, other):
if not isinstance(other, self.__class__):
raise TypeError
return self.value >= other.value
def __le__(self, other):
if not isinstance(other, self.__class__):
raise TypeError
return self.value <= other.value
def serialise(self, options=SerialiseOptions.DEFAULT):
return self.value
@classmethod
def deserialise(cls, value):
return cls._values[value]
EnumField = EmbeddedObjectField

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-2019 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -16,7 +16,8 @@
from eos.core.objects import * from eos.core.objects import *
class TaskStatus(EosEnum): class Task(TopLevelObject):
class Status:
UNKNOWN = 0 UNKNOWN = 0
READY = 20 READY = 20
@ -26,24 +27,15 @@ class TaskStatus(EosEnum):
FAILED = -10 FAILED = -10
TIMEOUT = -20 TIMEOUT = -20
def is_error(self):
return self.value < 0
class Task(TopLevelObject):
label = 'Unknown task'
_ver = StringField(default='0.8')
_id = UUIDField() _id = UUIDField()
run_strategy = EmbeddedObjectField() run_strategy = EmbeddedObjectField()
run_at = DateTimeField() run_at = DateTimeField()
timeout = IntField(default=3600) # seconds
started_at = DateTimeField() started_at = DateTimeField()
completed_at = DateTimeField() completed_at = DateTimeField()
status = EnumField(TaskStatus, default=TaskStatus.UNKNOWN) status = IntField(default=0)
messages = ListField(StringField()) messages = ListField(StringField())
def run(self): def run(self):
@ -52,12 +44,6 @@ class Task(TopLevelObject):
def _run(self): def _run(self):
pass pass
def complete(self):
pass
def error(self):
pass
class DummyTask(Task): class DummyTask(Task):
_db_name = Task._db_name _db_name = Task._db_name
label = 'A dummy task' label = 'A dummy task'
@ -80,12 +66,9 @@ class TaskScheduler:
tasks = Task.get_all() tasks = Task.get_all()
for task in tasks: for task in tasks:
if task.status == TaskStatus.READY: if task.status == Task.Status.READY:
pending_tasks.append(task) pending_tasks.append(task)
# Sort them to ensure we iterate over them in the correct order
pending_tasks.sort(key=lambda task: task.run_at.timestamp() if task.run_at else 0)
return pending_tasks return pending_tasks
@staticmethod @staticmethod
@ -94,7 +77,7 @@ class TaskScheduler:
tasks = Task.get_all() tasks = Task.get_all()
for task in tasks: for task in tasks:
if task.status == TaskStatus.PROCESSING: if task.status == Task.Status.PROCESSING:
active_tasks.append(task) active_tasks.append(task)
return active_tasks return active_tasks
@ -105,7 +88,7 @@ class TaskScheduler:
tasks = Task.get_all() tasks = Task.get_all()
for task in tasks: for task in tasks:
if task.status == TaskStatus.COMPLETE or task.status.is_error(): if task.status == Task.Status.COMPLETE or task.status < 0:
completed_tasks.append(task) completed_tasks.append(task)
if limit: if limit:
@ -116,16 +99,6 @@ class TaskScheduler:
@staticmethod @staticmethod
def tick(): def tick():
now = DateTimeField.now()
for task in TaskScheduler.pending_tasks(): for task in TaskScheduler.pending_tasks():
if task.run_at and task.run_at < now: if task.run_at and task.run_at < DateTimeField.now():
task.run() task.run()
for task in TaskScheduler.active_tasks():
if task.timeout and (now - task.started_at).total_seconds() > task.timeout:
task.status = TaskStatus.TIMEOUT
task.completed_at = DateTimeField.now()
task.messages.append('Elapsed time exceeded timeout')
task.save()
task.error()

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-18 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -19,19 +19,17 @@ from eos.core.objects import *
class DirectRunStrategy(RunStrategy): class DirectRunStrategy(RunStrategy):
def run(self, task): def run(self, task):
task.status = TaskStatus.PROCESSING task.status = Task.Status.PROCESSING
task.started_at = DateTimeField.now() task.started_at = DateTimeField.now()
task.save() task.save()
try: try:
task._run() task._run()
task.status = TaskStatus.COMPLETE task.status = Task.Status.COMPLETE
task.completed_at = DateTimeField.now() task.completed_at = DateTimeField.now()
task.save() task.save()
task.complete()
except Exception as e: except Exception as e:
task.status = TaskStatus.FAILED task.status = Task.Status.FAILED
task.completed_at = DateTimeField.now() task.completed_at = DateTimeField.now()
if is_python: if is_python:
#__pragma__('skip') #__pragma__('skip')
@ -41,5 +39,3 @@ class DirectRunStrategy(RunStrategy):
else: else:
task.messages.append(repr(e)) task.messages.append(repr(e))
task.save() task.save()
task.error()

View File

@ -1,51 +0,0 @@
# Eos - Verifiable elections
# Copyright © 2017-2019 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/>.
from eos.core.tasks import *
from eos.core.objects import *
import threading
class ThreadingRunStrategy(RunStrategy):
def run(self, task):
def _run():
task.status = TaskStatus.PROCESSING
task.started_at = DateTimeField.now()
task.save()
try:
task._run()
task.status = TaskStatus.COMPLETE
task.completed_at = DateTimeField.now()
task.save()
task.complete()
except Exception as e:
task.status = TaskStatus.FAILED
task.completed_at = DateTimeField.now()
if is_python:
#__pragma__('skip')
import traceback
#__pragma__('noskip')
task.messages.append(traceback.format_exc())
else:
task.messages.append(repr(e))
task.save()
task.error()
thread = threading.Thread(target=_run, args=())
thread.start()

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-2021 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -133,7 +133,6 @@ class TaskTestCase(EosTestCase):
def setUpClass(cls): def setUpClass(cls):
cls.db_connect_and_reset() cls.db_connect_and_reset()
@py_only
def test_normal(self): def test_normal(self):
class TaskNormal(Task): class TaskNormal(Task):
result = StringField() result = StringField()
@ -145,12 +144,11 @@ class TaskTestCase(EosTestCase):
task.save() task.save()
task.run() task.run()
self.assertEqual(task.status, TaskStatus.COMPLETE) self.assertEqual(task.status, Task.Status.COMPLETE)
self.assertEqual(len(task.messages), 1) self.assertEqual(len(task.messages), 1)
self.assertEqual(task.messages[0], 'Hello World') self.assertEqual(task.messages[0], 'Hello World')
self.assertEqual(task.result, 'Success') self.assertEqual(task.result, 'Success')
@py_only
def test_error(self): def test_error(self):
class TaskError(Task): class TaskError(Task):
def _run(self): def _run(self):
@ -160,6 +158,6 @@ class TaskTestCase(EosTestCase):
task.save() task.save()
task.run() task.run()
self.assertEqual(task.status, TaskStatus.FAILED) self.assertEqual(task.status, Task.Status.FAILED)
self.assertEqual(len(task.messages), 1) self.assertEqual(len(task.messages), 1)
self.assertTrue('Test exception' in task.messages[0]) self.assertTrue('Test exception' in task.messages[0])

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-2019 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -14,23 +14,8 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import eos.core.objects import eos.js
import eos.core.bigint
import eos.core.hashing
import eos.core.tests import eos.core.tests
import eos.core.tasks
import eos.core.tasks.direct
import eos.base.election
import eos.base.workflow
import eos.psr.bitstream
import eos.psr.crypto
import eos.psr.election
import eos.psr.mixnet
import eos.psr.workflow
import eos.redditauth.election
import eos.base.tests import eos.base.tests
import eos.psr.tests import eos.psr.tests

View File

@ -1,30 +0,0 @@
# Eos - Verifiable elections
# Copyright © 2017-2019 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/>.
from eos.base.election import *
from eos.core.objects import *
class NationStatesUser(User):
username = StringField()
@property
def name(self):
return self.username
def matched_by(self, other):
if not isinstance(other, NationStatesUser):
return False
return other.username.lower().strip().replace(' ', '_') == self.username.lower().strip().replace(' ', '_')

View File

@ -153,7 +153,7 @@ class BitStream(EosObject):
bs.seek(0) bs.seek(0)
return bs return bs
def serialise(self, options=SerialiseOptions.DEFAULT): def serialise(self):
return self.impl return self.impl
@classmethod @classmethod
@ -173,7 +173,7 @@ class InfiniteHashBitStream(BitStream):
# 11000110110 # 11000110110
# ^---- # ^----
if nbits is None: if nbits is None:
raise Exception('Cannot read indefinite amount from InfiniteHashBitStream') nbits = self.remaining
while nbits > self.remaining: while nbits > self.remaining:
self.ctr += 1 self.ctr += 1
self.sha.update_text(str(self.ctr)) self.sha.update_text(str(self.ctr))

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-2021 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -144,7 +144,7 @@ class EGPrivateKey(EmbeddedObject):
result.commitmentA = pow(self.public_key.group.g, w, self.public_key.group.p) result.commitmentA = pow(self.public_key.group.g, w, self.public_key.group.p)
result.commitmentB = pow(ciphertext.gamma, w, self.public_key.group.p) result.commitmentB = pow(ciphertext.gamma, w, self.public_key.group.p)
result.challenge = SHA256().update_obj(ciphertext).update_obj(result.commitmentA).update_obj(result.commitmentB).update_obj(result.message).hash_as_bigint() result.challenge = SHA256().update_obj(ciphertext).update_obj(result.commitmentA).update_obj(result.commitmentB).hash_as_bigint()
result.response = w + self.x * result.challenge result.response = w + self.x * result.challenge
@ -175,8 +175,6 @@ class EGCiphertext(EmbeddedObject):
return ct.gamma == self.gamma and ct.delta == self.delta return ct.gamma == self.gamma and ct.delta == self.delta
class EGProvedPlaintext(EmbeddedObject): class EGProvedPlaintext(EmbeddedObject):
_ver = StringField(default='0.6')
message = EmbeddedObjectField(BigInt) message = EmbeddedObjectField(BigInt)
ciphertext = EmbeddedObjectField() ciphertext = EmbeddedObjectField()
@ -259,8 +257,7 @@ class PedersenVSSPrivateKey(EmbeddedObject):
def get_modified_secret(self): def get_modified_secret(self):
mod_s = self.x mod_s = self.x
for j in range(1, threshold + 1): # 1 to threshold for j in range(1, threshold + 1): # 1 to threshold
# TODO ...
pass
def decrypt(self, ciphertext): def decrypt(self, ciphertext):
if ( if (

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-2021 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -96,8 +96,8 @@ class MixingTrustee(Trustee):
# Use the raw ballots from voters # Use the raw ballots from voters
orig_answers = [] orig_answers = []
for voter in self.recurse_parents(Election).voters: for voter in self.recurse_parents(Election).voters:
if len(voter.votes.get_all()) > 0: if len(voter.votes) > 0:
vote = voter.votes.get_all()[-1] vote = voter.votes[-1]
ballot = vote.ballot ballot = vote.ballot
orig_answers.append(ballot.encrypted_answers[question_num]) orig_answers.append(ballot.encrypted_answers[question_num])
return orig_answers return orig_answers
@ -195,8 +195,8 @@ class InternalMixingTrustee(MixingTrustee):
else: else:
orig_answers = [] orig_answers = []
for voter in election.voters: for voter in election.voters:
if len(voter.votes.get_all()) > 0: if len(voter.votes) > 0:
ballot = voter.votes.get_all()[-1].ballot ballot = voter.votes[-1].ballot
orig_answers.append(ballot.encrypted_answers[question]) orig_answers.append(ballot.encrypted_answers[question])
shuffled_answers, commitments = self.mixnets[question].shuffle(orig_answers) shuffled_answers, commitments = self.mixnets[question].shuffle(orig_answers)
self.mixed_questions.append(EosList(shuffled_answers)) self.mixed_questions.append(EosList(shuffled_answers))
@ -225,20 +225,12 @@ class InternalMixingTrustee(MixingTrustee):
return True return True
class PSRElection(Election): class PSRElection(Election):
is_voters_public = BooleanField(is_hashed=False, default=True)
is_votes_public = BooleanField(is_hashed=False, default=True)
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() mixing_trustees = EmbeddedObjectListField()
def can_audit(self):
"""Overrides Election.can_audit"""
return True
def verify(self): def verify(self):
"""Overrides Election.verify"""
# Verify ballots # Verify ballots
super().verify() super().verify()

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-18 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -224,13 +224,13 @@ class ElectionTestCase(EosTestCase):
cls.db_connect_and_reset() cls.db_connect_and_reset()
def do_task_assert(self, election, task, next_task): def do_task_assert(self, election, task, next_task):
self.assertEqual(election.workflow.get_task(task).status, WorkflowTaskStatus.READY) self.assertEqual(election.workflow.get_task(task).status, WorkflowTask.Status.READY)
if next_task is not None: if next_task is not None:
self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTaskStatus.NOT_READY) self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTask.Status.NOT_READY)
election.workflow.get_task(task).enter() election.workflow.get_task(task).enter()
self.assertEqual(election.workflow.get_task(task).status, WorkflowTaskStatus.EXITED) self.assertEqual(election.workflow.get_task(task).status, WorkflowTask.Status.EXITED)
if next_task is not None: if next_task is not None:
self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTaskStatus.READY) self.assertEqual(election.workflow.get_task(next_task).status, WorkflowTask.Status.READY)
@py_only @py_only
def test_run_election(self): def test_run_election(self):
@ -278,10 +278,10 @@ class ElectionTestCase(EosTestCase):
answer = ApprovalAnswer(choices=VOTES[i][j]) answer = ApprovalAnswer(choices=VOTES[i][j])
encrypted_answer = BlockEncryptedAnswer.encrypt(election.sk.public_key, answer) encrypted_answer = BlockEncryptedAnswer.encrypt(election.sk.public_key, answer)
ballot.encrypted_answers.append(encrypted_answer) ballot.encrypted_answers.append(encrypted_answer)
vote = Vote(voter_id=election.voters[i]._id, ballot=ballot, cast_at=DateTimeField.now()) vote = Vote(ballot=ballot, cast_at=DateTimeField.now())
vote.save() election.voters[i].votes.append(vote)
#election.save() election.save()
# Close voting # Close voting
self.do_task_assert(election, 'eos.base.workflow.TaskCloseVoting', 'eos.psr.workflow.TaskMixVotes') self.do_task_assert(election, 'eos.base.workflow.TaskCloseVoting', 'eos.psr.workflow.TaskMixVotes')

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-2021 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -43,9 +43,9 @@ class BasePyTestCase(TestCase):
class BaseJSTestCase(TestCase): class BaseJSTestCase(TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
with open('eosweb/core/static/js/eosjs.js', 'r') as f: with open('eos/__javascript__/eos.js_tests.js', 'r') as f:
code = f.read() code = f.read()
cls.ctx = execjs.get().compile('var window={},navigator={};' + code + 'var eosjs=require("eosjs");var test=eosjs.' + cls.module + '.' + cls.name + '();test.setUpClass();') cls.ctx = execjs.get().compile('var window={},navigator={};' + code + 'var test=window.eosjs_tests.' + cls.module + '.__all__.' + cls.name + '();test.setUpClass();')
@classmethod @classmethod
def add_method(cls, method): def add_method(cls, method):

View File

@ -19,7 +19,6 @@
"semantic": "semantic-ui#^2.2.13", "semantic": "semantic-ui#^2.2.13",
"nunjucks": "^3.0.1", "nunjucks": "^3.0.1",
"dragula.js": "dragula#^3.7.2", "dragula.js": "dragula#^3.7.2",
"fingerprintjs2": "^1.5.1", "fingerprintjs2": "^1.5.1"
"progress-tracker": "^1.4.0"
} }
} }

View File

@ -1,67 +0,0 @@
# Eos - Verifiable elections
# Copyright © 2017-18 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/>.
import logging
import premailer
import sass
import flask
import flask_mail
from eos.base.election import *
def send_email(title, html_text, body_text, recipients):
# Prepare email
css = sass.compile(string=flask.render_template('email/base.scss'))
html = flask.render_template(
'email/base.html',
title=title,
css=css,
text=html_text
)
html = premailer.Premailer(html, strip_important=False).transform()
body = flask.render_template(
'email/base.txt',
title=title,
text=body_text
)
# Send email
mail = flask_mail.Mail(flask.current_app)
msg = flask_mail.Message(
title,
recipients=recipients,
body=body,
html=html
)
mail.send(msg)
def voter_email_password(election, voter):
send_email(
'Registered to vote: {}'.format(election.name),
'<p>Dear {},</p><p>You are registered to vote in <i>{}</i>.</p><p>Your login details are as follows:</p><p>Email: <code>{}</code></p><p>Password: <code>{}</code></p>'.format(voter.name, election.name, voter.user.email, voter.user.password),
'Dear {},\n\nYou are registered to vote in "{}".\n\nYour login details are as follows:\n\nEmail: {}\nPassword: {}'.format(voter.name, election.name, voter.user.email, voter.user.password),
[voter.user.email]
)
def task_email_failure(task):
send_email(
'Task failed: {}'.format(task.label),
'<p>The task <i>{}</i> failed execution. The output was:</p><pre>{}</pre>'.format(task.label, '\n'.join(task.messages)),
'The task "{}" failed execution. The output was:\n\n{}'.format(task.label, '\n'.join(task.messages)),
[admin.email for admin in flask.current_app.config['ADMINS'] if isinstance(admin, EmailUser)]
)

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-2021 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -16,7 +16,6 @@
import click import click
import flask import flask
import flask_session
import timeago import timeago
from eos.core.objects import * from eos.core.objects import *
@ -29,10 +28,6 @@ from eos.psr.election import *
from eos.psr.mixnet import * from eos.psr.mixnet import *
from eos.psr.workflow import * from eos.psr.workflow import *
from eosweb.core.tasks import *
from . import emails
import eos.core.hashing import eos.core.hashing
import eosweb import eosweb
@ -45,7 +40,6 @@ import json
import os import os
import pytz import pytz
import subprocess import subprocess
import uuid
app = flask.Flask(__name__, static_folder=None) app = flask.Flask(__name__, static_folder=None)
@ -63,16 +57,6 @@ if 'EOSWEB_SETTINGS' in os.environ:
# Connect to database # Connect to database
db_connect(app.config['DB_NAME'], app.config['DB_URI'], app.config['DB_TYPE']) db_connect(app.config['DB_NAME'], app.config['DB_URI'], app.config['DB_TYPE'])
# Configure sessions
if app.config['DB_TYPE'] == 'mongodb':
app.config['SESSION_TYPE'] = 'mongodb'
app.config['SESSION_MONGODB'] = dbinfo.provider.client
app.config['SESSION_MONGODB_DB'] = dbinfo.provider.db_name
elif app.config['DB_TYPE'] == 'postgresql':
app.config['SESSION_TYPE'] = 'sqlalchemy'
app.config['SQLALCHEMY_DATABASE_URI'] = app.config['DB_URI'] + app.config['DB_NAME']
flask_session.Session(app)
# Set configs # Set configs
User.admins = app.config['ADMINS'] User.admins = app.config['ADMINS']
@ -115,10 +99,49 @@ def run_tests(prefix, lang):
import eos.tests import eos.tests
eos.tests.run_tests(prefix, lang) eos.tests.run_tests(prefix, lang)
# Create the session databases (SQL only) # TODO: Will remove this once we have a web UI
@app.cli.command('sessdb') @app.cli.command('drop_db_and_setup')
def sessdb(): def setup_test_election():
app.session_interface.db.create_all() # DANGER!
dbinfo.provider.reset_db()
# Set up election
election = PSRElection()
election.workflow = PSRWorkflow()
# Set election details
election.name = 'Test Election'
from eos.redditauth.election import RedditUser
election.voters.append(UserVoter(user=EmailUser(name='Alice', email='alice@localhost')))
election.voters.append(UserVoter(user=EmailUser(name='Bob', email='bob@localhost')))
election.voters.append(UserVoter(user=EmailUser(name='Carol', email='carol@localhost')))
election.voters.append(UserVoter(user=RedditUser(username='RunasSudo')))
for voter in election.voters:
if isinstance(voter, UserVoter):
if isinstance(voter.user, EmailUser):
voter.user.email_password(app.config['SMTP_HOST'], app.config['SMTP_PORT'], app.config['SMTP_USER'], app.config['SMTP_PASS'], app.config['SMTP_FROM'])
election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting'))
election.mixing_trustees.append(InternalMixingTrustee(name='Eos Voting'))
election.sk = EGPrivateKey.generate()
election.public_key = election.sk.public_key
question = PreferentialQuestion(prompt='President', choices=[
Ticket(name='ACME Party', choices=[
Choice(name='John Smith'),
Choice(name='Joe Bloggs', party='Independent ACME')
]),
Choice(name='John Q. Public')
], min_choices=0, max_choices=3, randomise_choices=True)
election.questions.append(question)
question = ApprovalQuestion(prompt='Chairman', choices=[Choice(name='John Doe'), Choice(name='Andrew Citizen')], min_choices=0, max_choices=1)
election.questions.append(question)
election.save()
@app.cli.command('verify_election') @app.cli.command('verify_election')
@click.option('--electionid', default=None) @click.option('--electionid', default=None)
@ -135,8 +158,7 @@ def verify_election(electionid):
@click.option('--electionid', default=None) @click.option('--electionid', default=None)
@click.option('--qnum', default=0) @click.option('--qnum', default=0)
@click.option('--randfile', default=None) @click.option('--randfile', default=None)
@click.option('--seats', default=1) def tally_stv_election(electionid, qnum, randfile):
def tally_stv_election(electionid, qnum, randfile, numseats):
election = Election.get_by_id(electionid) election = Election.get_by_id(electionid)
with open(randfile, 'r') as f: with open(randfile, 'r') as f:
@ -145,22 +167,8 @@ def tally_stv_election(electionid, qnum, randfile, numseats):
election_id=election._id, election_id=election._id,
q_num=qnum, q_num=qnum,
random=dat, random=dat,
num_seats=numseats, num_seats=7,
status=TaskStatus.READY, status=Task.Status.READY,
run_strategy=EosObject.lookup(app.config['TASK_RUN_STRATEGY'])()
)
task.save()
task.run()
@app.cli.command('run_task')
@click.option('--electionid', default=None)
@click.option('--task_name', default=None)
def tally_stv_election(electionid, task_name):
election = Election.get_by_id(electionid)
task = WorkflowTaskEntryWebTask(
election_id=election._id,
workflow_task=task_name,
status=TaskStatus.READY,
run_strategy=EosObject.lookup(app.config['TASK_RUN_STRATEGY'])() run_strategy=EosObject.lookup(app.config['TASK_RUN_STRATEGY'])()
) )
task.save() task.save()
@ -183,6 +191,10 @@ def tick_scheduler():
# === Views === # === Views ===
@app.route('/')
def index():
return flask.render_template('index.html')
def using_election(func): def using_election(func):
@functools.wraps(func) @functools.wraps(func)
def wrapped(election_id, **kwargs): def wrapped(election_id, **kwargs):
@ -192,83 +204,17 @@ def using_election(func):
def election_admin(func): def election_admin(func):
@functools.wraps(func) @functools.wraps(func)
def wrapped(*args, **kwargs): 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(*args, **kwargs) return func(election, **kwargs)
else: else:
return flask.Response('Administrator credentials required', 403) return flask.Response('Administrator credentials required', 403)
return wrapped return wrapped
@app.route('/')
def index():
elections = Election.get_all()
elections.sort(key=lambda e: e.name)
elections_open = [e for e in elections if e.workflow.get_task('eos.base.workflow.TaskCloseVoting').status == WorkflowTaskStatus.READY]
elections_soon = [e for e in elections if e.workflow.get_task('eos.base.workflow.TaskOpenVoting').status != WorkflowTaskStatus.EXITED and e.workflow.get_task('eos.base.workflow.TaskOpenVoting').get_entry_task()]
elections_soon.sort(key=lambda e: e.workflow.get_task('eos.base.workflow.TaskOpenVoting').get_entry_task().run_at)
elections_closed = [e for e in elections if e.workflow.get_task('eos.base.workflow.TaskCloseVoting').status == WorkflowTaskStatus.EXITED]
elections_closed.sort(key=lambda e: e.workflow.get_task('eos.base.workflow.TaskCloseVoting').exited_at, reverse=True)
elections_closed = elections_closed[:5]
return flask.render_template('index.html', elections_open=elections_open, elections_soon=elections_soon, elections_closed=elections_closed)
@app.route('/elections')
def elections():
elections = Election.get_all()
elections.sort(key=lambda e: e.name)
return flask.render_template('elections.html', elections=elections)
@app.route('/elections/batch', methods=['GET', 'POST'])
@election_admin
def elections_batch():
if flask.request.method == 'POST':
# Execute
for k, v in flask.request.form.items():
if k.startswith('election_') and v:
election_id = k[9:]
election = Election.get_by_id(election_id)
for workflow_task in election.workflow.tasks:
if workflow_task.status == eos.base.workflow.WorkflowTaskStatus.READY:
task = WorkflowTaskEntryWebTask(
election_id=election._id,
workflow_task=workflow_task._name,
status=TaskStatus.READY,
run_strategy=EosObject.lookup(app.config['TASK_RUN_STRATEGY'])()
)
task.run()
break
elections = []
for election in Election.get_all():
if any(workflow_task.status == eos.base.workflow.WorkflowTaskStatus.READY for workflow_task in election.workflow.tasks):
elections.append(election)
elections.sort(key=lambda e: e.name)
return flask.render_template('elections_batch.html', elections=elections)
@app.route('/election/<election_id>/') @app.route('/election/<election_id>/')
@using_election @using_election
def election_api_json(election): def election_api_json(election):
is_full = 'full' in flask.request.args return flask.Response(EosObject.to_json(EosObject.serialise_and_wrap(election, should_protect=True, for_hash=('full' not in flask.request.args))), mimetype='application/json')
serialised = EosObject.serialise_and_wrap(election, None, SerialiseOptions(should_protect=True, for_hash=(not is_full), combine_related=True))
# Protect voters, votes if required
if not election.is_voters_public:
if 'voters' in serialised['value']:
del serialised['value']['voters']
if not election.is_votes_public:
if 'voters' in serialised['value']:
for voter in serialised['value']['voters']:
if 'votes' in voter['value']:
del voter['value']['votes']
return flask.Response(EosObject.to_json(serialised), mimetype='application/json')
@app.route('/election/<election_id>/view') @app.route('/election/<election_id>/view')
@using_election @using_election
@ -291,21 +237,8 @@ def election_view_questions(election):
@app.route('/election/<election_id>/view/ballots') @app.route('/election/<election_id>/view/ballots')
@using_election @using_election
def election_view_ballots(election): def election_view_ballots(election):
if election.is_voters_public or ('user' in flask.session and flask.session['user'].is_admin()):
return flask.render_template('election/view/ballots.html', election=election) return flask.render_template('election/view/ballots.html', election=election)
return flask.Response('Voters not public', 403)
@app.route('/election/<election_id>/voter/<voter_id>')
@using_election
def election_voter_view(election, voter_id):
if (election.is_voters_public and election.is_votes_public) or ('user' in flask.session and flask.session['user'].is_admin()):
voter_id = uuid.UUID(voter_id)
voter = next(voter for voter in election.voters if voter._id == voter_id)
return flask.render_template('election/voter/view.html', election=election, voter=voter)
return flask.Response('Voters not public', 403)
@app.route('/election/<election_id>/view/trustees') @app.route('/election/<election_id>/view/trustees')
@using_election @using_election
def election_view_trustees(election): def election_view_trustees(election):
@ -322,13 +255,13 @@ def election_admin_summary(election):
@election_admin @election_admin
def election_admin_enter_task(election): def election_admin_enter_task(election):
workflow_task = election.workflow.get_task(flask.request.args['task_name']) workflow_task = election.workflow.get_task(flask.request.args['task_name'])
if workflow_task.status != WorkflowTaskStatus.READY: if workflow_task.status != WorkflowTask.Status.READY:
return flask.Response('Task is not yet ready or has already exited', 409) return flask.Response('Task is not yet ready or has already exited', 409)
task = WorkflowTaskEntryWebTask( task = WorkflowTaskEntryTask(
election_id=election._id, election_id=election._id,
workflow_task=workflow_task._name, workflow_task=workflow_task._name,
status=TaskStatus.READY, status=Task.Status.READY,
run_strategy=EosObject.lookup(app.config['TASK_RUN_STRATEGY'])() run_strategy=EosObject.lookup(app.config['TASK_RUN_STRATEGY'])()
) )
task.run() task.run()
@ -341,31 +274,25 @@ def election_admin_enter_task(election):
def election_admin_schedule_task(election): def election_admin_schedule_task(election):
workflow_task = election.workflow.get_task(flask.request.form['task_name']) workflow_task = election.workflow.get_task(flask.request.form['task_name'])
task = WorkflowTaskEntryWebTask( task = WorkflowTaskEntryTask(
election_id=election._id, election_id=election._id,
workflow_task=workflow_task._name, workflow_task=workflow_task._name,
run_at=DateTimeField().deserialise(flask.request.form['datetime']), run_at=DateTimeField().deserialise(flask.request.form['datetime']),
status=TaskStatus.READY, status=Task.Status.READY,
run_strategy=EosObject.lookup(app.config['TASK_RUN_STRATEGY'])() run_strategy=EosObject.lookup(app.config['TASK_RUN_STRATEGY'])()
) )
task.save() task.save()
return flask.redirect(flask.url_for('election_admin_summary', election_id=election._id)) return flask.redirect(flask.url_for('election_admin_summary', election_id=election._id))
@app.route('/election/<election_id>/stage_ballot', methods=['POST'])
@using_election
def election_api_stage_ballot(election):
flask.session['staged_ballot'] = json.loads(flask.request.data)
return 'OK'
@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 < WorkflowTaskStatus.EXITED or election.workflow.get_task('eos.base.workflow.TaskCloseVoting').status > WorkflowTaskStatus.READY: 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 # Voting is not yet open or has closed
return flask.Response('Voting is not yet open or has closed', 409) return flask.Response('Voting is not yet open or has closed', 409)
data = flask.session['staged_ballot'] data = json.loads(flask.request.data)
if 'user' not in flask.session: if 'user' not in flask.session:
# User is not authenticated # User is not authenticated
@ -383,7 +310,7 @@ def election_api_cast_vote(election):
# Cast the vote # Cast the vote
ballot = EosObject.deserialise_and_unwrap(data['ballot']) ballot = EosObject.deserialise_and_unwrap(data['ballot'])
vote = Vote(voter_id=voter._id, ballot=ballot, cast_at=DateTimeField.now()) vote = Vote(ballot=ballot, cast_at=DateTimeField.now())
# Store data # Store data
if app.config['CAST_FINGERPRINT']: if app.config['CAST_FINGERPRINT']:
@ -394,13 +321,13 @@ def election_api_cast_vote(election):
else: else:
vote.cast_ip = flask.request.remote_addr vote.cast_ip = flask.request.remote_addr
vote.save() voter.votes.append(vote)
del flask.session['staged_ballot'] election.save()
return flask.Response(json.dumps({ return flask.Response(json.dumps({
'voter': EosObject.serialise_and_wrap(voter, None, SerialiseOptions(should_protect=True)), 'voter': EosObject.serialise_and_wrap(voter, should_protect=True),
'vote': EosObject.serialise_and_wrap(vote, None, SerialiseOptions(should_protect=True)) 'vote': EosObject.serialise_and_wrap(vote, should_protect=True)
}), mimetype='application/json') }), mimetype='application/json')
@app.route('/election/<election_id>/export/question/<int:q_num>/<format>') @app.route('/election/<election_id>/export/question/<int:q_num>/<format>')
@ -411,12 +338,6 @@ def election_api_export_question(election, q_num, format):
resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
return resp return resp
@app.route('/task/<task_id>')
@election_admin
def task_view(task_id):
task = Task.get_by_id(task_id)
return flask.render_template('task/view.html', task=task)
@app.route('/auditor') @app.route('/auditor')
def auditor(): def auditor():
return flask.render_template('election/auditor.html') return flask.render_template('election/auditor.html')
@ -427,27 +348,13 @@ def debug():
@app.route('/auth/login') @app.route('/auth/login')
def login(): def login():
flask.session['login_next'] = flask.request.referrer
return flask.render_template('auth/login.html') return flask.render_template('auth/login.html')
@app.route('/auth/stage_next', methods=['POST'])
def auth_stage_next():
flask.session['login_next'] = flask.request.data
return 'OK'
@app.route('/auth/logout') @app.route('/auth/logout')
def logout(): def logout():
flask.session['user'] = None flask.session['user'] = None
if flask.request.referrer: #return flask.redirect(flask.request.args['next'] if 'next' in flask.request.args else '/')
return flask.redirect(flask.request.referrer) # I feel like there's some kind of exploit here, so we'll leave this for now
else:
return flask.redirect('/')
@app.route('/auth/login_callback')
def login_callback():
if 'login_next' in flask.session and flask.session['login_next']:
return flask.redirect(flask.session['login_next'])
else:
return flask.redirect('/') return flask.redirect('/')
@app.route('/auth/login_complete') @app.route('/auth/login_complete')
@ -466,14 +373,6 @@ def email_login():
def email_authenticate(): def email_authenticate():
user = None user = None
for u in app.config['ADMINS']:
if isinstance(u, EmailUser):
if u.email.lower() == flask.request.form['email'].lower():
if u.password == flask.request.form['password']:
user = u
break
if user is None:
for election in Election.get_all(): for election in Election.get_all():
for voter in election.voters: for voter in election.voters:
if isinstance(voter.user, EmailUser): if isinstance(voter.user, EmailUser):
@ -493,7 +392,7 @@ def email_authenticate():
for app_name in app.config['APPS']: for app_name in app.config['APPS']:
app_main = importlib.import_module(app_name + '.main') app_main = importlib.import_module(app_name + '.main')
app.register_blueprint(app_main.blueprint) app_main.main(app)
# === Model-Views === # === Model-Views ===

View File

@ -1,6 +1,6 @@
/* /*
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -26,18 +26,6 @@
word-break: break-all; word-break: break-all;
} }
.monoout {
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
overflow-y: scroll;
padding: .78571429em 1em;
border: 1px solid rgba(34,36,38,.15);
color: rgba(0,0,0,.87);
border-radius: .28571429rem;
}
.superem { .superem {
font-weight: bold; font-weight: bold;
font-style: italic; font-style: italic;
@ -61,11 +49,6 @@ time[title] {
margin-left: 0 !important; margin-left: 0 !important;
} }
/* Fix nested selectable tables */
.ui.table.selectable tr > td.selectable:hover {
background: initial !important;
}
@media print { @media print {
body, html { body, html {
/* Default height: 100% causes blank pages */ /* Default height: 100% causes blank pages */
@ -135,29 +118,15 @@ time[title] {
padding: 0.5em 0 0.5em 0.5em; padding: 0.5em 0 0.5em 0.5em;
} }
.ticket-choices .number { .ticket-choices .number, .ticket-choices .content {
padding: 0;
}
.ticket-choices .content {
padding: 0 0 0 0.5em; padding: 0 0 0 0.5em;
} }
.preferential-choice .number { .preferential-choice .number {
width: 2em;
text-align: center; text-align: center;
} }
.preferential-choice .number select {
background-color: white;
}
.preferential-choice .content {
/* Vertically center */
display: flex;
flex-direction: column;
justify-content: center;
}
.preferential-choice .party-name, .preferential-choice .ticket-party-name { .preferential-choice .party-name, .preferential-choice .ticket-party-name {
font-style: italic; font-style: italic;
font-size: small; font-size: small;
@ -170,7 +139,3 @@ time[title] {
.ticket > .content > .party-name { .ticket > .content > .party-name {
font-size: inherit; font-size: inherit;
} }
#selections-make-help {
display: none;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

View File

@ -1,956 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="700"
height="372.06461"
viewBox="0 0 185.20833 98.442096"
version="1.1"
id="svg8"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="guide.svg"
inkscape:export-filename="/home/runassudo/git/Eos/eosweb/core/static/img/guide.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath987">
<rect
style="fill:#008000;fill-opacity:1;stroke:none;stroke-width:0.30000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect989"
width="91.290924"
height="6.1393275"
x="9.0702581"
y="78.243805" />
</clipPath>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="0.70710678"
inkscape:cx="293.72104"
inkscape:cy="292.29832"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="708"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
units="px"
inkscape:snap-global="false"
fit-margin-top="16"
fit-margin-bottom="16"
fit-margin-left="16"
fit-margin-right="16" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-6.1610108,-74.519826)">
<g
id="g1063"
transform="translate(5.4050422)">
<path
inkscape:connector-curvature="0"
id="path873"
d="M 13.985119,122.37498 H 172.73512"
style="fill:none;stroke:#cccccc;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
r="3.5907738"
cy="122.37498"
cx="13.985118"
id="path851"
style="fill:#5fbcd3;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<circle
style="fill:#5fbcd3;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="circle863"
cx="45.735119"
cy="122.37498"
r="3.5907738" />
<circle
r="3.5907738"
cy="122.37498"
cx="77.485115"
id="circle865"
style="fill:#5fbcd3;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<circle
style="fill:#5fbcd3;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="circle867"
cx="109.23512"
cy="122.37498"
r="3.5907738" />
<circle
r="3.5907738"
cy="122.37498"
cx="140.98512"
id="circle869"
style="fill:#5fbcd3;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<circle
style="fill:#5fbcd3;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="circle871"
cx="172.73515"
cy="122.37498"
r="3.5907738" />
<text
id="text877"
y="124.16092"
x="13.872649"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.55025291px;line-height:1.25;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.1387551"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.55025291px;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;writing-mode:lr-tb;text-anchor:middle;fill:#ffffff;stroke-width:0.1387551"
y="124.16092"
x="13.872649"
id="tspan875"
sodipodi:role="line">1</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.55025291px;line-height:1.25;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.1387551"
x="45.62265"
y="124.16092"
id="text881"><tspan
y="124.16092"
x="45.62265"
id="tspan899"
sodipodi:role="line">2</tspan></text>
<text
id="text885"
y="124.16092"
x="77.37265"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.55025291px;line-height:1.25;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.1387551"
xml:space="preserve"><tspan
y="124.16092"
x="77.37265"
id="tspan901"
sodipodi:role="line">3</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.55025291px;line-height:1.25;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.1387551"
x="109.12263"
y="124.16092"
id="text889"><tspan
y="124.16092"
x="109.12263"
id="tspan903"
sodipodi:role="line">4</tspan></text>
<text
id="text893"
y="124.16092"
x="140.87263"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.55025291px;line-height:1.25;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.1387551"
xml:space="preserve"><tspan
y="124.16092"
x="140.87263"
id="tspan905"
sodipodi:role="line">5</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.55025291px;line-height:1.25;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.1387551"
x="172.62267"
y="124.16092"
id="text897"><tspan
y="124.16092"
x="172.62267"
id="tspan907"
sodipodi:role="line">6</tspan></text>
<image
width="68.262505"
height="36.35281"
preserveAspectRatio="none"
xlink:href="data:image/png;base64,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
"
id="image946"
x="107.57722"
y="132.37578" />
<image
width="83.738396"
height="26.784683"
preserveAspectRatio="none"
xlink:href="data:image/png;base64,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
r7zyCubOnVvaYdBLrHv37khLS8NXX32FW7du4ZVXXkGrVq3w3XffST8RQYbhnE9EpB1vTSQiIiIi
IjIz3ppIRERERERkZkzEiIiIiIiIzIyJGBERERERkZkxESMiIiIiIjIzJmJERERERERmxkSMiIiI
iIjIzJiIERERERERmRkTMSIiIiIiIjNjIkZERERERGRmTMSIiIiIiIjMjIkYERERERGRmTERIyIi
IiIiMjMmYkRERERERGbGRIyIiIiIiMjMmIgRERERERGZGRMxIiIiIiIiM2MiRkREREREZGZMxIiI
iIiIiMyMiRgREREREZGZMREjIiIiIiIyMyZiREREREREZsZEjIiIiIiIyMyYiBEREREREZkZEzEi
IiIiIiIzYyJGRERERERkZhWMKZyRkYkHD+4jIyPToPJVqtjCyckZVarYGhMGERERERFRuWIhhBCG
FMzIyER8fDxq134d9vb2BlWelpaG27cTUadOHSZjRERERET00jD41sT79+8blYQBgL29PWrXfh33
7983eB9ERERERETljcGJWGZmplFJWAF7e3tkZhp2ayMREREREVF59EI8rCM4OBgNGzbUq0yvXr0w
ePBgk8Yxf/58uLi4qPzz8PBAz549sX79emRlZZm0PiIiIiIiKp+MelgHqTdjxgzp08KMjAycPn0a
c+bMwY8//oiNGzfC2dm5lCMkIiIiIqLSxESsBHTt2hVOTk7S8qBBg3Dy5El88MEHGDNmDMLDw2Fh
YVGKERIRERERUWl6IW5NLA/eeustfPLJJzh9+jSOHj1a2uEQEREREVEpemETse+++w4BAQFwdXXF
m2++iRkzZiAlJaXYdocPH0bHjh3h7u6OPn364O+//5Ze69WrF0JDQ7Fr1y74+fmp3UYfffr0gYWF
BSIjI/WONSwsDP7+/nBzc0OHDh2watUq5ObmGhQHERERERGVrhfy1sRly5YhNDQUQ4cOhVKpREJC
AhYtWoTz58/jhx9+QMWKFQEAsbGxSE1NxYQJE5Cfn4958+Zh1KhROHjwICpUeN41GzduxL/+9S8s
X74c+fn5mDp1arFt5HrllVdQp04dXLp0Sa9Y16xZg9WrVyMkJATu7u6Ii4vDrFmzkJaWhuDgYNN1
HBERERERmcULl4ilpKRgxYoVeP/99xESEiKtr1OnDgYOHIi9e/eiZ8+eAIBatWph27ZtqFSpEgBA
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
U1WW59ScOKv8hvTcu22TXv/DOzP1j+vmKXtOjPac6lOzZ0TrbkrSjkcXKd8VO+7arKRovfxQvho7
h/XGYY9yk2P14z/N1aJUuySptqlf+5oDz36d7hrWv9ae17/vm3wZ4mT9TsXjq9L1D/dky++XXv+k
U0cvDOnR5W79R0WerjxPF3mzZkasuvp3qq7+XfDYZotWxQMPqri4OHhu3bp1KilZocWLF8tiseiT
jz/Wtm3/rQ8/+lBLCgvV29Oj3e/vliUqSn/+2GPKzs6WJHV0dMjlck0Ys6OjQ1VVVYqOjtGmTZtk
s82atwsAAABfIZfC0+c9I+oYGLvqtVmJ0XqsLE1+Q3rwlRM6emFIkvTP6+epYqlLT67O0OM7mse1
+d7OZlUdDcxm+fyGKotTtPaPknS8fUjvn+xVmsOmNQWJOtkxrBf2nLvmuq/W77XKdEbrr25L16nO
YVW+dlJjPkOS9Ozd2Sq/JVmluY5gUJxJZk2yyM6ep7lZcyVJfb19OnHihLZv36Yhr1dlK1dKktLS
0pWWFlgXaxiGUtPSJEldnV2SpJMnT2psbFQ5ubnBECZJbrd7wni9vb166603NTo6ovJvbQz2CwAA
AMw0l2Z9jGu49rZ8p6wW6aPWgWAIk6Stn3SqYqlLq/KdE9o0eUa+8O/A0sVMZ/T1lBy2flfNdyrG
atGClFj9/snCCa9nJcVcX5HTZNYEscWLFwc365CkxsZGbfn5z/TOrne0tKhICQkJ8nq9qqmp0ZEj
h9Xe3i6/L7Ae1Hfx98DgoCTJ6Zj4x3W5V//rFUnSnOTkcbNuAAAAwExzpjsQaLKSYpQcZ5XH++XP
RSXHByLA5TNnl46T7FZFWQI7Ll7RxfNRljAv+gux38RYq6TAEskXPxg/G2dIOnbBG47qwm7WPSN2
SVZWliTJ7/PJ4/FodHRUP928We+/967S09O1cWOFNlZUjGuTcHEzjt7e3kn7X3fvvcrJzVW3x6Oq
qqrw3wAAAAAQJvXNfRoc9ctqkX5wZ+ZVr+0aDAQud8L4OZmUi8c9Q74vD2Em8hvXVsT5/lFJUkq8
TR+2Doz7+ah1QP0j/kl6iIxZGcQMw1Bdba0kyWqzye12q6WlRR0d7SpYuEibNlWquLhYGRnj/wjz
58+XJSpKra0tam1pCZ4/ffq0vN7xSTkvL1+VlZWKi4/XvrpaHTt2bPpvDAAAAAhB37BfW+oDOwhW
Fqfon+6dp7mJgSV+CTFR2rjUpb3fvVm35TlU09inEZ+hZVkJ4zbGeGhZYOfDvY19Ux5/9OJzWc7Y
8MWLgRG/+oYDISovObC8MDrKormXLTWsawrcz01pdt21KCl4vsAdqweLJu4DMVPMmqWJ9fX1ajgc
2P2wp6dHgwMDkqQ1X1sju92uxMREWSwWtbV9rn11dfIbftXU1IzrIzk5WaWlZdpXV6stW7ZowYIF
GvQOqrmp6Ypb0yclzVF5ebl+9dpr2r5tm/76iSfkdE6+rBEAAAAw20s155TnitV9S+bogSKXHihy
yWcEvmxZknyGlOqIVm1Tv35Sc15Prc7Q9kcWav+ZfqU7olWYEae+Yb/+5YNr32zjkhMXN9coyXFo
x3cWamjUUOWrJ6/7nv7nWLcqlrr08kPzVXO6T2W5Ti1wj9/VsWvQp5f2ntMP7szU5o152tfUL4tF
Ks11SAosWfy8Z+RK3UfUrJkR6+vr1dm2Np1ta5NvbEy5eXnaVPlnwefG3G63Nmy4T9E2m3ZV7dJn
n36m++8vV0HBwnH9rF+/Xt/85t1KcCTo2PFj6u7u1h13rNbatWuvOG5h4S0qLS3T4OCAtm3bJuMa
p0gBAAAAM/kN6XtvNOsvf9OkvY198nh98huGLvSP6e0j3Sr/xfHgFyv/W+15Pb2rRa3dI7o936l5
c2JUdbRH5b88rtNdw5OMNNGhc169tPe8eod8ykqM0enOoS/9guepeLa6TTsbPHLF27T+5mTtP9Ov
Z3a1TNiUZHPdBf3w7RYdvzCkkpwELc2M176mPj38q1MzMoRJksUIMVk0n2mZ/CKEJDdnXqRLAAAA
QJh88cuIceNqfKZoStfPmhkxAAAAALhREMQAAAAAwGQEMQAAAAAwGUEMAAAAAExGEAMAAAAAkxHE
AAAAAMBkBDEAAAAAMBlBDAAAAABMRhADAAAAAJMRxAAAAADAZCEHMavVGs46cBHvKwAAwI1laXps
pEvANAvlMw45iMXHx4XaFFcRF2ePdAkAAAAIo2/dMifSJWCalRcmTblNyEHMlZys6OjoUJvjCqKj
bUpxuSJdBgAAAMLo4ZIM3ZrNJMaNamVOnB5ZkTnldhbDMIzrGbijo1MDg4PX0wUkJcTHy+1OiXQZ
AAAAmCYv7mnVKx91yTN0Xf/9xgyRbLfo4T9x6YnV2SG1v+4gBgAAAACYGnZNBAAAAACTEcQAAAAA
wGQEMQAAAAAwGUEMAAAAAExGEAMAAAAAkxHEAAAAAMBkBDEAAAAAMBlBDAAAAABMRhADAAAAAJMR
xAAAAADAZAQxAAAAADAZQQwAAAAATPZ/wVxNFcH7JnEAAAAASUVORK5CYII=
"
id="image957"
x="13.085088"
y="84.769852" />
<rect
rx="2.1166668"
y="78.90316"
x="10.57835"
height="34.661263"
width="88.751862"
id="rect960"
style="fill:none;fill-opacity:1;stroke:#cccccc;stroke-width:0.30000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<rect
transform="translate(0,-1.5875)"
clip-path="url(#clipPath987)"
style="fill:#cccccc;fill-opacity:1;stroke:none;stroke-width:0.30000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect962"
width="88.751862"
height="32.924011"
x="10.57835"
y="80.490662"
rx="2.1166668" />
<rect
rx="0.81605595"
y="79.615738"
x="95.155502"
height="2.4673245"
width="2.4673245"
id="rect993"
style="fill:#c83737;fill-opacity:1;stroke:none;stroke-width:0.30000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<rect
style="fill:#d45500;fill-opacity:1;stroke:none;stroke-width:0.30000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect995"
width="2.4673245"
height="2.4673245"
x="91.45134"
y="79.615738"
rx="0.81605595" />
<rect
rx="0.81605595"
y="79.615738"
x="87.747177"
height="2.4673245"
width="2.4673245"
id="rect997"
style="fill:#2ca02c;fill-opacity:1;stroke:none;stroke-width:0.30000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<text
id="text1001"
y="89.363792"
x="105.43783"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.64444447px;line-height:1.25;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.64444447px;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.26458332"
y="89.363792"
x="105.43783"
id="tspan999"
sodipodi:role="line">Click ‘Help’ for guidance</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.64444447px;line-height:1.25;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="105.43783"
y="102.59296"
id="text1005"><tspan
sodipodi:role="line"
id="tspan1003"
x="105.43783"
y="102.59296"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.64444447px;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.26458332">Follow the instructions</tspan><tspan
id="tspan1007"
sodipodi:role="line"
x="105.43783"
y="109.64851"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.64444447px;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.26458332">to cast your ballot</tspan></text>
<text
id="text1013"
y="143.86795"
x="42.731579"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.64444447px;line-height:1.25;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.64444447px;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.26458332"
y="143.86795"
x="42.731579"
sodipodi:role="line"
id="tspan1011">When you receive a <tspan
id="tspan1025"
style="font-weight:bold;fill:#005500">green</tspan></tspan><tspan
id="tspan1017"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.64444447px;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.26458332"
y="150.92351"
x="42.731579"
sodipodi:role="line">‘smart ballot tracker’,</tspan><tspan
id="tspan1023"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.64444447px;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.26458332"
y="157.97906"
x="42.731579"
sodipodi:role="line">you're done!</tspan></text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,187 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="172.86369mm"
height="69.114601mm"
viewBox="0 0 172.86369 69.114601"
version="1.1"
id="svg8"
sodipodi:docname="logo.svg"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
inkscape:export-filename="/tmp/bitmap.png"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300">
<defs
id="defs2">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath4586">
<ellipse
style="fill:#008000;fill-opacity:1;stroke:none;stroke-width:1.29814279;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="ellipse4588"
cx="57.005764"
cy="92.121689"
rx="29.557428"
ry="29.557301" />
</clipPath>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="0.43289459"
inkscape:cx="65.975662"
inkscape:cy="27.661024"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="708"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:snap-global="true"
showguides="false"
fit-margin-top="5"
fit-margin-left="5"
fit-margin-right="5"
fit-margin-bottom="5" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-22.448336,-359.18952)">
<g
clip-path="url(#clipPath4586)"
id="g4865"
transform="translate(0,301.62513)">
<rect
style="fill:#0b1728;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4859"
width="59.285988"
height="39.253845"
x="27.362764"
y="93.292252" />
<rect
y="49.178513"
x="27.362764"
height="54.870293"
width="59.285988"
id="rect4861"
style="fill:#1f4272;fill-opacity:1;stroke:none;stroke-width:1.11792147;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
<g
transform="translate(0.21824265,300.61125)"
id="g4879">
<path
style="fill:#f9f9f9;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 46.70614,80.20013 h 20.162756 l 4.233337,6.690286 H 42.47281 Z"
id="path4867"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<g
id="g4873"
style="fill:#e0e0e0;fill-opacity:1"
transform="translate(0,-1.5875)">
<rect
y="88.477921"
x="42.472809"
height="17.719406"
width="28.629425"
id="rect4869"
style="fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
ry="3"
rx="3"
style="fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4871"
width="28.629425"
height="15.76951"
x="42.472809"
y="96.689354" />
</g>
<path
style="fill:none;stroke:#0b1728;stroke-width:0.69999999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 51.385265,83.545273 H 62.189779"
id="path4875"
inkscape:connector-curvature="0" />
<rect
style="fill:#5fbcd3;fill-opacity:1;stroke:none;stroke-width:0.69999999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4877"
width="12.071115"
height="6.5703545"
x="-22.056793"
y="91.344513"
rx="1"
ry="1"
transform="rotate(-45.88581)" />
</g>
<text
id="text4883"
y="393.78824"
x="95.370705"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.57777786px;line-height:1.25;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#0b1728;fill-opacity:1;stroke:none;stroke-width:0.26458332"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.57777786px;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#0b1728;stroke-width:0.26458332"
y="393.78824"
x="95.370705"
id="tspan4881"
sodipodi:role="line">Eos Voting</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:9.87777805px;line-height:1.25;font-family:Carlito;-inkscape-font-specification:'Carlito, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#373e48;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="96.429039"
y="410.19238"
id="text4887"><tspan
y="410.19238"
x="96.429039"
id="tspan4885"
sodipodi:role="line">Vote with confidence</tspan></text>
<g
transform="translate(0.51981687,300.63247)"
id="g4895">
<rect
style="fill:none;fill-opacity:1;stroke:#3771c8;stroke-width:1.20000005;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4889"
width="12.425195"
height="12.425195"
x="50.574924"
y="92.730827" />
<path
sodipodi:nodetypes="ccccc"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#e0e0e0;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 64.052734,91.082031 -7.623046,9.347659 1.640433,1.15336 7.531441,-9.237347 z"
id="path4891"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#002255;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 64.096834,92.609763 -7.590808,9.309147 -3.025264,-3.025268"
id="path4893"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccc" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -1,6 +1,6 @@
/* /*
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2021 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -19,37 +19,18 @@
window = self; // Workaround for libraries window = self; // Workaround for libraries
isLibrariesLoaded = false; isLibrariesLoaded = false;
eosjs = null;
function generateEncryptedVote(election, answers, should_do_fingerprint) { function generateEncryptedVote(election, answers, should_do_fingerprint) {
if (election._name === 'eos.psr.election.PSRElection') {
encrypted_answers = []; encrypted_answers = [];
for (var q_num = 0; q_num < answers.length; q_num++) { for (var q_num = 0; q_num < answers.length; q_num++) {
answer_json = answers[q_num]; answer_json = answers[q_num];
answer = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(answer_json, null); answer = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(answer_json, null);
encrypted_answer = eosjs.eos.psr.election.BlockEncryptedAnswer.encrypt(election.public_key, answer, election.questions.__getitem__(q_num).max_bits() + 32); // +32 bits for the length encrypted_answer = eosjs.eos.psr.election.__all__.BlockEncryptedAnswer.encrypt(election.public_key, answer, election.questions.__getitem__(q_num).max_bits() + 32); // +32 bits for the length
encrypted_answers.push(eosjs.eos.core.objects.EosObject.serialise_and_wrap(encrypted_answer, null)); encrypted_answers.push(eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(encrypted_answer, null));
} }
postMessage({ postMessage({
encrypted_answers: encrypted_answers encrypted_answers: encrypted_answers
}); });
} else if (election._name === 'eos.base.election.Election') {
encrypted_answers = [];
for (var q_num = 0; q_num < answers.length; q_num++) {
answer_json = answers[q_num];
answer = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(answer_json, null);
encrypted_answer = eosjs.eos.base.election.NullEncryptedAnswer();
encrypted_answer.answer = answer;
encrypted_answers.push(eosjs.eos.core.objects.EosObject.serialise_and_wrap(encrypted_answer, null));
}
postMessage({
encrypted_answers: encrypted_answers
});
} else {
throw "Don't know how to encrypt ballots in election of type " + election._name;
}
} }
onmessage = function(msg) { onmessage = function(msg) {
@ -59,11 +40,10 @@ onmessage = function(msg) {
msg.data.static_base_url + "js/eosjs.js" msg.data.static_base_url + "js/eosjs.js"
); );
isLibrariesLoaded = true; isLibrariesLoaded = true;
eosjs = require("eosjs");
} }
if (msg.data.action === "generateEncryptedVote") { if (msg.data.action === "generateEncryptedVote") {
msg.data.election = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(msg.data.election, null); msg.data.election = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(msg.data.election, null);
generateEncryptedVote(msg.data.election, msg.data.answers); generateEncryptedVote(msg.data.election, msg.data.answers);
} else { } else {

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2019 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -25,7 +25,7 @@
<p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p> <p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p>
</div> </div>
<p>The following is your ballot with fingerprint <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64() }}</span>, decrypted and ready for auditing.</p> <p>The following is your ballot with fingerprint <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>, decrypted and ready for auditing.</p>
<div class="ui form"> <div class="ui form">
{# For some reason nunjucks doesn't like calling this the normal way #} {# For some reason nunjucks doesn't like calling this the normal way #}
@ -41,11 +41,3 @@
{# Dirty trick to go back to the encryption step #} {# Dirty trick to go back to the encryption step #}
<button class="ui right floated primary button" onclick="nextTemplate(-2);">Continue</button> <button class="ui right floated primary button" onclick="nextTemplate(-2);">Continue</button>
{% endblock %} {% endblock %}
{% block help %}
<p>Eos is now ready to audit your ballot. This is an optional step that you can take to ensure that your ballot was prepared correctly by the voting booth.</p>
<p>To complete the audit, provide the data shown to a trusted third-party, or <a href="/auditor?electionUrl={{ election_base_url }}" target="_blank">click here to open the Eos auditor</a> and copy-paste in the data shown.</p>
<p>If you are satisfied with the result of the audit, click the blue ‘Continue’ button to proceed to cast your ballot.</p>
<p>Click the ‘OK’ button below to close this help screen.</p>
<p>If you require further assistance, contact your election administrator.</p>
{% endblock %}

View File

@ -1,6 +1,6 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2021 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -18,80 +18,39 @@
<h1>{{ election.name }}</h1> <h1>{{ election.name }}</h1>
<p><small><b>{{ election.kind|title }} fingerprint:</b> <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(election).hash_as_b64() }}</span></small></p> <p><small><b>{{ election.kind|title }} fingerprint:</b> <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(election).hash_as_b64() }}</span></small></p>
{# Convert the template name to a numerical index for comparison #} {# Convert the template name to a numerical index for comparison #}
{% if template == 'booth/welcome.html' %} {% if template == 'booth/welcome.html' %}
{% set menuindex = 1 %} {% set menuindex = 1 %}
{% elif template == 'booth/selections.html' %} {% elif template == 'booth/selections.html' %}
{% set menuindex = 2 %} {% set menuindex = 2 %}
{% elif template == 'booth/encrypt.html' %}
{% set menuindex = 2.5 %}
{% elif template == 'booth/review.html' %} {% elif template == 'booth/review.html' %}
{% set menuindex = 3 %} {% set menuindex = 3 %}
{% elif template == 'booth/audit.html' %} {% elif template == 'booth/audit.html' %}
{% set menuindex = 4 %} {% set menuindex = 4 %}
{% elif template == 'booth/cast.html' %} {% elif template == 'booth/cast.html' %}
{% if election.can_audit() %}
{% set menuindex = 5 %} {% set menuindex = 5 %}
{% else %}
{% set menuindex = 4 %}
{% endif %}
{% elif template == 'booth/complete.html' %} {% elif template == 'booth/complete.html' %}
{% if election.can_audit() %}
{% set menuindex = 6 %} {% set menuindex = 6 %}
{% else %}
{% set menuindex = 5 %}
{% endif %}
{% endif %} {% endif %}
{% macro menuitem(index, text) %} <div class="ui secondary pointing menu" id="election-tab-menu">
<li class="progress-step{% if menuindex > index %} is-complete{% elif menuindex == index %} is-active{% endif %}"> {# oh dear god #}
<span class="progress-marker">{% if menuindex > index or menuindex == 6 %}<span style="font-family: Icons;">&#xf00c;</span>{% else %}{{ index }}{% endif %}</span> <span class="ui{% if menuindex >= 1 %} active{% endif %} item"{% if menuindex != 1 %} style="color: #767676;{% if menuindex > 1 %} font-weight: 400;{% endif %}"{% endif %}>1. Welcome</span>
<span class="progress-text"> <span class="ui{% if menuindex >= 2 %} active{% endif %} item"{% if menuindex != 2 %} style="color: #767676;{% if menuindex > 2 %} font-weight: 400;{% endif %}"{% endif %}>2. Make selections</span>
{{ text }} <span class="ui{% if menuindex >= 3 %} active{% endif %} item"{% if menuindex != 3 %} style="color: #767676;{% if menuindex > 3 %} font-weight: 400;{% endif %}"{% endif %}>3. Review selections</span>
</span> <span class="ui{% if menuindex >= 4 %} active{% endif %} item"{% if menuindex != 4 %} style="color: #767676;{% if menuindex > 4 %} font-weight: 400;{% endif %}"{% endif %}>4. Audit ballot</span>
</li> <span class="ui{% if menuindex >= 5 %} active{% endif %} item"{% if menuindex != 5 %} style="color: #767676;{% if menuindex > 5 %} font-weight: 400;{% endif %}"{% endif %}>5. Cast ballot</span>
{% endmacro %} </div>
<ul class="progress-tracker progress-tracker--word progress-tracker--word-center" id="election-tab-menu" style="margin-bottom: 20px;">
{{ menuitem(1, "Welcome") }}
{{ menuitem(2, "Select") }}
{{ menuitem(3, "Review") }}
{% if election.can_audit() %}
{{ menuitem(4, "Audit") }}
{{ menuitem(5, "Cast") }}
{{ menuitem(6, "Finish") }}
{% else %}
{{ menuitem(4, "Cast") }}
{{ menuitem(5, "Finish") }}
{% endif %}
</ul>
<div class="ui container"> <div class="ui container">
{% block helpbtn %}
<button class="tiny ui right floated labeled icon teal button" onclick="$('#modal-help').modal('show');" style="margin-bottom: 1em;"><i class="help circle icon"></i> Help</button>
{% endblock %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
<div class="ui container" style="margin-top: 1em;"> <div class="ui container" style="margin-top: 1em;">
<div style="clear: both;"></div>
{% block buttons %}{% endblock %} {% block buttons %}{% endblock %}
<div style="clear: both;"></div> <div style="clear: both;"></div>
</div> </div>
<div class="ui modal" id="modal-help">
<i class="close icon"></i>
<div class="header">
Help
</div>
<div class="content">
{% block help %}{% endblock %}
</div>
<div class="actions">
<button class="ui approve button">OK</button>
</div>
</div>
{% block after %}{% endblock %} {% block after %}{% endblock %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2021 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -20,7 +20,7 @@
{% block content %} {% block content %}
<div id="cast_prompt"> <div id="cast_prompt">
<p>Your vote has <span class="superem">not</span> yet been cast.{% if election.can_audit() %} Please make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64(true) }}</span>.{% endif %}</p> <p>Your vote has <span class="superem">not</span> yet been cast. If you have not already done so, please make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p>
<div class="ui negative message"> <div class="ui negative message">
<p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p> <p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p>
@ -53,125 +53,53 @@
<p id="error_unknown_tech"></p> <p id="error_unknown_tech"></p>
</div> </div>
</div> </div>
<div class="ui basic segment" id="casting" style="display: none; min-height: 4em;"> <div id="casting" style="display: none;">
<div class="ui active text loader">Casting your ballot. Please wait.</div> <div class="ui active text loader">Casting your ballot. Please wait.</div>
</div> </div>
{% endblock %} {% endblock %}
{% block buttons %} {% block buttons %}
{% if is_cast %}
<a class="ui left floated button" href="{{ election_base_url }}booth">Reset</a>
<button class="ui right floated primary button" id="cast_button" onclick="castBallot();"{% if not username %} style="display: none;"{% endif %}>Cast ballot</button>
{% else %}
<button class="ui left floated button" onclick="prevTemplate(2);">Back</button> <button class="ui left floated button" onclick="prevTemplate(2);">Back</button>
<button class="ui right floated primary button" id="cast_button" onclick="stageBallot(castBallot);"{% if not username %} style="display: none;"{% endif %}>Cast ballot</button> <button class="ui right floated primary button" id="cast_button" onclick="castBallot();"{% if not username %} style="display: none;"{% endif %}>Cast ballot</button>
{% endif %}
{% endblock %} {% endblock %}
{% block after %} {% block after %}
{% if election.can_audit() %}
<div class="ui tiny message" style="margin-top: 3em;">
<div class="header">Information for advanced users</div>
<p>Your full ballot fingerprint is <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p>
</div>
{% endif %}
<script> <script>
$(".message .close").on("click", function() { $(".message .close").on("click", function() {
$(this).closest(".message").addClass("hidden"); $(this).closest(".message").addClass("hidden");
}); });
function login(el) { function login(el) {
// Stage the vote for casting in case we change page
stageBallot(function(data) {
// Stage the next page in case we change page
stageNext("{{ election_base_url }}booth?cast", function(data) {
window.open(el.getAttribute("href"), "eos_login_window", "width=400,height=600"); window.open(el.getAttribute("href"), "eos_login_window", "width=400,height=600");
});
});
} }
function callback_complete(username) { function callback_complete(username) {
$("#cast_button").show(); $("#cast_button").show();
$("#booth_logged_in_as").text("You are currently logged in as " + username + "."); $("#booth_logged_in_as").text("You are currently logged in as " + username + ".");
// Ballot was staged when we clicked the login button
castBallot(); castBallot();
return true; return true;
} }
function stageBallot(callback) {
// Prepare ballot
var deauditedBallot = booth.ballot.deaudit();
$.ajax({
url: "{{ election_base_url }}stage_ballot",
type: "POST",
data: eosjs.eos.core.objects.EosObject.to_json({
"ballot": eosjs.eos.core.objects.EosObject.serialise_and_wrap(deauditedBallot, null),
"fingerprint": booth.fingerprint || null
}),
contentType: "application/json",
dataType: "text",
async: false // so window.open happens in main thread
})
.done(function(data) {
callback(data);
})
.fail(function(xhr, status, err) {
if (xhr.responseText && xhr.responseText.length < 100) {
$("#error_unknown_tech").text("Technical details: " + err + " – " + xhr.responseText);
} else {
$("#error_unknown_tech").text("Technical details: " + err);
}
$("#error_unknown").removeClass("hidden");
$("#error_invalid_id").addClass("hidden");
console.error(xhr);
throw err;
});
}
function stageNext(url, callback) {
$.ajax({
url: "/auth/stage_next",
type: "POST",
data: url,
contentType: "text/plain",
dataType: "text",
async: false // so window.open happens in main thread
})
.done(function(data) {
callback(data);
})
.fail(function(xhr, status, err) {
if (xhr.responseText && xhr.responseText.length < 100) {
$("#error_unknown_tech").text("Technical details: " + err + " – " + xhr.responseText);
} else {
$("#error_unknown_tech").text("Technical details: " + err);
}
$("#error_unknown").removeClass("hidden");
$("#error_invalid_id").addClass("hidden");
console.error(xhr);
throw err;
});
}
function castBallot() { function castBallot() {
$("#cast_prompt").hide(); $("#cast_prompt").hide();
$("#casting").show(); $("#casting").show();
// Prepare ballot
booth.ballot = booth.ballot.deaudit();
$.ajax({ $.ajax({
url: "{{ election_base_url }}cast_ballot", url: "{{ election_base_url }}cast_ballot",
type: "POST", type: "POST",
data: eosjs.eos.core.objects.__all__.EosObject.to_json({
"ballot": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(booth.ballot, null),
"fingerprint": booth.fingerprint || null
}),
contentType: "application/json",
dataType: "text" dataType: "text"
}) })
.done(function(data) { .done(function(data) {
response = eosjs.eos.core.objects.EosObject.from_json(data); response = eosjs.eos.core.objects.__all__.EosObject.from_json(data);
booth.voter = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(response.voter); booth.voter = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(response.voter);
booth.vote = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(response.vote); booth.vote = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(response.vote);
// Clear plaintexts // Clear plaintexts
booth.answers = null; booth.answers = null;
@ -201,16 +129,5 @@
throw err; throw err;
}); });
} }
{% if is_cast %}
castBallot();
{% endif %}
</script> </script>
{% endblock %} {% endblock %}
{% block help %}
<p>You are now ready to cast your ballot. If you disconnected your internet connection earlier, you must now reconnect it before proceeding.</p>
<p>Click the ‘OK’ button below to close this help screen, then click the blue ‘Cast ballot’ button. If there is no ‘Cast ballot’ button, select your login method from the list shown, and enter your voter details. These details may be found in the email or message instructing you to vote.</p>
<p>Once you have logged in, your vote will be automatically cast.</p>
<p>If you require further assistance, contact your election administrator.</p>
{% endblock %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2021 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -19,13 +19,13 @@
#} #}
{% block content %} {% block content %}
<div class="ui negative message" style="clear: both;"> <p>Your vote has <span class="superem">not</span> yet been cast.</p>
<div class="ui negative message">
<p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p> <p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p>
</div> </div>
{% if election.can_audit() %} <p>If you have not already done so, please make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>. Please retain a copy of your ballot fingerprint – you can use it to verify that your vote has been counted correctly. You may <a href="#" onclick="window.print();return false;">print this page</a> as a receipt if you wish.</p>
<p>Please make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64(true) }}</span>. Please retain a copy of your ballot fingerprint – you can use it to verify that your vote has been counted correctly. You may <a href="#" onclick="window.print();return false;">print this page</a> as a receipt if you wish.</p>
{% endif %}
<p>To continue, copy and paste the ballot below and provide it to the election administrator.</p> <p>To continue, copy and paste the ballot below and provide it to the election administrator.</p>
@ -39,19 +39,3 @@
{% block buttons %} {% block buttons %}
<button class="ui left floated button" onclick="prevTemplate();">Back</a> <button class="ui left floated button" onclick="prevTemplate();">Back</a>
{% endblock %} {% endblock %}
{% block after %}
{% if election.can_audit() %}
<div class="ui tiny message" style="margin-top: 3em;">
<div class="header">Information for advanced users</div>
<p>Your full ballot fingerprint is <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p>
</div>
{% endif %}
{% endblock %}
{% block help %}
<p>You are now ready to cast your ballot. As this is a pre-poll voting booth, you must manually provide your ballot to the election administrator.</p>
<p>Copy and paste the entire text of the ballot shown in the textbox into a message to the election administrator.</p>
<p>Click the ‘OK’ button below to close this help screen.</p>
<p>If you require further assistance, contact your election administrator.</p>
{% endblock %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2021 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -21,44 +21,17 @@
{% block content %} {% block content %}
<p>Your vote has been successfully cast. The following is your ‘smart ballot tracker’. Please retain a copy of your smart ballot tracker – you can use it to verify that your vote has been counted correctly. You may <a href="#" onclick="window.print();return false;">print this page</a> as a receipt if you wish.</p> <p>Your vote has been successfully cast. The following is your ‘smart ballot tracker’. Please retain a copy of your smart ballot tracker – you can use it to verify that your vote has been counted correctly. You may <a href="#" onclick="window.print();return false;">print this page</a> as a receipt if you wish.</p>
<div class="ui success icon message"> <div class="ui success message">
<i class="check circle outline icon"></i>
<div class="content">
<div class="header">Smart ballot tracker</div> <div class="header">Smart ballot tracker</div>
<p>This smart ballot tracker confirms that {{ voter.py_name }} cast a vote in the election {{ election.py_name }} at {{ vote.cast_at }}.</p> <p>This smart ballot tracker confirms that {{ voter.name }} cast a vote in the election {{ election.py_name }} at {{ vote.cast_at }}.</p>
{% if election.can_audit() %} <p>Ballot fingerprint: <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(vote.ballot).hash_as_b64() }}</span></p>
<p>Ballot fingerprint: <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(vote.ballot).hash_as_b64(true) }}</span></p>
{% endif %}
</div>
</div> </div>
{% if election.can_audit() %}
<p>Please check that the ballot fingerprint above matches the ballot fingerprint you recorded earlier.</p> <p>Please check that the ballot fingerprint above matches the ballot fingerprint you recorded earlier.</p>
<p>To confirm that your ballot was cast correctly, please go to the <a href="{{ election_base_url }}view/ballots">‘Voters and ballots’ page</a> for the {{ election.kind }} or click ‘Finish’, and confirm that the above ballot fingerprint appears next to your name.</p> <p>To confirm that your ballot was cast correctly, please go to the <a href="{{ election_base_url }}view/ballots">‘Voters and ballots’ page</a> for the {{ election.kind }} or click ‘Finish’, and confirm that the above ballot fingerprint appears next to your name.</p>
{% endif %}
{% endblock %} {% endblock %}
{% block buttons %} {% block buttons %}
{% if election.is_votes_public %}
<a href="{{ election_base_url }}view/ballots" class="ui right floated primary button">Finish</a> <a href="{{ election_base_url }}view/ballots" class="ui right floated primary button">Finish</a>
{% else %}
<a href="{{ election_base_url }}view" class="ui right floated primary button">Finish</a>
{% endif %}
{% endblock %}
{% block after %}
{% if election.can_audit() %}
<div class="ui tiny message" style="margin-top: 3em;">
<div class="header">Information for advanced users</div>
<p>Your full ballot fingerprint is <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(vote.ballot).hash_as_b64() }}</span>.</p>
</div>
{% endif %}
{% endblock %}
{% block help %}
<p>You have now successfully cast your ballot in this election!</p>
<p>The green ‘smart ballot tracker’ shows the details of your vote. Please retain a copy of your smart ballot tracker, as it can be used as proof that you voted in this election, and can be used to verify that your vote is counted corectly.</p>
<p>You may now close this window and exit the voting booth.</p>
<p>Click the ‘OK’ button below to close this help screen.</p>
<p>If you require further assistance, contact your election administrator.</p>
{% endblock %} {% endblock %}

View File

@ -1,8 +1,6 @@
{% extends templates['booth/base.html'] %}
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2019 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -18,41 +16,41 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
#} #}
{% block content %} <div class="ui active text loader">Preparing your ballot. Please wait.</div>
<div class="ui basic segment" style="min-height: 4em;">
<div class="ui active text loader">Preparing your ballot. Please wait.</div>
</div>
{% endblock %}
{% block after %} <script>
<script>
boothWorker.onmessage = function(msg) { boothWorker.onmessage = function(msg) {
try { try {
rawAnswers = []; rawAnswers = [];
for (var answer_json of booth.answers) { for (var answer_json of booth.answers) {
rawAnswers.push(eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(answer_json, null)); rawAnswers.push(eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(answer_json, null));
} }
encryptedAnswers = []; encryptedAnswers = [];
for (var encrypted_answer_json of msg.data.encrypted_answers) { for (var encrypted_answer_json of msg.data.encrypted_answers) {
encryptedAnswers.push(eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(encrypted_answer_json, null)); encryptedAnswers.push(eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(encrypted_answer_json, null));
} }
booth.ballot = eosjs.eos.base.election.Ballot(); booth.ballot = eosjs.eos.base.election.__all__.Ballot();
booth.ballot.answers = rawAnswers; booth.ballot.answers = rawAnswers;
booth.ballot.encrypted_answers = encryptedAnswers; booth.ballot.encrypted_answers = encryptedAnswers;
booth.ballot.election_id = election._id; booth.ballot.election_id = election._id;
booth.ballot.election_hash = eosjs.eos.core.hashing.SHA256().update_obj(election).hash_as_b64(); booth.ballot.election_hash = eosjs.eos.core.hashing.__all__.SHA256().update_obj(election).hash_as_b64();
if (should_do_fingerprint) { if (should_do_fingerprint) {
// String.prototype.join confuses fingerprintjs2 // String.prototype.join confuses fingerprintjs2
var strjoin = String.prototype.join; var strjoin = String.prototype.join;
String.prototype.join = undefined; String.prototype.join = undefined;
try {
new Fingerprint2().get(function(result, components) { new Fingerprint2().get(function(result, components) {
String.prototype.join = strjoin; String.prototype.join = strjoin;
booth.fingerprint = components; booth.fingerprint = components;
nextTemplate(); nextTemplate();
}); });
} catch (ex) {
console.error(ex);
nextTemplate();
}
} else { } else {
nextTemplate(); nextTemplate();
} }
@ -69,12 +67,11 @@
boothWorker.postMessage({ boothWorker.postMessage({
"action": "generateEncryptedVote", "action": "generateEncryptedVote",
"static_base_url": "{{ static_base_url }}", "static_base_url": "{{ static_base_url }}",
"election": eosjs.eos.core.objects.EosObject.serialise_and_wrap(election, null), "election": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(election, null),
"answers": booth.answers "answers": booth.answers
}); });
} catch (err) { } catch (err) {
boothError(err); boothError(err);
throw err; throw err;
} }
</script> </script>
{% endblock %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2021 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -30,12 +30,8 @@
{% include templates[selection_model_view_map[election.questions.__getitem__(loop.index0)._name]["selections_review"]] %} {% include templates[selection_model_view_map[election.questions.__getitem__(loop.index0)._name]["selections_review"]] %}
{% endfor %} {% endfor %}
{% if election.can_audit() %} <p>If you are happy with your selections, then make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p>
<p>If you are happy with your selections, then make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64(true) }}</span>.</p> <p>Click ‘Continue’, and you will be able to log into cast your vote.</p>
<p>Click ‘Continue’, and you will be able to log in to cast your vote.</p>
{% else %}
<p>If you are happy with your selections, then click ‘Continue’, and you will be able to log in to cast your vote.</p>
{% endif %}
{% endblock %} {% endblock %}
{% block buttons %} {% block buttons %}
@ -44,18 +40,6 @@
{% endblock %} {% endblock %}
{% block after %} {% block after %}
{% if election.can_audit() %} <div style="clear: both; margin-bottom: 1em;"></div>
<div class="ui tiny message" style="margin-top: 3em;"> <p><small>If you would like to audit your ballot, <a href="#" onclick="nextTemplate(1);">click here</a>. Auditing your ballot is an <b>optional</b> step you can take to check that your vote has been prepared correctly. You do not need to audit your ballot in order to cast a vote.</small></p>
<div class="header">Information for advanced users</div>
<p>Your ballot fingerprint is <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p>
<p>If you would like to audit your ballot, <a href="#" onclick="nextTemplate(1);">click here</a></p>
</div>
{% endif %}
{% endblock %}
{% block help %}
<p>This screen shows your selections for each of the questions in this election. Please carefully check your selections and ensure they are as you intended.</p>
<p>If there are any problems with your selections, they will be shown in red or orange.</p>
<p>Click the ‘OK’ button below to close this help screen, then click the blue ‘Continue’ button if you are satisfied with your selections, otherwise, click the ‘Back’ button.</p>
<p>If you require further assistance, contact your election administrator.</p>
{% endblock %} {% endblock %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2021 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -30,12 +30,8 @@
{% include templates[selection_model_view_map[election.questions.__getitem__(loop.index0)._name]["selections_review"]] %} {% include templates[selection_model_view_map[election.questions.__getitem__(loop.index0)._name]["selections_review"]] %}
{% endfor %} {% endfor %}
{% if election.can_audit() %} <p>If you are happy with your selections, then make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p>
<p>If you are happy with your selections, then make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(ballot).hash_as_b64(true) }}</span>.</p>
<p>Click ‘Continue’, and you will be able to copy your pre-poll ballot to provide to the election administrator.</p> <p>Click ‘Continue’, and you will be able to copy your pre-poll ballot to provide to the election administrator.</p>
{% else %}
<p>If you are happy with your selections, then click ‘Continue’, and you will be able to copy your pre-poll ballot to provide to the election administrator.</p>
{% endif %}
{% endblock %} {% endblock %}
{% block buttons %} {% block buttons %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2019 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -33,12 +33,7 @@
boothError("Question template unable to save selections"); boothError("Question template unable to save selections");
} }
function loadHelp() {
$("#modal-help .content").html($("#selections-make-help").html());
}
showTemplate(selection_model_view_map[election.questions.__getitem__(booth.questionNum)._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box"); showTemplate(selection_model_view_map[election.questions.__getitem__(booth.questionNum)._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
loadHelp();
function previousQuestion() { function previousQuestion() {
saveSelections(); saveSelections();
@ -47,20 +42,16 @@
} else { } else {
booth.questionNum--; booth.questionNum--;
showTemplate(selection_model_view_map[election.questions.__getitem__(booth.questionNum)._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box"); showTemplate(selection_model_view_map[election.questions.__getitem__(booth.questionNum)._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
loadHelp();
} }
} }
function nextQuestion() { function nextQuestion() {
if (!saveSelections()) { saveSelections();
return;
}
if (booth.questionNum == election.questions.__len__() - 1) { if (booth.questionNum == election.questions.__len__() - 1) {
nextTemplate(); nextTemplate();
} else { } else {
booth.questionNum++; booth.questionNum++;
showTemplate(selection_model_view_map[election.questions.__getitem__(booth.questionNum)._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box"); showTemplate(selection_model_view_map[election.questions.__getitem__(booth.questionNum)._name]["selections_make"], { "questionNum": booth.questionNum }, "#question-box");
loadHelp();
} }
} }
</script> </script>

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2019 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -18,26 +18,12 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
#} #}
{% block helpbtn %}
{# Don't float – we want it to overlap with the image #}
<div style="position: relative;">
<button class="tiny ui labeled icon teal button" style="position: absolute; right: 0; z-index: 1; margin: 0;" onclick="$('#modal-help').modal('show');"><i class="help circle icon"></i> Help</button>
</div>
{% endblock %}
{% block content %} {% block content %}
<img src="/static/img/logo.png" style="width: 100%; max-width: 500px; display: block; margin: 0 auto;">
<img src="/static/img/guide.png" style="width: 100%; max-width: 500px; display: block; margin: 0 auto; margin-top: 1em;">
{% endblock %}
{% block help %}
<p>Welcome to the {{ election.name }} voting booth.</p> <p>Welcome to the {{ election.name }} voting booth.</p>
<p>Follow the on-screen directions to prepare and cast your ballot in this {{ election.kind }}. The bar at the top of the page will show your progress. Please note that your ballot will <b>not be cast until</b> you complete the final ‘Cast ballot’ stage and receive a <b>‘smart ballot tracker’</b>.</p> <p>Follow the on-screen directions to prepare and cast your ballot in this {{ election.kind }}. The bar above will show your progress. Please note that your ballot will <b>not be cast until</b> you complete the final ‘Cast ballot’ stage and receive a <b>‘smart ballot tracker’</b>.</p>
<p>If at any point you wish to return to a previous screen, click the ‘Back’ button.</p> <p>If at any point you wish to return to a previous screen, click the ‘Back’ button below.</p>
{# <p>If you wish, you may disconnect your internet connection now while preparing your ballot, however you must re-connect your internet connection before logging in to cast your ballot.</p> #} <p>If you wish, you may disconnect your internet connection now while preparing your ballot, however you must re-connect your internet connection before logging in to cast your ballot.</p>
<p>Click the ‘OK’ button below to close this help screen, then click the blue ‘Continue’ button to continue to the next step.</p> <p>Please click the blue ‘Continue’ button below to continue.</p>
<p>If you require further assistance, contact your election administrator.</p>
{% endblock %} {% endblock %}
{% block buttons %} {% block buttons %}

View File

@ -1,6 +1,6 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2019 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -18,55 +18,18 @@
<h2>{{ questionNum + 1 }}. {{ election.questions.__getitem__(questionNum).prompt }}</h2> <h2>{{ questionNum + 1 }}. {{ election.questions.__getitem__(questionNum).prompt }}</h2>
{% if election.questions.__getitem__(questionNum).description %} <p><small>Vote for between {{ election.questions.__getitem__(questionNum).min_choices }} and {{ election.questions.__getitem__(questionNum).max_choices }} choices. Click the check-boxes to the left of the choices to make your selection, then click the ‘Continue’ button. If you make a mistake, click the check-boxes again to clear your selection.</small></p>
<p>{{ election.questions.__getitem__(questionNum).description | urlize | replace('<a ', '<a target="_blank" ') | safe }}</p>
{% endif %}
<p><small>
Vote for
{% if election.questions.__getitem__(questionNum).min_choices == election.questions.__getitem__(questionNum).max_choices %}
exactly {{ election.questions.__getitem__(questionNum).min_choices }}
{% elif election.questions.__getitem__(questionNum).min_choices == election.questions.__getitem__(questionNum).max_choices - 1 %}
either {{ election.questions.__getitem__(questionNum).min_choices }} or {{ election.questions.__getitem__(questionNum).max_choices }}
{% else %}
between {{ election.questions.__getitem__(questionNum).min_choices }} and {{ election.questions.__getitem__(questionNum).max_choices }}
{% endif %}
choices.
{% if is_radio %}
Click the radio buttons to the left of the choices to make your selection, then click the ‘Continue button’. If you make a mistake, click another radio button to change your selection.
{% else %}
Click the check-boxes to the left of the choices to make your selection, then click the ‘Continue’ button. If you make a mistake, click the check-boxes again to clear your selection.
{% endif %}
</small></p>
{% set is_radio = election.questions.__getitem__(questionNum).max_choices == 1 %}
<div id="question-choices" class="ui form" style="margin-bottom: 1em;"> <div id="question-choices" class="ui form" style="margin-bottom: 1em;">
<div class="grouped fields"> <div class="grouped fields">
{% for choice in election.questions.__getitem__(questionNum).randomised_choices().impl %} {% for choice in election.questions.__getitem__(questionNum).randomised_choices().impl %}
<div class="field"> <div class="field">
<div class="ui{% if is_radio %} radio{% endif %} checkbox"> <div class="ui checkbox">
<input <input type="checkbox" id="question-choice-{{ election.questions.__getitem__(questionNum).choices.impl.indexOf(choice) }}" onchange="choicesChanged();">
{% if is_radio %}
name="question-choices"
type="radio"
{% else %}
type="checkbox"
{% endif %}
id="question-choice-{{ election.questions.__getitem__(questionNum).choices.impl.indexOf(choice) }}" onchange="choicesChanged();">
<label for="question-choice-{{ election.questions.__getitem__(questionNum).choices.impl.indexOf(choice) }}">{{ choice.name }}{% if choice.party %} – {{ choice.party }}{% endif %}</label> <label for="question-choice-{{ election.questions.__getitem__(questionNum).choices.impl.indexOf(choice) }}">{{ choice.name }}{% if choice.party %} – {{ choice.party }}{% endif %}</label>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
{% if is_radio and election.questions.__getitem__(questionNum).min_choices == 0 %}
<div class="field">
<div class="ui{% if is_radio %} radio{% endif %} checkbox">
<input name="question-choices" type="radio" id="question-choice-none" onchange="choicesChanged();" checked>
<label for="question-choice-none"><i>None of the above</i></label>
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
@ -76,7 +39,6 @@
<script> <script>
function choicesChanged() { function choicesChanged() {
{% if not is_radio %}
var numChoices = $("#question-choices input:checked").length; var numChoices = $("#question-choices input:checked").length;
if (numChoices >= election.questions.__getitem__(booth.questionNum).max_choices) { if (numChoices >= election.questions.__getitem__(booth.questionNum).max_choices) {
// Prevent making any more selections // Prevent making any more selections
@ -86,7 +48,6 @@
$("#question-choices input").prop("disabled", false); $("#question-choices input").prop("disabled", false);
$("#message-max-choices").addClass("hidden"); $("#message-max-choices").addClass("hidden");
} }
{% endif %}
} }
// Fill in ballot with previous selections // Fill in ballot with previous selections
@ -100,40 +61,9 @@
function saveSelections() { function saveSelections() {
selections = []; selections = [];
$("#question-choices input:checked").each(function(i, el) { $("#question-choices input:checked").each(function(i, el) {
if (el.id !== "question-choice-none") {
selections.push(parseInt(el.id.substring("question-choice-".length))); selections.push(parseInt(el.id.substring("question-choice-".length)));
}
}); });
answer = eosjs.eos.base.election.__all__.ApprovalAnswer(eosjs.__kwargtrans__({choices: selections}));
if (selections.length < election.questions.__getitem__(booth.questionNum).min_choices) { booth.answers[booth.questionNum] = eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(answer);
if (!window.confirm('You have selected fewer than the minimum required number of choices. If you proceed to cast this ballot, it will **NOT** be counted. If this was not your intention, please click the "Cancel" button below now, and correct your selections.')) {
return false;
}
}
answer = eosjs.eos.base.election.ApprovalAnswer(eosjs.__kwargtrans__({choices: selections}));
booth.answers[booth.questionNum] = eosjs.eos.core.objects.EosObject.serialise_and_wrap(answer);
return true;
} }
</script> </script>
<div id="selections-make-help">
<p>{% if is_radio %}This is a vote-for-one question.{% else %}This is an approval voting question.{% endif %} You are required to vote for
{% if election.questions.__getitem__(questionNum).min_choices == election.questions.__getitem__(questionNum).max_choices %}
exactly {{ election.questions.__getitem__(questionNum).min_choices }}
{% elif election.questions.__getitem__(questionNum).min_choices == election.questions.__getitem__(questionNum).max_choices - 1 %}
either {{ election.questions.__getitem__(questionNum).min_choices }} or {{ election.questions.__getitem__(questionNum).max_choices }}
{% else %}
between {{ election.questions.__getitem__(questionNum).min_choices }} and {{ election.questions.__getitem__(questionNum).max_choices }}
{% endif %}
choices.</p>
{% if is_radio %}
<p>Click or tap the radio buttons to the left of the choices to make your selection. If you make a mistake, click or tap another radio button to change your selection.</p>
{% else %}
<p>Click or tap the check-boxes to the left of the choices to make your selection. If you make a mistake, click or tap the check-boxes again to clear your selection.</p>
{% endif %}
<p>Once you are satisfied with your selections, click the ‘Continue’ button.</p>
<p>Click the ‘OK’ button below to close this help screen.</p>
<p>If you require further assistance, contact your election administrator.</p>
</div>

View File

@ -30,11 +30,7 @@
{% endfor %} {% endfor %}
</div> </div>
{% if booth.answers[loop.index0].value.choices.length < question.min_choices %} {% if booth.answers[loop.index0].value.choices.length < question.max_choices %}
<div class="ui error message">
<p>You have selected fewer than the minimum required number of choices. If you proceed to cast this ballot, it will <span class="superem">not</span> be counted. If this was not your intention, please click the ‘Back’ button below now, and correct your selections.</p>
</div>
{% elif booth.answers[loop.index0].value.choices.length < question.max_choices %}
<div class="ui warning message"> <div class="ui warning message">
<p>You have selected fewer than the maximum allowed number of choices. If this was not your intention, please click the ‘Back’ button below now, and correct your selections.</p> <p>You have selected fewer than the maximum allowed number of choices. If this was not your intention, please click the ‘Back’ button below now, and correct your selections.</p>
</div> </div>

View File

@ -1,6 +1,6 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2019 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -18,21 +18,7 @@
<h2>{{ questionNum + 1 }}. {{ election.questions.__getitem__(questionNum).prompt }}</h2> <h2>{{ questionNum + 1 }}. {{ election.questions.__getitem__(questionNum).prompt }}</h2>
{% if election.questions.__getitem__(questionNum).description %} <p><small>Vote for between {{ election.questions.__getitem__(questionNum).min_choices }} and {{ election.questions.__getitem__(questionNum).max_choices }} choices. Click and drag the choices from the grey box to the blue box in order from most-preferred to least-preferred. It is in your best interests to vote for as many choices as you can.</small></p>
<p>{{ election.questions.__getitem__(questionNum).description | urlize | replace('<a ', '<a target="_blank" ') | safe }}</p>
{% endif %}
<p><small>
Vote for
{% if election.questions.__getitem__(questionNum).min_choices == election.questions.__getitem__(questionNum).max_choices %}
exactly {{ election.questions.__getitem__(questionNum).min_choices }}
{% elif election.questions.__getitem__(questionNum).min_choices == election.questions.__getitem__(questionNum).max_choices - 1 %}
either {{ election.questions.__getitem__(questionNum).min_choices }} or {{ election.questions.__getitem__(questionNum).max_choices }}
{% else %}
between {{ election.questions.__getitem__(questionNum).min_choices }} and {{ election.questions.__getitem__(questionNum).max_choices }}
{% endif %}
choices. Click and drag the choices from the grey box to the blue box in order from most-preferred to least-preferred. It is in your best interests to vote for as many choices as you can.
</small></p>
<div id="question-choices-selected" class="preferential-choices"> <div id="question-choices-selected" class="preferential-choices">
<div style="color: #3465a4;">Options voted for:</div> <div style="color: #3465a4;">Options voted for:</div>
@ -52,14 +38,7 @@
{% macro printchoice(choice, ticket=None) %} {% macro printchoice(choice, ticket=None) %}
<div class="preferential-choice" data-choiceno="{{ flat_choices.indexOf(choice) }}"> <div class="preferential-choice" data-choiceno="{{ flat_choices.indexOf(choice) }}">
<div class="number"> <div class="number"></div>
<select class="ui dropdown">
<option selected></option>
{% for i in range(flat_choices|length) %}
<option>{{ i + 1 }}</option>
{% endfor %}
</select>
</div>
<div class="content"> <div class="content">
<div class="candidate-name">{{ choice.name }}</div> <div class="candidate-name">{{ choice.name }}</div>
{% if choice.party %} {% if choice.party %}
@ -83,14 +62,7 @@
{% if choice.choices %} {% if choice.choices %}
{# Ticket #} {# Ticket #}
<div class="preferential-choice ticket"> <div class="preferential-choice ticket">
<div class="number"> <div class="number"></div>
<select class="ui dropdown">
<option selected></option>
{% for i in range(flat_choices|length) %}
<option>{{ i + 1 }}</option>
{% endfor %}
</select>
</div>
<div class="content"> <div class="content">
<div class="party-name">{{ choice.name }}</div> <div class="party-name">{{ choice.name }}</div>
<div class="ticket-choices"> <div class="ticket-choices">
@ -113,18 +85,18 @@
function choicesChanged() { function choicesChanged() {
// Recalculate numbers // Recalculate numbers
$(".preferential-choices .preferential-choice .number select").val(""); $(".preferential-choices .preferential-choice .number").each(function(i, el) {
var selectedChoices = $("#question-choices-selected .dragarea > .preferential-choice > .number select"); $(el).text("");
selectedChoices.each(function(i, el) {
$(el).val(i + 1);
}); });
var selectedCandidates = $("#question-choices-selected .preferential-choice:not(.ticket) .number select"); var selectedChoices = $("#question-choices-selected .dragarea > .preferential-choice > .number");
selectedChoices.each(function(i, el) {
$(el).text(i + 1);
});
var selectedCandidates = $("#question-choices-selected .preferential-choice:not(.ticket) .number");
if (selectedCandidates.length >= election.questions.__getitem__(booth.questionNum).max_choices) { if (selectedCandidates.length >= election.questions.__getitem__(booth.questionNum).max_choices) {
// Prevent making any more selections // Prevent making any more selections
allowAdding = false; allowAdding = false;
$("#question-choices-remaining .preferential-choice .number select").prop("disabled", true);
if (selectedCandidates.length > election.questions.__getitem__(booth.questionNum).max_choices) { if (selectedCandidates.length > election.questions.__getitem__(booth.questionNum).max_choices) {
// Prevent progression // Prevent progression
$(".primary.button").addClass("disabled"); $(".primary.button").addClass("disabled");
@ -135,12 +107,9 @@
} }
} else { } else {
allowAdding = true; allowAdding = true;
$("#question-choices-remaining .preferential-choice .number select").prop("disabled", false);
$(".primary.button").removeClass("disabled");
$("#message-max-choices").addClass("hidden"); $("#message-max-choices").addClass("hidden");
$("#message-too-many-choices").addClass("hidden"); $("#message-too-many-choices").addClass("hidden");
$(".primary.button").removeClass("disabled");
} }
} }
@ -151,10 +120,6 @@
choicesChanged(); choicesChanged();
} }
// =============
// DRAG AND DROP
// =============
var dragulaChoices = dragula( var dragulaChoices = dragula(
[document.querySelector("#question-choices-selected .dragarea"), document.querySelector("#question-choices-remaining .dragarea")].concat([].slice.apply(document.querySelectorAll(".ticket-choices"))), [document.querySelector("#question-choices-selected .dragarea"), document.querySelector("#question-choices-remaining .dragarea")].concat([].slice.apply(document.querySelectorAll(".ticket-choices"))),
{ {
@ -172,111 +137,32 @@
); );
function breakTicket(ticket) { function breakTicket(ticket) {
ticket.find(".ticket-choices").first().children(".preferential-choice").detach().insertAfter(ticket); ticket.find(".ticket-choices .preferential-choice").each(function(i, el) {
$(el).detach().insertAfter(ticket);
});
ticket.remove(); ticket.remove();
} }
dragulaChoices.on("drop", function(el, target, source, sibling) { dragulaChoices.on("drop", function(el, target, source, sibling) {
// If the source or target is a ticket, break the ticket if necessary // If the source or target is a ticket, break the ticket
if ($(source).parents(".ticket").length > 0) { if ($(source).parents(".ticket").length > 0) {
// This is a candidate dragged out of a ticket – break the ticket breakTicket($(source).parents(".ticket").first())
breakTicket($(source).parents(".ticket").first());
} }
if ($(target).parents(".ticket").length > 0) { if ($(target).parents(".ticket").length > 0) {
// This is a candidate/ticket dragged into a .ticket-choices breakTicket($(target).parents(".ticket").first())
// Was it dragged to the first or last position?
if (sibling == null) {
// Dragged to the end – just move it away
$(el).detach().insertAfter($(target).parents(".ticket").first());
} else if(sibling == $(target).children(".preferential-choice:not(.ticket)").first().get(0)) {
// Dragged to the beginning – just move it away
$(el).detach().insertBefore($(target).parents(".ticket").first());
} else {
// Dragged into the middle – break the ticket
breakTicket($(target).parents(".ticket").first());
}
} }
choicesChanged(); choicesChanged();
}); });
// ===============
// DROP DOWN BOXES
// ===============
$(".preferential-choices .preferential-choice .number select").change(function(evt) {
var index = parseInt($(this).val());
var choiceEl = $(this).parents(".preferential-choice").first();
var ticket = null;
// If the source is within a ticket, break the ticket afterwards
// (It is impossible for the target to be within a ticket)
if (choiceEl.parents(".ticket").length > 0) {
ticket = choiceEl.parents(".ticket").first();
}
// Simulate drag-and-drop
var selectedChoices = $("#question-choices-selected .dragarea > .preferential-choice");
if ($(this).val() === "") {
// Deselect
choiceEl.detach().appendTo($("#question-choices-remaining .dragarea"));
} else if (index > selectedChoices.length) {
// Append to end
choiceEl.detach().appendTo($("#question-choices-selected .dragarea"));
} else {
// Insert in the middle
choiceEl.detach().insertBefore(selectedChoices[index - 1]);
}
// Now break the ticket
if (ticket !== null) {
breakTicket(ticket);
}
choicesChanged();
});
// =======
// GENERAL
// =======
function saveSelections() { function saveSelections() {
selections = []; selections = [];
$("#question-choices-selected .preferential-choice:not(.ticket)").each(function(i, el) { $("#question-choices-selected .preferential-choice:not(.ticket)").each(function(i, el) {
selections.push(parseInt(el.dataset.choiceno)); selections.push(parseInt(el.dataset.choiceno));
}); });
answer = eosjs.eos.base.election.__all__.PreferentialAnswer(eosjs.__kwargtrans__({choices: selections}));
if (selections.length < election.questions.__getitem__(booth.questionNum).min_choices) { booth.answers[booth.questionNum] = eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(answer);
if (!window.confirm('You have selected fewer than the minimum required number of choices. If you proceed to cast this ballot, it will **NOT** be counted. If this was not your intention, please click the "Cancel" button below now, and correct your selections.')) {
return false;
}
}
answer = eosjs.eos.base.election.PreferentialAnswer(eosjs.__kwargtrans__({choices: selections}));
booth.answers[booth.questionNum] = eosjs.eos.core.objects.EosObject.serialise_and_wrap(answer);
booth.q_state[booth.questionNum] = [$("#question-choices-selected .dragarea").html(), $("#question-choices-remaining .dragarea").html()]; // wew lad booth.q_state[booth.questionNum] = [$("#question-choices-selected .dragarea").html(), $("#question-choices-remaining .dragarea").html()]; // wew lad
return true;
} }
</script> </script>
<div id="selections-make-help">
<p>This is a preferential voting question. You are required to vote for
{% if election.questions.__getitem__(questionNum).min_choices == election.questions.__getitem__(questionNum).max_choices %}
exactly {{ election.questions.__getitem__(questionNum).min_choices }}
{% elif election.questions.__getitem__(questionNum).min_choices == election.questions.__getitem__(questionNum).max_choices - 1 %}
either {{ election.questions.__getitem__(questionNum).min_choices }} or {{ election.questions.__getitem__(questionNum).max_choices }}
{% else %}
between {{ election.questions.__getitem__(questionNum).min_choices }} and {{ election.questions.__getitem__(questionNum).max_choices }}
{% endif %}
choices.</p>
<p>If using a desktop computer or a laptop, use your mouse to click and drag the choices from the grey box to the blue box.</p>
<p>If using a smartphone or tablet, use your finger to drag the choices from the grey box to the blue box.</p>
<p>You should order your choices inside the blue box in order from most-preferred (at the top) to least-preferred (at the bottom).</p>
<p>It is in your best interests to vote for as many choices as you can.</p>
<p>Once you are satisfied with your selections, click the ‘Continue’ button.</p>
<p>Click the ‘OK’ button below to close this help screen.</p>
<p>If you require further assistance, contact your election administrator.</p>
</div>

View File

@ -35,11 +35,7 @@
</div> </div>
{% endif %} {% endif %}
{% if booth.answers[loop.index0].value.choices.length < question.min_choices %} {% if booth.answers[loop.index0].value.choices.length < question.max_choices %}
<div class="ui error message">
<p>You have selected fewer than the minimum required number of choices. If you proceed to cast this ballot, it will <span class="superem">not</span> be counted. If this was not your intention, please click the ‘Back’ button below now, and correct your selections.</p>
</div>
{% elif booth.answers[loop.index0].value.choices.length < question.max_choices %}
<div class="ui warning message"> <div class="ui warning message">
<p>You have selected fewer than the maximum allowed number of choices. If this was not your intention, please click the ‘Back’ button below now, and correct your selections.</p> <p>You have selected fewer than the maximum allowed number of choices. If this was not your intention, please click the ‘Back’ button below now, and correct your selections.</p>
</div> </div>

View File

@ -1,27 +0,0 @@
# Eos - Verifiable elections
# Copyright © 2017-18 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/>.
from . import emails
from eos.core.tasks import *
from eos.base.election import *
class WebTask(Task):
def error(self):
emails.task_email_failure(self)
class WorkflowTaskEntryWebTask(WorkflowTaskEntryTask, WebTask):
pass

View File

@ -1,6 +1,6 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -26,26 +26,26 @@
<div class="menu"> <div class="menu">
<div class="header">Active tasks</div> <div class="header">Active tasks</div>
{% for task in eos.core.tasks.TaskScheduler.active_tasks() %} {% for task in eos.core.tasks.TaskScheduler.active_tasks() %}
<a class="item" href="{{ url_for('task_view', task_id=task._id) }}"> <div class="item">
{{ task.label }} {{ task.label }}
<br><small><i class="wait icon"></i> started {{ task.started_at|pretty_date }}</small> <br><small><i class="wait icon"></i> started {{ task.started_at|pretty_date }}</small>
</a> </div>
{% endfor %} {% endfor %}
<div class="divider"></div> <div class="divider"></div>
<div class="header">Pending tasks</div> <div class="header">Pending tasks</div>
{% for task in eos.core.tasks.TaskScheduler.pending_tasks() %} {% for task in eos.core.tasks.TaskScheduler.pending_tasks() %}
<a class="item" href="{{ url_for('task_view', task_id=task._id) }}"> <div class="item">
{{ task.label }} {{ task.label }}
<br><small><i class="wait icon"></i> due {{ task.run_at|pretty_date }}</small> <br><small><i class="wait icon"></i> due {{ task.run_at|pretty_date }}</small>
</a> </div>
{% endfor %} {% endfor %}
<div class="divider"></div> <div class="divider"></div>
<div class="header">Recently completed tasks</div> <div class="header">Recently completed tasks</div>
{% for task in eos.core.tasks.TaskScheduler.completed_tasks(3) %} {% for task in eos.core.tasks.TaskScheduler.completed_tasks(3) %}
<a class="item" href="{{ url_for('task_view', task_id=task._id) }}"> <div class="item">
{% if task.status.is_error() %}<i class="warning sign icon"></i> {% endif %}{{ task.label }} {% if task.status < 0 %}<i class="warning sign icon"></i> {% endif %}{{ task.label }}
<br><small><i class="wait icon"></i> completed {{ task.completed_at|pretty_date }}</small> <br><small><i class="wait icon"></i> completed {{ task.completed_at|pretty_date }}</small>
</a> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -31,12 +31,6 @@
<li><a href="/auth/{{ auth_method[0] }}/login" target="_blank" onclick="login(this);return false;">{{ auth_method[1] }}</a></li> <li><a href="/auth/{{ auth_method[0] }}/login" target="_blank" onclick="login(this);return false;">{{ auth_method[1] }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
<div class="ui hidden error message">
<div class="header">Error logging in</div>
<p>Your log in details appear to be correct, however there was an unknown error while logging you in.</p>
<p>Please try again. If the problem persists, contact your election administrator.</p>
</div>
{% endblock %} {% endblock %}
{% block basecontent %} {% block basecontent %}
@ -46,8 +40,8 @@
window.open(el.getAttribute("href"), "eos_login_window", "width=400,height=600"); window.open(el.getAttribute("href"), "eos_login_window", "width=400,height=600");
} }
function callback_complete() { function callback_complete(name) {
window.location = "{{ url_for('login_callback') }}"; window.location = "/";
return true; return true;
} }
</script> </script>

View File

@ -23,46 +23,33 @@
{% block basecontent %} {% block basecontent %}
<div class="ui middle aligned center aligned grid" style="height: 100%;"> <div class="ui middle aligned center aligned grid" style="height: 100%;">
<div class="column" style="max-width: 400px;"> <div class="column" style="max-width: 400px;">
<div class="ui hidden success message" id="success-popup"> <div class="ui hidden success message">
<div class="header">Log in successful</div> <div class="header">Log in successful</div>
<p>You have successfully logged in to your account.</p> <p>You have successfully logged in to your account.</p>
<p>You may now close this window and return to your previous page.</p> <p>You may now close this window and return to your previous page.</p>
</div> </div>
<div class="ui hidden success message" id="success-redirect">
<div class="header">Log in successful</div>
<p>You have successfully logged in to your account.</p>
<p>Please wait, you should be returned to your previous page momentarily.</p>
</div>
<div class="ui hidden error message"> <div class="ui hidden error message">
<div class="header">Error logging in</div> <div class="header">Error logging in</div>
<p>Your log in details appear to be correct, however there was an unknown error while logging you in.</p> <p>Your log in details appear to be correct, however there was an unknown error while logging you in.</p>
<p>Please close this window and try again. If the problem persists, contact your election administrator.</p> <p>Please close this window try again. If the problem persists, contact your election administrator.</p>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
if (window.opener && window.opener.callback_complete) {
// Normal popup window
// Redirect happens in the opener
try { try {
var result = window.opener.callback_complete("{{ session.user.name }}"); var result = window.opener.callback_complete("{{ session.user.name }}");
if (result) { if (result) {
$("#success-popup").removeClass("hidden"); $(".success.message").removeClass("hidden");
setTimeout(window.close, 1000);
window.setTimeout(function() {
window.close();
}, 1000);
} else { } else {
$(".error.message").removeClass("hidden"); $(".error.message").removeClass("hidden");
} }
} catch (ex) { } catch (ex) {
$(".error.message").removeClass("hidden"); $(".error.message").removeClass("hidden");
console.error(ex);
throw ex;
}
} else {
// This browser does not support popups
// Manually redirect in this window
$("#success-redirect").removeClass("hidden");
window.location = "{{ url_for('login_callback') }}";
} }
</script> </script>
{% endblock %} {% endblock %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -29,7 +29,7 @@
<a href="https://github.com/RunasSudo/Eos" class="item">Source Code</a> <a href="https://github.com/RunasSudo/Eos" class="item">Source Code</a>
{% if session.user %} {% if session.user %}
{% if session.user.is_admin() %} {% if session.user.is_admin() %}
{% include 'task/active_tasks_menu.html' %} {% include 'active_tasks_menu.html' %}
{% endif %} {% endif %}
<div class="ui simple dropdown item right"> <div class="ui simple dropdown item right">
<i class="{% if session.user.is_admin() %}legal{% else %}user circle{% endif %} icon"></i> {{ session.user.name }} <i class="dropdown icon"></i> <i class="{% if session.user.is_admin() %}legal{% else %}user circle{% endif %} icon"></i> {{ session.user.name }} <i class="dropdown icon"></i>
@ -42,7 +42,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="ui main text container" id="main_container" {% block mainContainerOpts %}{% endblock %}> <div class="ui main text container" id="main_container">
{% block content %} {% block content %}
{% endblock content %} {% endblock content %}
</div> </div>

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -23,7 +23,7 @@
<ul> <ul>
{% for task in election.workflow.tasks %} {% for task in election.workflow.tasks %}
{% if task.status == eos.base.workflow.WorkflowTaskStatus.READY %} {% if task.status == eos.base.workflow.WorkflowTask.Status.READY %}
<li><a href="{{ url_for('election_admin_enter_task', election_id=election._id, task_name=task._name) }}" onclick="return window.confirm('Are you sure you want to execute the task \'{{ task.label }}\'? This action is irreversible.');">{{ task.label }}</a></li> <li><a href="{{ url_for('election_admin_enter_task', election_id=election._id, task_name=task._name) }}" onclick="return window.confirm('Are you sure you want to execute the task \'{{ task.label }}\'? This action is irreversible.');">{{ task.label }}</a></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -57,7 +57,7 @@
$("#election-tab-menu .item").removeClass("active"); $("#election-tab-menu .item").removeClass("active");
linkEl.addClass("active"); linkEl.addClass("active");
$("#election-tab-content").html('<div class="ui basic segment" style="min-height: 8em;"><div class="ui active text loader">Loading. Please wait.</div></div>'); $("#election-tab-content").html('<div class="ui active text loader">Loading. Please wait.</div>');
$("#election-tab-content").load(linkEl.attr("href") + " #election-tab-content", function() { $("#election-tab-content").load(linkEl.attr("href") + " #election-tab-content", function() {
linkEl.find(".loader").removeClass("active"); linkEl.find(".loader").removeClass("active");

View File

@ -1,6 +1,6 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2021 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -19,11 +19,5 @@
{% block tabs %} {% block tabs %}
{{ tab('Overview', 'election_view') }} {{ tab('Overview', 'election_view') }}
{{ tab('Questions', 'election_view_questions') }} {{ tab('Questions', 'election_view_questions') }}
{% if election.is_voters_public or (session.user and session.user.is_admin()) %}
{% if election.is_votes_public or (session.user and session.user.is_admin()) %}
{{ tab('Voters and ballots', 'election_view_ballots') }} {{ tab('Voters and ballots', 'election_view_ballots') }}
{% else %}
{{ tab('Voters', 'election_view_ballots') }}
{% endif %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2021 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -19,30 +19,21 @@
#} #}
{% block electioncontent %} {% block electioncontent %}
<table class="ui selectable celled table"> <table class="ui celled table">
<thead> <thead>
<tr> <tr>
<th>Voter</th> <th>Voter</th>
{% if election.is_votes_public or (session.user and session.user.is_admin()) %}
<th>Ballot fingerprint</th> <th>Ballot fingerprint</th>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for voter in election.voters %} {% for voter in election.voters %}
<tr> <tr>
{% if election.is_votes_public or (session.user and session.user.is_admin()) %}
<td class="selectable"><a href="{{ url_for('election_voter_view', election_id=election._id, voter_id=voter._id) }}">{{ voter.name }}</a></td>
{% set votes = voter.votes.get_all() %}
<td class="selectable"><a href="{{ url_for('election_voter_view', election_id=election._id, voter_id=voter._id) }}">
{% if votes|length > 0 %}
<span class="hash">{{ SHA256().update_obj(votes[-1].ballot).hash_as_b64(True) }}</span>
{% else %}
&nbsp;
{% endif %}
</a></td>
{% else %}
<td>{{ voter.name }}</td> <td>{{ voter.name }}</td>
{% if voter.votes|length > 0 %}
<td class="hash">{{ SHA256().update_obj(voter.votes[-1].ballot).hash_as_b64() }}</td>
{% else %}
<td class="hash"></td>
{% endif %} {% endif %}
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2021 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -23,7 +23,6 @@
{% block head %} {% block head %}
{{ super() }} {{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='bower_components/dragula.js/dist/dragula.min.css') }}" type="text/css"> <link rel="stylesheet" href="{{ url_for('static', filename='bower_components/dragula.js/dist/dragula.min.css') }}" type="text/css">
<link rel="stylesheet" href="{{ url_for('static', filename='bower_components/progress-tracker/app/styles/progress-tracker.css') }}" type="text/css">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -44,8 +43,6 @@
{% endif %} {% endif %}
<script> <script>
var eosjs = require("eosjs");
var templates = {}; var templates = {};
var election = null; var election = null;
var booth = null; var booth = null;
@ -69,18 +66,10 @@
resetBooth(); resetBooth();
function loadElection() { function loadElection() {
// Verify booth
if (should_do_fingerprint) {
if (typeof Fingerprint2 === 'undefined') {
boothError('Your browser did not load fingerprintjs2 correctly. Please try again after disabling your ad blockers and similar software. If the issue persists, try using a different browser.');
return;
}
}
$.ajax({ url: "{{ url_for('election_api_json', election_id=election._id) }}", dataType: "text" }) $.ajax({ url: "{{ url_for('election_api_json', election_id=election._id) }}", dataType: "text" })
.done(function(data) { .done(function(data) {
try { try {
election = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.EosObject.from_json(data), null); election = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.__all__.EosObject.from_json(data), null);
boothWorker = new Worker("{{ url_for('static', filename='js/booth_worker.js') }}"); boothWorker = new Worker("{{ url_for('static', filename='js/booth_worker.js') }}");
@ -119,7 +108,7 @@
}) })
.done(function(data) { .done(function(data) {
try { try {
templates[templateUrl] = nunjucks.compile(data, null, templateUrl); templates[templateUrl] = nunjucks.compile(data);
numTemplatesLoaded += 1; numTemplatesLoaded += 1;
if (numTemplatesLoaded == templateUrls.length) { if (numTemplatesLoaded == templateUrls.length) {
// All templates loaded. Show voting booth // All templates loaded. Show voting booth
@ -202,24 +191,19 @@
// === BOOTH TASKS === // === BOOTH TASKS ===
// TODO: Make modular // TODO: Make modular
templates['booth/base.html'] = null;
if (location.search.indexOf('?cast') < 0) {
// Normal booth
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
showTemplate('booth/welcome.html'); showTemplate('booth/welcome.html');
} }
}); });
templates['booth/base.html'] = null;
templates['booth/welcome.html'] = null; templates['booth/welcome.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
showTemplate('booth/selections.html'); showTemplate('booth/selections.html');
} }
}); });
templates['booth/selections.html'] = null; templates['booth/selections.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
if (fromLeft) { if (fromLeft) {
@ -239,14 +223,12 @@
} }
}); });
templates['booth/review_prepoll.html'] = null; templates['booth/review_prepoll.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
showTemplate('booth/audit.html', {ballot: booth.ballot}); showTemplate('booth/audit.html', {ballot: booth.ballot});
} }
}); });
templates['booth/audit.html'] = null; templates['booth/audit.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
showTemplate('booth/cast_prepoll.html', {ballot: booth.ballot}); showTemplate('booth/cast_prepoll.html', {ballot: booth.ballot});
@ -261,40 +243,18 @@
} }
}); });
templates['booth/review.html'] = null; templates['booth/review.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
showTemplate('booth/audit.html', {ballot: booth.ballot}); showTemplate('booth/audit.html', {ballot: booth.ballot});
} }
}); });
templates['booth/audit.html'] = null; templates['booth/audit.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
showTemplate('booth/cast.html', {ballot: booth.ballot, is_cast: false}); showTemplate('booth/cast.html', {ballot: booth.ballot});
} }
}); });
templates['booth/cast.html'] = null; templates['booth/cast.html'] = null;
boothTasks.append({
activate: function(fromLeft) {
showTemplate('booth/complete.html', {voter: booth.voter, vote: booth.vote});
}
});
templates['booth/complete.html'] = null;
}
} else {
// Cast immediately
{% if session.staged_ballot %}
booth.ballot = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.EosObject.from_json('{{ eos.core.objects.EosObject.to_json(session.staged_ballot.ballot)|safe }}'), null);
{% endif %}
boothTasks.append({
activate: function(fromLeft) {
showTemplate('booth/cast.html', {ballot: booth.ballot, is_cast: true});
}
});
templates['booth/cast.html'] = null;
boothTasks.append({ boothTasks.append({
activate: function(fromLeft) { activate: function(fromLeft) {
showTemplate('booth/complete.html', {voter: booth.voter, vote: booth.vote}); showTemplate('booth/complete.html', {voter: booth.voter, vote: booth.vote});

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-2019 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -21,9 +21,6 @@
{% block electioncontent %} {% block electioncontent %}
{% for question in election.questions %} {% for question in election.questions %}
<h2>{{ loop.index }}. {{ question.prompt }}</h2> <h2>{{ loop.index }}. {{ question.prompt }}</h2>
{% if question.description %}
<p>{{ question.description | urlize }}</p>
{% endif %}
{% include eosweb.core.main.model_view_map[question.__class__]['view'] %} {% include eosweb.core.main.model_view_map[question.__class__]['view'] %}
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -18,7 +18,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
#} #}
{% set Status = eos.base.workflow.WorkflowTaskStatus %} {% set Status = eos.base.workflow.WorkflowTask.Status %}
{% block electioncontent %} {% block electioncontent %}
{% if election.workflow.get_task('eos.base.workflow.TaskConfigureElection').status == Status.EXITED %} {% if election.workflow.get_task('eos.base.workflow.TaskConfigureElection').status == Status.EXITED %}
@ -84,7 +84,7 @@
<p><a href="{{ url_for('election_api_json', election_id=election._id) }}?full" class="mini ui labeled icon button"><i class="download icon"></i> Export as Eos JSON</a></p> <p><a href="{{ url_for('election_api_json', election_id=election._id) }}?full" class="mini ui labeled icon button"><i class="download icon"></i> Export as Eos JSON</a></p>
{% if election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status == Status.EXITED %} {% if election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status == Status.EXITED %}
<p>Results were released {{ election.workflow.get_task('eos.base.workflow.TaskReleaseResults').exited_at|pretty_date }}.</p> <p>Results were released at {{ election.workflow.get_task('eos.base.workflow.TaskReleaseResults').exited_at|pretty_date }}.</p>
{% else %} {% else %}
<div class="ui warning message"> <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. 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.

View File

@ -1,82 +0,0 @@
{% extends 'base.html' %}
{#
Eos - Verifiable elections
Copyright © 2017-18 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/>.
#}
{# Big tables on this page #}
{% block mainContainerOpts %}style="max-width: 100% !important;"{% endblock %}
{% block title %}{{ voter.name }} – {{ election.name }}{% endblock %}
{% block content %}
<h1>{{ election.name }}</h1>
<p><small><b>{{ election.kind|title }} fingerprint:</b> <span class="hash">{{ SHA256().update_obj(election).hash_as_b64() }}</span></small></p>
<h2>{{ voter.name }}</h2>
<h3>Votes cast</h3>
<table class="ui celled table">
<thead>
<tr>
<th class="two wide column">Cast at</th>
<th class="three wide column">Comment</th>
{% if session.user and session.user.is_admin() %}
<th class="three wide column">Client</th>
<th class="eight wide column">Ballot fingerprint (<i class="dropdown icon" style="margin: 0;"></i>Content/Actions )</th>
{% else %}
<th class="eleven wide column">Ballot fingerprint (<i class="dropdown icon" style="margin: 0;"></i>Content )</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for vote in voter.votes.get_all() %}
<tr>
<td>{{ vote.cast_at|pretty_date }}</td>
<td>{% if vote.comment %}{{ vote.comment }}{% endif %}</td>
{% if session.user and session.user.is_admin() %}
<td>
{% if vote.cast_ip %}{{ vote.cast_ip }}{% if vote.cast_fingerprint %}<br>{% endif %}{% endif %}
{% if vote.cast_fingerprint %}<span class="hash">{{ eos.core.hashing.SHA256().update_text(eos.core.objects.EosObject.to_json(vote.cast_fingerprint)).hash_as_b64(True) }}</span>{% endif %}
</td>
{% endif %}
<td>
<div class="ui accordion">
<div class="title">
<i class="dropdown icon"></i>
<span class="hash">{{ eos.core.hashing.SHA256().update_obj(vote.ballot).hash_as_b64() }}</span>
</div>
<div class="content">
<div class="monoout" style="max-height: 10em;">{% if session.user and session.user.is_admin() %}{{ eos.core.objects.EosObject.to_json(eos.core.objects.EosObject.serialise_and_wrap(vote)) }}{% else %}{{ eos.core.objects.EosObject.to_json(eos.core.objects.EosObject.serialise_and_wrap(vote, options=eos.core.objects.SerialiseOptions(should_protect=True))) }}{% endif %}</div>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% block basecontent %}
{{ super() }}
<script>
$('.ui.accordion').accordion();
</script>
{% endblock %}

View File

@ -1,37 +0,0 @@
{% extends 'base.html' %}
{#
Eos - Verifiable elections
Copyright © 2017-2019 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/>.
#}
{% block title %}Elections{% endblock %}
{% block content %}
<h1>All elections: Eos Voting for {{ eosweb.app.config['ORG_NAME'] }}</h1>
{% if session.user and session.user.is_admin() %}
<div style="text-align: right; font-size: small;"><a href="{{ url_for('elections_batch') }}">Batch operations</a></div>
{% endif %}
<p>Please choose an election from the list below:</p>
<ul>
{% for election in elections %}
<li><a href="{{ url_for('election_view', election_id=election._id) }}">{{ election.name }}</a></li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -1,56 +0,0 @@
{% extends 'base.html' %}
{#
Eos - Verifiable elections
Copyright © 2017-2019 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/>.
#}
{% block title %}Perform batch operations{% endblock %}
{% block content %}
<h1>Perform batch operations</h1>
<form method="POST">
<table class="ui selectable celled table">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Next stage</th>
</tr>
</thead>
<tbody>
{% for election in elections %}
<tr>
<td><input type="checkbox" name="election_{{ election._id }}" id="election_{{ election._id }}"></td>
<td>{{ election.name }}</td>
<td>
<ul style="padding-left: 1em; margin: 0;">
{% for task in election.workflow.tasks %}
{% if task.status == eos.base.workflow.WorkflowTaskStatus.READY %}
<li>{{ task.label }}</li>
{% endif %}
{% endfor %}
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<input class="ui primary button" type="submit" value="Execute">
</form>
{% endblock %}

View File

@ -1,76 +0,0 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
This file adapted from https://github.com/leemunroe/responsive-html-email-template, licensed under the MIT licence.
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/>.
#}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>{{ title }}</title>
<style>
{{ css|safe }}
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
{{ text|safe }}
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td class="content-block">
<a href="{{ eosweb.app.config['BASE_URI'] }}" style="text-decoration: none">Eos Voting for {{ eosweb.app.config['ORG_NAME'] }}</a>
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -1,347 +0,0 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
This file adapted from https://github.com/leemunroe/responsive-html-email-template, licensed under the MIT licence.
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/>.
#}
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 20px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td, .footer p, .footer span, .footer a {
color: #999999;
font-size: 12px;
text-align: center;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1, h2, h3, h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px;
}
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}
p, ul, ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px;
li {
list-style-position: inside;
margin-left: 5px;
}
}
a {
color: #3498db;
text-decoration: underline;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%;
& > tbody > tr > td {
padding-bottom: 15px;
}
table {
width: auto;
td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
}
a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize;
}
}
.btn-primary {
table td {
background-color: #3498db;
}
a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff;
}
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0;
}
/* -------------------------------------
MISCELLANEOUS
------------------------------------- */
pre {
white-space: pre-wrap;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] {
h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
p, ul, ol, td, span, a {
font-size: 16px !important;
}
.wrapper, .article {
padding: 10px !important;
}
.content {
padding: 0 !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
.btn {
table {
width: 100% !important;
}
a {
width: 100% !important;
}
.img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}

View File

@ -1,26 +0,0 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
This file adapted from https://github.com/leemunroe/responsive-html-email-template, licensed under the MIT licence.
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/>.
#}
{{ text|safe }}
---
Eos Voting for {{ eosweb.app.config['ORG_NAME'] }}
{{ eosweb.app.config['BASE_URI'] }}

View File

@ -2,7 +2,7 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -24,33 +24,9 @@
<h1>Eos Voting for {{ eosweb.app.config['ORG_NAME'] }}</h1> <h1>Eos Voting for {{ eosweb.app.config['ORG_NAME'] }}</h1>
<p>Please choose an election from the list below:</p> <p>Please choose an election from the list below:</p>
{% if elections_open %}
<h2>Currently open</h2>
<ul> <ul>
{% for election in elections_open %} {% for election in eos.base.election.Election.get_all() %}
<li><a href="{{ url_for('election_view', election_id=election._id) }}">{{ election.name }}</a></li> <li><a href="{{ url_for('election_view', election_id=election._id) }}">{{ election.name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %}
{% if elections_soon %}
<h2>Opening soon</h2>
<ul>
{% for election in elections_soon %}
<li><a href="{{ url_for('election_view', election_id=election._id) }}">{{ election.name }}</a></li>
{% endfor %}
</ul>
{% endif %}
{% if elections_closed %}
<h2>Recently closed</h2>
<ul>
{% for election in elections_closed %}
<li><a href="{{ url_for('election_view', election_id=election._id) }}">{{ election.name }}</a></li>
{% endfor %}
</ul>
{% endif %}
<a href="{{ url_for('elections') }}">Show all</a>
{% endblock %} {% endblock %}

View File

@ -16,18 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
#} #}
<p><small> <p><small>Approval voting. Vote for between {{ question.min_choices }} and {{ question.max_choices }} choices.{% if question.randomise_choices %} Order of choices is randomised.{% endif %}</small></p>
Approval voting. Vote for
{% if question.min_choices == question.max_choices %}
exactly {{ question.min_choices }}
{% else %}
between {{ question.min_choices }} and {{ question.max_choices }}
{% endif %}
choices.
{% if question.randomise_choices %}
Order of choices is randomised.
{% endif %}
</small></p>
<ul class="ui list"> <ul class="ui list">
{% for choice in question.choices %} {% for choice in question.choices %}

View File

@ -1,6 +1,6 @@
{# {#
Eos - Verifiable elections Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li) Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published by
@ -40,6 +40,8 @@
Count log Count log
</div> </div>
<div class="content"> <div class="content">
<div class="monoout" style="max-height: 20em;">{{ result.log }}</div> <div class="ui form">
<textarea style="font-family: monospace;" rows="20">{{ result.log }}</textarea>
</div>
</div> </div>
</div> </div>

View File

@ -16,18 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
#} #}
<p><small> <p><small>Preferential voting. Vote for between {{ question.min_choices }} and {{ question.max_choices }} choices.{% if question.randomise_choices %} Order of choices is randomised.{% endif %}</small></p>
Preferential voting. Vote for
{% if question.min_choices == question.max_choices %}
exactly {{ question.min_choices }}
{% else %}
between {{ question.min_choices }} and {{ question.max_choices }}
{% endif %}
choices.
{% if question.randomise_choices %}
Order of choices is randomised.
{% endif %}
</small></p>
<ul class="ui list"> <ul class="ui list">
{% for choice in question.choices %} {% for choice in question.choices %}

View File

@ -1,66 +0,0 @@
{% extends 'base.html' %}
{#
Eos - Verifiable elections
Copyright © 2017-18 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/>.
#}
{% block title %}{{ task.label }}{% endblock %}
{% block content %}
<h1>{{ task.label }}</h1>
<table class="ui celled definition table">
<tbody>
<tr>
<td>Status</td>
<td>{{ task.status.name|title }}</td>
</tr>
<tr>
<td>Strategy</td>
<td>{{ task.run_strategy._name }}</td>
</tr>
<tr>
<td>Scheduled</td>
<td>{% if task.run_at %}{{ task.run_at|pretty_date }}{% endif %}</td>
</tr>
<tr>
<td>Started</td>
<td>{% if task.started_at %}{{ task.started_at|pretty_date }}{% endif %}</td>
</tr>
<tr>
<td>Exited</td>
<td>{% if task.completed_at %}{{ task.completed_at|pretty_date }}{% endif %}</td>
</tr>
<tr>
<td>Messages</td>
<td>
{% if task.messages %}
<div class="monoout" style="max-height: 10em;">{{ '\n'.join(task.messages) }}</div>
{% endif %}
</td>
</tr>
<tr>
<td>Result</td>
<td>
{% if task.result %}
<div class="monoout" style="max-height: 3em;">{{ eos.core.object.EosObject.to_json(eos.core.object.EosObject.serialise_and_wrap(task.result)) }}</div>
{% endif %}
</td>
</tr>
</tbody>
</table>
{% endblock %}

View File

@ -1,46 +0,0 @@
# Eos - Verifiable elections
# Copyright © 2017-2019 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/>.
import flask
from eos.nsauth.election import *
import urllib.request, urllib.parse
blueprint = flask.Blueprint('eosweb.nsauth', __name__, template_folder='templates')
app = None
@blueprint.record
def reddit_register(setup_state):
global app
app = setup_state.app
@blueprint.route('/auth/nationstates/login')
def nationstates_login():
return flask.render_template('auth/nationstates/login.html')
@blueprint.route('/auth/nationstates/authenticate', methods=['POST'])
def nationstates_authenticate():
username = flask.request.form['username'].lower().strip().replace(' ', '_')
with urllib.request.urlopen(urllib.request.Request('https://www.nationstates.net/cgi-bin/api.cgi?a=verify&' + urllib.parse.urlencode({'nation': username, 'checksum': flask.request.form['checksum']}), headers={'User-Agent': app.config['NATIONSTATES_USER_AGENT']})) as resp:
if resp.read().decode('utf-8').strip() != '1':
return flask.render_template('auth/nationstates/login.html', error='The nation name or verification code you entered was invalid. Please check your details and try again. If the issue persists, contact the election administrator.')
flask.session['user'] = NationStatesUser(username=username)
return flask.redirect(flask.url_for('login_complete'))

View File

@ -1,17 +0,0 @@
# Eos - Verifiable elections
# Copyright © 2017-2019 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/>.
NATIONSTATES_USER_AGENT = 'FIXME'

View File

@ -1,51 +0,0 @@
{% extends 'semantic_base.html' %}
{#
Eos - Verifiable elections
Copyright © 2017-2019 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/>.
#}
{% block title %}Log in{% endblock %}
{% block basecontent %}
<div class="ui middle aligned center aligned grid" style="height: 100%;">
<div class="column" style="max-width: 400px;">
<p>1. Log in to NationStates below if necessary, and copy your <i>Login Verification Code</i>.</p>
<iframe src="https://m.nationstates.net/page=verify_login" style="width: 100%; height: 10em;"></iframe>
<p>2. Type your nation name and paste your Login Verification Code into the form below.</p>
<form class="ui large form" action="{{ url_for('eosweb.nsauth.nationstates_authenticate') }}" method="post">
{% if error %}
<div class="ui visible error message">{{ error }}</div>
{% endif %}
<div class="ui stacked segment">
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i>
<input type="text" name="username" placeholder="Nation name">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="linkify icon"></i>
<input type="text" name="checksum" placeholder="Login verification code">
</div>
</div>
<input type="submit" class="ui fluid large teal submit button" value="Log in">
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections # Eos - Verifiable elections
# Copyright © 2017-18 RunasSudo (Yingtong Li) # Copyright © 2017 RunasSudo (Yingtong Li)
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from authlib.integrations.flask_client import OAuth from flask_oauthlib.client import OAuth
import flask import flask
@ -23,52 +23,44 @@ from eos.redditauth.election import *
import base64 import base64
import uuid import uuid
blueprint = flask.Blueprint('eosweb.redditauth', __name__) def main(app):
oauth = OAuth()
app = None reddit = oauth.remote_app('Reddit',
oauth = None request_token_url=None,
@blueprint.record
def reddit_register(setup_state):
global app, oauth
app = setup_state.app
oauth = OAuth(app)
oauth.register('reddit',
#request_token_url=None,
authorize_url='https://www.reddit.com/api/v1/authorize.compact', authorize_url='https://www.reddit.com/api/v1/authorize.compact',
authorize_params={'duration': 'temporary', 'scope': 'identity'}, request_token_params={'duration': 'temporary', 'scope': 'identity'},
access_token_url='https://www.reddit.com/api/v1/access_token', access_token_url='https://www.reddit.com/api/v1/access_token',
access_token_method='POST', access_token_method='POST',
access_token_headers={ access_token_headers={
'Authorization': 'Basic ' + base64.b64encode('{}:{}'.format(app.config['REDDIT_OAUTH_CLIENT_ID'], app.config['REDDIT_OAUTH_CLIENT_SECRET']).encode('ascii')).decode('ascii'), 'Authorization': 'Basic ' + base64.b64encode('{}:{}'.format(app.config['REDDIT_OAUTH_CLIENT_ID'], app.config['REDDIT_OAUTH_CLIENT_SECRET']).encode('ascii')).decode('ascii'),
'User-Agent': app.config['REDDIT_USER_AGENT'] 'User-Agent': app.config['REDDIT_USER_AGENT']
}, },
client_id=app.config['REDDIT_OAUTH_CLIENT_ID'], consumer_key=app.config['REDDIT_OAUTH_CLIENT_ID'],
client_secret=app.config['REDDIT_OAUTH_CLIENT_SECRET'], consumer_secret=app.config['REDDIT_OAUTH_CLIENT_SECRET']
fetch_token=lambda: flask.session.get('user').oauth_token
) )
@blueprint.route('/auth/reddit/login') @app.route('/auth/reddit/login')
def reddit_login(): def reddit_login():
return oauth.reddit.authorize_redirect(redirect_uri=app.config['BASE_URI'] + flask.url_for('eosweb.redditauth.reddit_oauth_authorized'), state=str(uuid.uuid4())) return reddit.authorize(callback=app.config['BASE_URI'] + flask.url_for('reddit_oauth_authorized'), state=uuid.uuid4())
@blueprint.route('/auth/reddit/oauth_callback') @reddit.tokengetter
def reddit_oauth_authorized(): def get_reddit_oauth_token():
try: return (flask.session.get('user').oauth_token, '')
token = oauth.reddit.authorize_access_token()
except: @app.route('/auth/reddit/oauth_callback')
def reddit_oauth_authorized():
resp = reddit.authorized_response()
if resp is None:
# Request denied # Request denied
return flask.redirect(flask.url_for('login_cancelled')) return flask.redirect(flask.url_for('login_cancelled'))
user = RedditUser() user = RedditUser()
user.oauth_token = token user.oauth_token = resp['access_token']
flask.session['user'] = user flask.session['user'] = user
me = oauth.reddit.get('https://oauth.reddit.com/api/v1/me', headers={ me = reddit.get('https://oauth.reddit.com/api/v1/me', headers={
'User-Agent': app.config['REDDIT_USER_AGENT'] 'User-Agent': app.config['REDDIT_USER_AGENT']
}) })
user.username = me.json()['name'] user.username = me.data['name']
return flask.redirect(flask.url_for('login_complete')) return flask.redirect(flask.url_for('login_complete'))

View File

@ -1 +1 @@
<script src="eosweb/core/static/js/eosjs.js"></script> <script src="eos/__javascript__/eos.js_tests.js"></script>

View File

@ -9,11 +9,9 @@ AUTH_METHODS = [
('reddit', 'Reddit') ('reddit', 'Reddit')
] ]
import eos.base.election
import eos.redditauth.election import eos.redditauth.election
ADMINS = [ ADMINS = [
#eos.redditauth.election.RedditUser(username='xxxxxxxx'), #eos.redditauth.election.RedditUser(username='xxxxxxxx')
#eos.base.election.EmailUser(email='xxxxx@example.com', password='abc123'),
] ]
TASK_RUN_STRATEGY = 'eos.core.tasks.direct.DirectRunStrategy' TASK_RUN_STRATEGY = 'eos.core.tasks.direct.DirectRunStrategy'
@ -34,13 +32,11 @@ DB_NAME = 'eos'
# Email # Email
MAIL_SERVER, MAIL_PORT = 'localhost', 25 SMTP_HOST, SMTP_PORT = 'localhost', 25
MAIL_USERNAME, MAIL_PASSWORD = None, None SMTP_USER, SMTP_PASS = None, None
MAIL_DEFAULT_SENDER = 'eos@localhost' SMTP_FROM = 'eos@localhost'
# Reddit # Reddit
# Register a web app at https://www.reddit.com/prefs/apps
# The redirect URI will be http(s)://hostname(:port)/auth/reddit/oauth_callback
REDDIT_OAUTH_CLIENT_ID = 'xxxxxxxxxxxxxx' REDDIT_OAUTH_CLIENT_ID = 'xxxxxxxxxxxxxx'
REDDIT_OAUTH_CLIENT_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxx' REDDIT_OAUTH_CLIENT_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxx'

6839
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
{
"dependencies": {
"@babel/cli": "^7.15.7",
"@babel/core": "^7.15.8",
"@babel/preset-env": "^7.15.8",
"babelify": "^10.0.0",
"browserify": "^17.0.0"
}
}

View File

@ -1,17 +1,27 @@
Authlib==0.14.3 certifi==2017.11.5
chardet==3.0.4
click==6.7
coverage==4.4.1 coverage==4.4.1
Flask==0.12.2 Flask==0.12.2
Flask-Mail==0.9.1 Flask-OAuthlib==0.9.4
flask-paginate==0.7.0
Flask-Session==0.3.1
Flask-SQLAlchemy==2.3.2
gunicorn==19.7.1 gunicorn==19.7.1
libsass==0.13.4 idna==2.6
premailer==3.1.1 itsdangerous==0.24
psycopg2==2.8.5 Jinja2==2.10
MarkupSafe==1.0
mypy==0.550
oauthlib==2.0.6
psutil==5.4.1
psycopg2==2.7.3.2
PyExecJS==1.4.1 PyExecJS==1.4.1
pymongo[srv]==3.10.1 pymongo==3.5.1
pyRCV==0.3 pyRCV==0.3
pytz==2017.3 pytz==2017.3
requests==2.18.4
requests-oauthlib==0.8.0
six==1.10.0
timeago==1.0.8 timeago==1.0.8
Transcrypt==3.9.0 Transcrypt==3.6.60
typed-ast==1.1.0
urllib3==1.22
Werkzeug==0.12.2

View File

@ -1 +1 @@
python-3.7.2 python-3.6.3