Compare commits

..

24 Commits

Author SHA1 Message Date
RunasSudo 8333dd7c2d
Fix BLT export format 2021-10-16 22:12:22 +11:00
RunasSudo 913aac26ca
Fix unit tests 2021-10-16 20:44:56 +11:00
RunasSudo fc4366c028
Allow hiding voters and/or votes 2021-10-16 20:44:31 +11:00
RunasSudo d44c21cbd7
Support null-encrypted election in web UI 2021-10-16 20:13:28 +11:00
RunasSudo adbdb21e0d
Fix typo in voting booth when failing to load fingerprintjs2 2021-10-16 01:48:20 +11:00
RunasSudo c948615647
Update to Transcrypt/Python 3.9 2021-10-16 01:47:50 +11:00
RunasSudo 2c15f443f8
Update libraries
Update for Python 3.8
Migrate from Flask-OAuthlib to Authlib
2020-07-11 17:59:32 +10:00
RunasSudo da24cc4f63
Add threading task strategy 2019-03-17 12:06:46 +11:00
RunasSudo 1bb0197b46
Implement batch operations on elections #11 2019-03-06 13:43:54 +11:00
RunasSudo fd6f6bc4b1
Implement task timeout 2019-03-06 13:06:01 +11:00
RunasSudo 3097092ae5
Add run task CLI option 2019-02-17 15:04:59 +11:00
RunasSudo 993ec142ac
Add NationStates authentication 2019-02-13 09:46:11 +11:00
RunasSudo 1bc558fc97
Open voting booth description links in new tab 2019-01-14 20:22:49 +11:00
RunasSudo 706fd132cd
Document new npm dependencies 2019-01-14 18:20:25 +11:00
RunasSudo 5e06ffe577
Explicitly specify oauthlib 2 2019-01-14 18:20:19 +11:00
RunasSudo 9c2c0cf108
Add description field to questions 2019-01-14 17:55:24 +11:00
RunasSudo 2011749836
Prevent saving different version objects 2019-01-14 17:55:14 +11:00
RunasSudo cb3623fda7
Update for Transcrypt/Python 3.7 2019-01-14 17:54:31 +11:00
RunasSudo 86f01abfdd
Tidy docs 2018-12-30 13:44:59 +11:00
RunasSudo bb061e18cd
Documentation for Reddit OAuth callback URI 2018-12-29 19:23:48 +11:00
RunasSudo 4b74072063
Fix errors trying to pickle users / log in 2018-08-31 10:37:06 +10:00
RunasSudo 4bc3fcf30c
Allow administration through email 2018-08-23 14:31:58 +10:00
RunasSudo 05d5650f2f
Fix home page error on attempting to categorise unscheduled elections 2018-04-17 21:42:30 +10:00
RunasSudo f0950effca
Fix PostgreSQL 2018-04-15 19:41:16 +10:00
48 changed files with 7578 additions and 265 deletions

3
.babelrc Normal file
View File

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

3
.gitignore vendored
View File

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

View File

@ -12,6 +12,10 @@ Install the Python dependencies. (If doing this in a virtualenv, add the virtual
cd /path/to/Eos
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_js.sh

View File

@ -1,6 +1,6 @@
#!/bin/bash
# Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li)
# 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
@ -20,21 +20,14 @@ FLAGS="-k -mc -o"
#for f in eos.js eos.js_tests; do
for f in eos.js_tests; do
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
cp eos/__javascript__/eos.js_tests.js eosweb/core/static/js/eosjs.js
perl -0777 -pi -e 's/eosjs_tests/eosjs/g' eosweb/core/static/js/eosjs.js
# Transcrypt syntax errors
perl -0777 -pi -e 's/import \{, /import \{/g' __target__/eos*.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
# Copyright © 2017 RunasSudo (Yingtong Li)
# Copyright © 2017-2021 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
@ -29,6 +29,9 @@ class NullEncryptedAnswer(EncryptedAnswer):
def decrypt(self):
return None, self.answer
def deaudit(self):
return self
class Ballot(EmbeddedObject):
#_id = UUIDField()
@ -76,6 +79,9 @@ class User(EmbeddedObject):
if admin.matched_by(self):
return True
return False
def __getstate__(self):
return {k: v for k, v in self.__dict__.items() if k != '_instance'}
def generate_password():
if is_python:
@ -104,14 +110,15 @@ class UserVoter(Voter):
return self.user.name
class Question(EmbeddedObject):
_ver = StringField(default='0.7')
prompt = StringField()
description = StringField()
class Result(EmbeddedObject):
pass
class ListChoiceQuestion(Question):
_ver = StringField(default='0.5')
choices = EmbeddedObjectListField()
min_choices = IntField()
max_choices = IntField()
@ -207,6 +214,8 @@ class STVResult(Result):
random = BlobField()
class Election(TopLevelObject):
_ver = StringField(default='0.9')
_id = UUIDField()
workflow = EmbeddedObjectField(Workflow) # Once saved, we don't care what kind of workflow it is
name = StringField()
@ -215,6 +224,13 @@ class Election(TopLevelObject):
questions = EmbeddedObjectListField()
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):
#__pragma__('skip')
from eos.core.hashing import SHA256

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections
# Copyright © 2017-18 RunasSudo (Yingtong Li)
# Copyright © 2017-2021 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
@ -38,7 +38,7 @@ class ElectionTestCase(EosTestCase):
def test_run_election(self):
# Set up election
election = Election()
election.workflow = WorkflowBase()
election.workflow = BaseWorkflow()
# Check _instance
self.assertEqual(election.workflow._instance, (election, 'workflow'))

View File

@ -1,6 +1,6 @@
# Eos - Verifiable elections
# pyRCV - Preferential voting counting
# Copyright © 2016–2017 RunasSudo (Yingtong Li)
# Copyright © 2016-2021 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
@ -41,10 +41,10 @@ def writeBLT(election, q_num, seats, withdrawn=[]):
for candidate in flat_choices:
if candidate.party:
electionLines.append("'{}{}'".format(candidate.name, candidate.party))
electionLines.append('"{}{}"'.format(candidate.name, candidate.party))
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

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections
# Copyright © 2017-18 RunasSudo (Yingtong Li)
# Copyright © 2017-2021 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
@ -58,7 +58,12 @@ class WorkflowTask(EmbeddedObject):
def are_dependencies_met(self):
for depends_on_desc in self.depends_on:
for depends_on_task in self.workflow.get_tasks(depends_on_desc):
depends_on_tasks = list(self.workflow.get_tasks(depends_on_desc))
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 True
@ -184,7 +189,9 @@ class TaskReleaseResults(WorkflowTask):
# Concrete workflows
# ==================
class WorkflowBase(Workflow):
class BaseWorkflow(Workflow):
"""Base workflow, with no encryption"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li)
# 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
@ -35,17 +35,21 @@ class PostgreSQLDBProvider(eos.core.db.DBProvider):
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 '_id' in fields and val['_id'] != fields.pop('_id'):
continue
if 'type' in fields and val['type'] != fields.pop('type'):
continue
for field in fields:
if val['value'][field] != fields[field]:
continue
result.append(val)
if does_match(val):
result.append(val)
return result
def get_by_id(self, table, _id):

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li)
# 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
@ -184,18 +184,21 @@ class RelatedObjectListField(Field):
return None
return EosList([EosObject.deserialise_and_unwrap(x, self.object_type) for x in value])
if is_python:
class UUIDField(Field):
def __init__(self, *args, **kwargs):
class UUIDField(Field):
def __init__(self, *args, **kwargs):
if is_python:
super().__init__(default=uuid.uuid4, *args, **kwargs)
def serialise(self, value, options=SerialiseOptions.DEFAULT):
return str(value)
def deserialise(self, value):
else:
super().__init__(*args, **kwargs)
def serialise(self, value, options=SerialiseOptions.DEFAULT):
return str(value)
def deserialise(self, value):
if is_python:
return uuid.UUID(value)
else:
UUIDField = PrimitiveField
else:
return value
class DateTimeField(Field):
def pad(self, number):
@ -359,7 +362,16 @@ class DocumentObjectType(EosObjectType):
fields = {}
if hasattr(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:
# We must skip things with getters or else they will be called here (too soon)
if Object.getOwnPropertyDescriptor(cls, attr).js_get:
@ -407,6 +419,8 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType):
def __init__(self, *args, **kwargs):
super().__init__()
self._json = None
self._field_values = {}
# Different to Python
@ -444,6 +458,10 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType):
val.object_init(self, default)
def serialise(self, options=SerialiseOptions.DEFAULT):
if self._ver != self._fields['_ver'].default:
# 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
@ -455,7 +473,11 @@ class DocumentObject(EosObject, metaclass=DocumentObjectType):
for attr, val in cls._fields.items():
if attr in value:
attrs[val.internal_name] = val.deserialise(value[val.real_name])
return cls(**attrs)
inst = cls(**attrs)
inst._json = value
return inst
class TopLevelObjectType(DocumentObjectType):
def __new__(meta, name, bases, attrs):
@ -477,8 +499,10 @@ class TopLevelObjectType(DocumentObjectType):
class TopLevelObject(DocumentObject, metaclass=TopLevelObjectType):
def save(self):
#res = db[self._name].replace_one({'_id': self.serialise()['_id']}, self.serialise(), upsert=True)
#res = dbinfo.db[self._db_name].replace_one({'_id': self._fields['_id'].serialise(self._id)}, EosObject.serialise_and_wrap(self), upsert=True)
if self._ver != self._fields['_ver'].default:
# Different version, unable to save
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))
def delete(self):

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections
# Copyright © 2017-18 RunasSudo (Yingtong Li)
# 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
@ -31,12 +31,15 @@ class TaskStatus(EosEnum):
class Task(TopLevelObject):
label = 'Unknown task'
_ver = StringField(default='0.8')
_id = UUIDField()
run_strategy = EmbeddedObjectField()
run_at = DateTimeField()
timeout = IntField(default=3600) # seconds
started_at = DateTimeField()
completed_at = DateTimeField()
@ -113,6 +116,16 @@ class TaskScheduler:
@staticmethod
def tick():
now = DateTimeField.now()
for task in TaskScheduler.pending_tasks():
if task.run_at and task.run_at < DateTimeField.now():
if task.run_at and task.run_at < now:
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

@ -0,0 +1,51 @@
# 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
# Copyright © 2017-18 RunasSudo (Yingtong Li)
# Copyright © 2017-2021 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
@ -133,6 +133,7 @@ class TaskTestCase(EosTestCase):
def setUpClass(cls):
cls.db_connect_and_reset()
@py_only
def test_normal(self):
class TaskNormal(Task):
result = StringField()
@ -149,6 +150,7 @@ class TaskTestCase(EosTestCase):
self.assertEqual(task.messages[0], 'Hello World')
self.assertEqual(task.result, 'Success')
@py_only
def test_error(self):
class TaskError(Task):
def _run(self):

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li)
# 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
@ -14,8 +14,23 @@
# 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 eos.js
import eos.core.objects
import eos.core.bigint
import eos.core.hashing
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.psr.tests

30
eos/nsauth/election.py Normal file
View File

@ -0,0 +1,30 @@
# 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

@ -1,5 +1,5 @@
# Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li)
# Copyright © 2017-2021 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
@ -259,7 +259,8 @@ class PedersenVSSPrivateKey(EmbeddedObject):
def get_modified_secret(self):
mod_s = self.x
for j in range(1, threshold + 1): # 1 to threshold
...
# TODO
pass
def decrypt(self, ciphertext):
if (

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li)
# Copyright © 2017-2021 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
@ -225,12 +225,20 @@ class InternalMixingTrustee(MixingTrustee):
return True
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
public_key = EmbeddedObjectField(SEGPublicKey)
mixing_trustees = EmbeddedObjectListField()
def can_audit(self):
"""Overrides Election.can_audit"""
return True
def verify(self):
"""Overrides Election.verify"""
# Verify ballots
super().verify()

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Eos - Verifiable elections
# Copyright © 2017 RunasSudo (Yingtong Li)
# Copyright © 2017-2021 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
@ -43,9 +43,9 @@ class BasePyTestCase(TestCase):
class BaseJSTestCase(TestCase):
@classmethod
def setUpClass(cls):
with open('eos/__javascript__/eos.js_tests.js', 'r') as f:
with open('eosweb/core/static/js/eosjs.js', 'r') as f:
code = f.read()
cls.ctx = execjs.get().compile('var window={},navigator={};' + code + 'var test=window.eosjs_tests.' + cls.module + '.__all__.' + cls.name + '();test.setUpClass();')
cls.ctx = execjs.get().compile('var window={},navigator={};' + code + 'var eosjs=require("eosjs");var test=eosjs.' + cls.module + '.' + cls.name + '();test.setUpClass();')
@classmethod
def add_method(cls, method):

View File

@ -1,5 +1,5 @@
# Eos - Verifiable elections
# Copyright © 2017-18 RunasSudo (Yingtong Li)
# Copyright © 2017-2021 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
@ -120,50 +120,6 @@ def run_tests(prefix, lang):
def sessdb():
app.session_interface.db.create_all()
# TODO: Will remove this once we have a web UI
@app.cli.command('drop_db_and_setup')
def setup_test_election():
# 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):
emails.voter_email_password(election, voter)
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')
@click.option('--electionid', default=None)
def verify_election(electionid):
@ -196,6 +152,20 @@ def tally_stv_election(electionid, qnum, randfile, numseats):
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'])()
)
task.save()
task.run()
@app.context_processor
def inject_globals():
return {'eos': eos, 'eosweb': eosweb, 'SHA256': eos.core.hashing.SHA256}
@ -213,29 +183,6 @@ def tick_scheduler():
# === Views ===
@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().run_at]
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)
def using_election(func):
@functools.wraps(func)
def wrapped(election_id, **kwargs):
@ -252,11 +199,76 @@ def election_admin(func):
return flask.Response('Administrator credentials required', 403)
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>/')
@using_election
def election_api_json(election):
is_full = 'full' in flask.request.args
return flask.Response(EosObject.to_json(EosObject.serialise_and_wrap(election, None, SerialiseOptions(should_protect=True, for_hash=(not is_full), combine_related=True))), 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')
@using_election
@ -279,14 +291,20 @@ def election_view_questions(election):
@app.route('/election/<election_id>/view/ballots')
@using_election
def election_view_ballots(election):
return flask.render_template('election/view/ballots.html', election=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.Response('Voters not public', 403)
@app.route('/election/<election_id>/voter/<voter_id>')
@using_election
def election_voter_view(election, voter_id):
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)
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')
@using_election
@ -448,13 +466,21 @@ def email_login():
def email_authenticate():
user = None
for election in Election.get_all():
for voter in election.voters:
if isinstance(voter.user, EmailUser):
if voter.user.email.lower() == flask.request.form['email'].lower():
if voter.user.password == flask.request.form['password']:
user = voter.user
break
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 voter in election.voters:
if isinstance(voter.user, EmailUser):
if voter.user.email.lower() == flask.request.form['email'].lower():
if voter.user.password == flask.request.form['password']:
user = voter.user
break
if user is None:
return flask.render_template('auth/email/login.html', error='The email or password you entered was invalid. Please check your details and try again. If the issue persists, contact the election administrator.')

View File

@ -1,6 +1,6 @@
/*
Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li)
Copyright © 2017-2021 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
@ -19,18 +19,37 @@
window = self; // Workaround for libraries
isLibrariesLoaded = false;
eosjs = null;
function generateEncryptedVote(election, answers, should_do_fingerprint) {
encrypted_answers = [];
for (var q_num = 0; q_num < answers.length; q_num++) {
answer_json = answers[q_num];
answer = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(answer_json, null);
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.__all__.EosObject.serialise_and_wrap(encrypted_answer, null));
if (election._name === 'eos.psr.election.PSRElection') {
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.psr.election.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));
}
postMessage({
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;
}
postMessage({
encrypted_answers: encrypted_answers
});
}
onmessage = function(msg) {
@ -40,10 +59,11 @@ onmessage = function(msg) {
msg.data.static_base_url + "js/eosjs.js"
);
isLibrariesLoaded = true;
eosjs = require("eosjs");
}
if (msg.data.action === "generateEncryptedVote") {
msg.data.election = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(msg.data.election, null);
msg.data.election = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(msg.data.election, null);
generateEncryptedVote(msg.data.election, msg.data.answers);
} else {

View File

@ -2,7 +2,7 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
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
@ -25,7 +25,7 @@
<p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p>
</div>
<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>
<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>
<div class="ui form">
{# For some reason nunjucks doesn't like calling this the normal way #}

View File

@ -1,6 +1,6 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
Copyright © 2017-2021 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
@ -18,7 +18,7 @@
<h1>{{ election.name }}</h1>
<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>
<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>
{# Convert the template name to a numerical index for comparison #}
{% if template == 'booth/welcome.html' %}
@ -32,9 +32,17 @@
{% elif template == 'booth/audit.html' %}
{% set menuindex = 4 %}
{% elif template == 'booth/cast.html' %}
{% set menuindex = 5 %}
{% if election.can_audit() %}
{% set menuindex = 5 %}
{% else %}
{% set menuindex = 4 %}
{% endif %}
{% elif template == 'booth/complete.html' %}
{% set menuindex = 6 %}
{% if election.can_audit() %}
{% set menuindex = 6 %}
{% else %}
{% set menuindex = 5 %}
{% endif %}
{% endif %}
{% macro menuitem(index, text) %}
@ -50,9 +58,14 @@
{{ menuitem(1, "Welcome") }}
{{ menuitem(2, "Select") }}
{{ menuitem(3, "Review") }}
{{ menuitem(4, "Audit") }}
{{ menuitem(5, "Cast") }}
{{ menuitem(6, "Finish") }}
{% 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">

View File

@ -2,7 +2,7 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
Copyright © 2017-2021 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
@ -20,7 +20,7 @@
{% block content %}
<div id="cast_prompt">
<p>Your vote has <span class="superem">not</span> yet been cast. Please make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64(true) }}</span>.</p>
<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>
<div class="ui negative message">
<p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p>
@ -69,10 +69,12 @@
{% endblock %}
{% block after %}
<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.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p>
</div>
{% 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>
$(".message .close").on("click", function() {
@ -104,8 +106,8 @@
$.ajax({
url: "{{ election_base_url }}stage_ballot",
type: "POST",
data: eosjs.eos.core.objects.__all__.EosObject.to_json({
"ballot": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(deauditedBallot, null),
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",
@ -167,9 +169,9 @@
dataType: "text"
})
.done(function(data) {
response = eosjs.eos.core.objects.__all__.EosObject.from_json(data);
booth.voter = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(response.voter);
booth.vote = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(response.vote);
response = eosjs.eos.core.objects.EosObject.from_json(data);
booth.voter = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(response.voter);
booth.vote = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(response.vote);
// Clear plaintexts
booth.answers = null;

View File

@ -2,7 +2,7 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
Copyright © 2017-2021 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
@ -23,7 +23,9 @@
<p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p>
</div>
<p>Please make a note of your ballot fingerprint, <span class="hash">{{ eosjs.eos.core.hashing.__all__.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>
{% if election.can_audit() %}
<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>
@ -39,10 +41,12 @@
{% endblock %}
{% block after %}
<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.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>.</p>
</div>
{% 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 %}

View File

@ -2,7 +2,7 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
Copyright © 2017-2021 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
@ -26,24 +26,33 @@
<div class="content">
<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>Ballot fingerprint: <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(vote.ballot).hash_as_b64(true) }}</span></p>
{% if election.can_audit() %}
<p>Ballot fingerprint: <span class="hash">{{ eosjs.eos.core.hashing.SHA256().update_obj(vote.ballot).hash_as_b64(true) }}</span></p>
{% endif %}
</div>
</div>
<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>
{% if election.can_audit() %}
<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>
{% endif %}
{% endblock %}
{% block buttons %}
<a href="{{ election_base_url }}view/ballots" class="ui right floated primary button">Finish</a>
{% if election.is_votes_public %}
<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 %}
<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.__all__.SHA256().update_obj(vote.ballot).hash_as_b64() }}</span>.</p>
</div>
{% 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 %}

View File

@ -2,7 +2,7 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
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
@ -30,19 +30,19 @@
try {
rawAnswers = [];
for (var answer_json of booth.answers) {
rawAnswers.push(eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(answer_json, null));
rawAnswers.push(eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(answer_json, null));
}
encryptedAnswers = [];
for (var encrypted_answer_json of msg.data.encrypted_answers) {
encryptedAnswers.push(eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(encrypted_answer_json, null));
encryptedAnswers.push(eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(encrypted_answer_json, null));
}
booth.ballot = eosjs.eos.base.election.__all__.Ballot();
booth.ballot = eosjs.eos.base.election.Ballot();
booth.ballot.answers = rawAnswers;
booth.ballot.encrypted_answers = encryptedAnswers;
booth.ballot.election_id = election._id;
booth.ballot.election_hash = eosjs.eos.core.hashing.__all__.SHA256().update_obj(election).hash_as_b64();
booth.ballot.election_hash = eosjs.eos.core.hashing.SHA256().update_obj(election).hash_as_b64();
if (should_do_fingerprint) {
// String.prototype.join confuses fingerprintjs2
@ -69,7 +69,7 @@
boothWorker.postMessage({
"action": "generateEncryptedVote",
"static_base_url": "{{ static_base_url }}",
"election": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(election, null),
"election": eosjs.eos.core.objects.EosObject.serialise_and_wrap(election, null),
"answers": booth.answers
});
} catch (err) {

View File

@ -2,7 +2,7 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
Copyright © 2017-2021 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
@ -30,8 +30,12 @@
{% include templates[selection_model_view_map[election.questions.__getitem__(loop.index0)._name]["selections_review"]] %}
{% endfor %}
<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(true) }}</span>.</p>
<p>Click ‘Continue’, and you will be able to log in to cast your vote.</p>
{% 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.SHA256().update_obj(ballot).hash_as_b64(true) }}</span>.</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 %}
{% block buttons %}
@ -40,11 +44,13 @@
{% endblock %}
{% block after %}
<div class="ui tiny message" style="margin-top: 3em;">
<div class="header">Information for advanced users</div>
<p>Your ballot fingerprint is <span class="hash">{{ eosjs.eos.core.hashing.__all__.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>
{% if election.can_audit() %}
<div class="ui tiny message" style="margin-top: 3em;">
<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 %}

View File

@ -2,7 +2,7 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
Copyright © 2017-2021 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
@ -30,8 +30,12 @@
{% include templates[selection_model_view_map[election.questions.__getitem__(loop.index0)._name]["selections_review"]] %}
{% endfor %}
<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(true) }}</span>.</p>
<p>Click ‘Continue’, and you will be able to copy your pre-poll ballot to provide to the election administrator.</p>
{% 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.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>
{% 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 %}
{% block buttons %}

View File

@ -2,7 +2,7 @@
{#
Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li)
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

View File

@ -2,7 +2,7 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
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

View File

@ -1,6 +1,6 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
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
@ -18,6 +18,10 @@
<h2>{{ questionNum + 1 }}. {{ election.questions.__getitem__(questionNum).prompt }}</h2>
{% if election.questions.__getitem__(questionNum).description %}
<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 %}
@ -107,8 +111,8 @@
}
}
answer = eosjs.eos.base.election.__all__.ApprovalAnswer(eosjs.__kwargtrans__({choices: selections}));
booth.answers[booth.questionNum] = eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(answer);
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;
}

View File

@ -1,6 +1,6 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
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
@ -18,6 +18,10 @@
<h2>{{ questionNum + 1 }}. {{ election.questions.__getitem__(questionNum).prompt }}</h2>
{% if election.questions.__getitem__(questionNum).description %}
<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 %}
@ -249,8 +253,8 @@
}
}
answer = eosjs.eos.base.election.__all__.PreferentialAnswer(eosjs.__kwargtrans__({choices: selections}));
booth.answers[booth.questionNum] = eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(answer);
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

View File

@ -1,6 +1,6 @@
{#
Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li)
Copyright © 2017-2021 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
@ -19,5 +19,11 @@
{% block tabs %}
{{ tab('Overview', 'election_view') }}
{{ tab('Questions', 'election_view_questions') }}
{{ tab('Voters and ballots', 'election_view_ballots') }}
{% 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') }}
{% else %}
{{ tab('Voters', 'election_view_ballots') }}
{% endif %}
{% endif %}
{% endblock %}

View File

@ -2,7 +2,7 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
Copyright © 2017-2021 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
@ -23,21 +23,27 @@
<thead>
<tr>
<th>Voter</th>
<th>Ballot fingerprint</th>
{% if election.is_votes_public or (session.user and session.user.is_admin()) %}
<th>Ballot fingerprint</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for voter in election.voters %}
<tr>
<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>
{% 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>
{% endif %}
</tr>
{% endfor %}
</tbody>

View File

@ -2,7 +2,7 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
Copyright © 2017-2021 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
@ -44,6 +44,8 @@
{% endif %}
<script>
var eosjs = require("eosjs");
var templates = {};
var election = null;
var booth = null;
@ -70,7 +72,7 @@
// Verify booth
if (should_do_fingerprint) {
if (typeof Fingerprint2 === 'undefined') {
boothError('Your browser did not load fingerprintj2 correctly. Please try again after disabling your ad blockers and similar software. If the issue persists, try using a different browser.');
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;
}
}
@ -78,7 +80,7 @@
$.ajax({ url: "{{ url_for('election_api_json', election_id=election._id) }}", dataType: "text" })
.done(function(data) {
try {
election = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.__all__.EosObject.from_json(data), null);
election = eosjs.eos.core.objects.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.EosObject.from_json(data), null);
boothWorker = new Worker("{{ url_for('static', filename='js/booth_worker.js') }}");
@ -117,7 +119,7 @@
})
.done(function(data) {
try {
templates[templateUrl] = nunjucks.compile(data);
templates[templateUrl] = nunjucks.compile(data, null, templateUrl);
numTemplatesLoaded += 1;
if (numTemplatesLoaded == templateUrls.length) {
// All templates loaded. Show voting booth
@ -210,12 +212,14 @@
}
});
templates['booth/welcome.html'] = null;
boothTasks.append({
activate: function(fromLeft) {
showTemplate('booth/selections.html');
}
});
templates['booth/selections.html'] = null;
boothTasks.append({
activate: function(fromLeft) {
if (fromLeft) {
@ -235,12 +239,14 @@
}
});
templates['booth/review_prepoll.html'] = null;
boothTasks.append({
activate: function(fromLeft) {
showTemplate('booth/audit.html', {ballot: booth.ballot});
}
});
templates['booth/audit.html'] = null;
boothTasks.append({
activate: function(fromLeft) {
showTemplate('booth/cast_prepoll.html', {ballot: booth.ballot});
@ -255,18 +261,21 @@
}
});
templates['booth/review.html'] = null;
boothTasks.append({
activate: function(fromLeft) {
showTemplate('booth/audit.html', {ballot: booth.ballot});
}
});
templates['booth/audit.html'] = null;
boothTasks.append({
activate: function(fromLeft) {
showTemplate('booth/cast.html', {ballot: booth.ballot, is_cast: false});
}
});
templates['booth/cast.html'] = null;
boothTasks.append({
activate: function(fromLeft) {
showTemplate('booth/complete.html', {voter: booth.voter, vote: booth.vote});
@ -277,7 +286,7 @@
} else {
// Cast immediately
{% if session.staged_ballot %}
booth.ballot = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.__all__.EosObject.from_json('{{ eos.core.objects.EosObject.to_json(session.staged_ballot.ballot)|safe }}'), null);
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) {
@ -285,6 +294,7 @@
}
});
templates['booth/cast.html'] = null;
boothTasks.append({
activate: function(fromLeft) {
showTemplate('booth/complete.html', {voter: booth.voter, vote: booth.vote});

View File

@ -2,7 +2,7 @@
{#
Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li)
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
@ -21,6 +21,9 @@
{% block electioncontent %}
{% for question in election.questions %}
<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'] %}
{% endfor %}
{% endblock %}

View File

@ -2,7 +2,7 @@
{#
Eos - Verifiable elections
Copyright © 2017-18 RunasSudo (Yingtong Li)
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
@ -23,6 +23,10 @@
{% 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>

View File

@ -0,0 +1,56 @@
{% 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 %}

46
eosweb/nsauth/main.py Normal file
View File

@ -0,0 +1,46 @@
# 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'))

17
eosweb/nsauth/settings.py Normal file
View File

@ -0,0 +1,17 @@
# 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

@ -0,0 +1,51 @@
{% 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

@ -14,7 +14,7 @@
# 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 flask_oauthlib.client import OAuth
from authlib.integrations.flask_client import OAuth
import flask
@ -27,51 +27,48 @@ blueprint = flask.Blueprint('eosweb.redditauth', __name__)
app = None
oauth = None
reddit = None
@blueprint.record
def reddit_register(setup_state):
global app, oauth, reddit
global app, oauth
app = setup_state.app
oauth = OAuth()
reddit = oauth.remote_app('Reddit',
request_token_url=None,
oauth = OAuth(app)
oauth.register('reddit',
#request_token_url=None,
authorize_url='https://www.reddit.com/api/v1/authorize.compact',
request_token_params={'duration': 'temporary', 'scope': 'identity'},
authorize_params={'duration': 'temporary', 'scope': 'identity'},
access_token_url='https://www.reddit.com/api/v1/access_token',
access_token_method='POST',
access_token_headers={
'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']
},
consumer_key=app.config['REDDIT_OAUTH_CLIENT_ID'],
consumer_secret=app.config['REDDIT_OAUTH_CLIENT_SECRET']
client_id=app.config['REDDIT_OAUTH_CLIENT_ID'],
client_secret=app.config['REDDIT_OAUTH_CLIENT_SECRET'],
fetch_token=lambda: flask.session.get('user').oauth_token
)
@reddit.tokengetter
def get_reddit_oauth_token():
return (flask.session.get('user').oauth_token, '')
@blueprint.route('/auth/reddit/login')
def reddit_login():
return reddit.authorize(callback=app.config['BASE_URI'] + flask.url_for('eosweb.redditauth.reddit_oauth_authorized'), state=uuid.uuid4())
return oauth.reddit.authorize_redirect(redirect_uri=app.config['BASE_URI'] + flask.url_for('eosweb.redditauth.reddit_oauth_authorized'), state=str(uuid.uuid4()))
@blueprint.route('/auth/reddit/oauth_callback')
def reddit_oauth_authorized():
resp = reddit.authorized_response()
if resp is None:
try:
token = oauth.reddit.authorize_access_token()
except:
# Request denied
return flask.redirect(flask.url_for('login_cancelled'))
user = RedditUser()
user.oauth_token = resp['access_token']
user.oauth_token = token
flask.session['user'] = user
me = reddit.get('https://oauth.reddit.com/api/v1/me', headers={
me = oauth.reddit.get('https://oauth.reddit.com/api/v1/me', headers={
'User-Agent': app.config['REDDIT_USER_AGENT']
})
user.username = me.data['name']
user.username = me.json()['name']
return flask.redirect(flask.url_for('login_complete'))

View File

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

View File

@ -9,9 +9,11 @@ AUTH_METHODS = [
('reddit', 'Reddit')
]
import eos.base.election
import eos.redditauth.election
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'
@ -37,6 +39,8 @@ MAIL_USERNAME, MAIL_PASSWORD = None, None
MAIL_DEFAULT_SENDER = 'eos@localhost'
# 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_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxx'

6839
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

9
package.json Normal file
View File

@ -0,0 +1,9 @@
{
"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,16 +1,17 @@
Authlib==0.14.3
coverage==4.4.1
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
libsass==0.13.4
premailer==3.1.1
psycopg2==2.7.3.2
psycopg2==2.8.5
PyExecJS==1.4.1
pymongo==3.5.1
pymongo[srv]==3.10.1
pyRCV==0.3
pytz==2017.3
timeago==1.0.8
Transcrypt==3.6.60
Transcrypt==3.9.0

View File

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