Skip to content

Commit

Permalink
using json to store ballots
Browse files Browse the repository at this point in the history
  • Loading branch information
mdipierro committed May 7, 2017
1 parent 09cd634 commit 5afbf37
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 107 deletions.
50 changes: 28 additions & 22 deletions controllers/default.py
@@ -1,6 +1,5 @@
from ballot import ballot2form, form2ballot, blank_ballot, \
sign, uuid, regex_email, unpack_results, rsakeys, \
pack_counters, unpack_counters
sign, uuid, regex_email, rsakeys
from ranking_algorithms import iro, borda, schulze
import re

Expand Down Expand Up @@ -109,7 +108,7 @@ def self_service():
form = SQLFORM.factory(
Field('election_id','integer',requires=IS_NOT_EMPTY()),
Field('email',requires=IS_EMAIL()))
if form.process.accepted():
if form.process().accepted:
election = db.election(form.vars.id)
if not election: form.errors['election_id'] = 'Invalid'
voter = db.voter(election=election_id,email=form.vars.email)
Expand Down Expand Up @@ -189,22 +188,25 @@ def recompute_results():
redirect(URL('results',args=election.id))

def compute_results(election):
voted_ballots = db(db.ballot.election_id==election.id
)(db.ballot.voted==True).select()
query = db.ballot.election_id==election.id
voted_ballots = db(query)(db.ballot.voted==True).select()
counters = {}
rankers = {}
for k,ballot in enumerate(voted_ballots):
results = unpack_results(ballot.results)
for key in results:
for k,ballot in enumerate(voted_ballots):
for name in ballot.results:
# name is the name of a group as in {{name:ranking}}
# scheme is "ranking" or "checkbox" (default)
# value is the <input value="value"> assigned to this checkbox or input
(name,scheme,value) = key
if scheme == 'checkbox':

# INPORTANT ONLY SUPPORT SIMPLE MAJORITY
key = name +'/simple-majority/' + ballot.results[name]
(name,scheme,value) = key.split('/',3)
if scheme == 'simple-majority':
# counters[key] counts how many times this checkbox was checked
counters[key] = counters.get(key,0) + (
1 if results[key] else 0)
counters[key] = counters.get(key,0) + 1

elif scheme == 'ranking':
raise NotImplementedError
# rankers[name] = [[2,1,3],[3,1,2],[1,2,3],...]
# The sublists in rankers mean:
# [[my first-preferred candidate is
Expand Down Expand Up @@ -240,18 +242,22 @@ def compute_results(election):
vote[ranking-1] = value
else:
raise RuntimeError("Invalid Voting Scheme")

for name in rankers:
votes = rankers[name]
cmajority = borda(votes,mode='exponential')
ciro = iro(votes)
cschulze = schulze(votes)
key = name+'/simple-majority/'+k
for (r,k) in cmajority:
counters[(name,'ranking',k)] = 'M:%s' % r
counters[key] = 'M:%s' % r
for (r,k) in ciro:
counters[(name,'ranking',k)] += ' I:%s' % r
counters[key] += ' I:%s' % r
for (r,k) in cschulze:
counters[(name,'ranking',k)] += ' S:%s' % r
election.update_record(counters=pack_counters(counters))
counters[key] += ' S:%s' % r

print counters
election.update_record(counters=counters)

#@cache(request.env.path_info,time_expire=300,cache_model=cache.ram)
def results():
Expand All @@ -265,8 +271,7 @@ def results():
if (DEBUG_MODE or not election.counters or
not election.deadline or request.now<=election.deadline):
compute_results(election)
form = ballot2form(election.ballot_model,
counters=unpack_counters(election.counters))
form = ballot2form(election.ballot_model, counters=election.counters)
return dict(form=form,election=election)

def hash_ballot(text):
Expand Down Expand Up @@ -314,7 +319,7 @@ def close_election():
import zipfile, os
election = db.election(request.args(0,cast=int)) or \
redirect(URL('invalid_link'))
check_closed(election)
#check_closed(election)
response.subtitle = election.title
dialog = FORM.confirm(T('Close'),
{T('Cancel'):URL('elections')})
Expand Down Expand Up @@ -404,9 +409,10 @@ def vote():
session.flash += T('Your vote was NOT recorded')
redirect(URL('results',args=election.id))
response.subtitle = election.title + ' / Vote'
form = ballot2form(election.ballot_model,readonly=False)
form = ballot2form(election.ballot_model, readonly=False)
form.process()
if form.accepted:
results = {}
results = form.vars
for_update = not db._uri.startswith('sqlite') # not suported by sqlite
#if not for_update: db.executesql('begin immediate transaction;')
ballot = db(db.ballot.election_id==election_id)\
Expand All @@ -417,7 +423,7 @@ def vote():
token=ballot.ballot_uuid,
vars=request.post_vars,results=results)
signature = 'signature-'+sign(ballot_content,election.private_key)
ballot.update_record(results=str(results),
ballot.update_record(results=results,
ballot_content=ballot_content,
signature=signature,
voted=True,assigned=True,voted_on=request.now)
Expand Down
4 changes: 2 additions & 2 deletions models/db_votes.py
Expand Up @@ -13,7 +13,7 @@
Field('not_voted_email','text'),
Field('public_key','text',writable=False,readable=False),
Field('private_key','text',writable=False,readable=False),
Field('counters','text',writable=False,readable=False),
Field('counters','json',writable=False,readable=False),
Field('closed','boolean',writable=False,readable=False),
auth.signature,
format='%(title)s')
Expand All @@ -33,7 +33,7 @@
Field('assigned','boolean',default=False),
Field('voted','boolean',default=False),
Field('voted_on','datetime',default=None),
Field('results','text',default='{}'),
Field('results','json',default={}),
Field('ballot_uuid'), # uuid embedded in ballot
Field('signature')) # signature of ballot (voted or blank)

Expand Down
3 changes: 3 additions & 0 deletions models/messages.py
Expand Up @@ -80,6 +80,9 @@ def meta_send2(to,message,reply_to,subject,sender=None):
fromaddr = mail.settings.sender
msg = "From: %s\r\nTo: %s\r\nSubject: %s\r\nReply-to: %s\r\n\r\n%s" \
% (fromaddr, to, subject, reply_to, message)
if mail.settings.server=='logging':
print msg
return True
try:
server = None
server = smtplib.SMTP(mail.settings.server,timeout=5)
Expand Down
103 changes: 39 additions & 64 deletions modules/ballot.py
@@ -1,6 +1,8 @@
from gluon import *
import re, hashlib, base64
import rsa
import json
import random
import cPickle as pickle
from uuid import uuid4
try:
Expand All @@ -25,70 +27,48 @@ def sign(text,privkey_pem):
signature = base64.b16encode(rsa.sign(text,privkey,'SHA-1'))
return signature

def unpack_results(results):
return ast.literal_eval(results) if have_ast else eval(results)

def ballot2form(ballot,readonly=False,counters=None,filled=False):
def ballot2form(ballot_model,readonly=False, vars=None, counters=None):
"""If counters is passed this counts the results in the ballot.
If readonly is False, then the voter has not yet voted; if readonly
is True, then they have just voted."""
radioes = {}
if isinstance(counters,dict): readonly=True
def radio(item):
name = "ck_"+item.group(1)
value = radioes[name] = radioes.get(name,0)+1
if item.group(2):
scheme = 'ranking'
ballot_structure = json.loads(ballot_model)
ballot = FORM()
for question in ballot_structure:
div =DIV(_class="question")
ballot.append(div)
div.append(MARKMIN(question['preamble']))
table = TABLE()
div.append(table)
name = question['name']
if counters:
options = []
for answer in question['answers']:
key = name+'/simple-majority/'+answer
options.append((counters.get(key,0), answer))
options.sort(reverse=True)
options = map(lambda a: a[1], options)
else:
scheme = 'checkbox'
key = (name,scheme,value)
if isinstance(counters,dict):
return INPUT(_type='text',_readonly=True,
_value=counters.get(key,0),
_class='model-'+scheme,
_style="width:3em").xml()
### CHECK THIS!
#if counters is not None and 'x' in item.group().lower():
# counters[key] = counters.get(key,0)+1
### CHECK THIS!
if scheme == 'checkbox':
return INPUT(_type='radio',_name=name,_value=value,
_checked=('!' in item.group()),
_class='model-'+scheme,
_disabled=readonly).xml()
elif scheme == 'ranking':
name = name+'-%s-%s' % (item.group(2)[1:], value)
return INPUT(_type='input',_name=name,_value=value,
_checked=('!' in item.group()),
_class='model-'+scheme,
_disabled=readonly,
).xml()
body = regex_field.sub(radio,ballot.replace('\r',''))
form = FORM(XML(body),not readonly and INPUT(_type='submit', _value="Submit Your Ballot!") or '',_class="ballot")
if not readonly: form.process(formname="ballot")
return form
options = question['answers']
if question['randomize']:
random.shuffle(options)
for answer in options:
key = name + '/simple-majority/' + answer
if not counters:
if question['algorithm'] == 'simple-majority':
inp = INPUT(_name=question['name'], _type="radio", _value=answer)
if vars and vars.get(name) == answer:
inp['_checked'] = True
if readonly:
inp['_readonly'] = True
else:
inp = STRONG(counters.get(key, 0))
table.append(TR(TD(inp),TD(answer)))
if not readonly and not counters:
ballot.append(INPUT(_type='submit', _value="Submit Your Ballot!"))
return ballot

def form2ballot(ballot,token,vars,results):
radioes = {}
def check(item):
name = 'ck_'+item.group(1)
value = radioes[name] = radioes.get(name,0)+1
if item.group(2):
scheme = item.group(2)[1:]
name2 = name+'-%s-%s' % (scheme, value)
rank = vars.get(name2,0)
if isinstance(results,dict):
results[(name,scheme,value)] = int(rank)
return INPUT(_type="input",_name=name2,_value=rank,
_disabled=True).xml()
else:
scheme = 'checkbox'
checked = vars.get(name,0)==str(value)
if isinstance(results,dict):
results[(name,scheme,value)] = checked
return INPUT(_type="radio",_name=name,_value=value,
_disabled=True,_checked=checked).xml()
ballot_content = regex_field.sub(check,ballot.replace('\r',''))
def form2ballot(ballot_model, token, vars, results):
ballot_content = ballot2form(ballot_model, readonly=True, vars=vars).xml()
if token: ballot_content += '<pre>\n%s\n</pre>' % token
return '<div class="ballot">%s</div>' % ballot_content.strip()

Expand All @@ -97,8 +77,3 @@ def blank_ballot(token):
if token: ballot_content += '<pre>\n%s\n</pre>' % token
return '<div class="ballot">%s</div>' % ballot_content

def pack_counters(counters):
return pickle.dumps(counters)

def unpack_counters(counters):
return pickle.loads(counters)
2 changes: 1 addition & 1 deletion private/appconfig.ini
Expand Up @@ -3,7 +3,7 @@
development = False
title = E-Vote
subtitle = Free Secure Trusted Verifiable Online Voting
as_service = False
as_service = True
google_analytics_id =
users_filename =
debug_mode = False
Expand Down
6 changes: 3 additions & 3 deletions static/css/stupid.css
Expand Up @@ -15,7 +15,7 @@ label, strong {font-weight:bold}
ul {list-style-type:none; padding-left:20px}
a {text-decoration:none; color:#26a69a; white-space:nowrap}
a:hover {cursor:pointer}
h1,h2,h3,h4,h5,h6{font-weight:bold; text-transform:uppercase}
h1,h2,h3,h4,h5,h6{font-weight:bold}
h1{font-size:4em; margin:1.0em 0 0.25em 0}
h2{font-size:2.4em; margin:0.9em 0 0.25em 0}
h3{font-size:1.8em; margin:0.8em 0 0.25em 0}
Expand Down Expand Up @@ -44,7 +44,7 @@ header, main, footer {display:block; with:100%} /* IE fix */
.btn:hover, button:hover, [type=button]:hover, [type=submit]:hover {box-shadow:0 0 10px #666; text-decoration:none; cursor:pointer}
.btn.small, table .btn {padding:0.25em 0.5em; font-size:0.8em}
.btn.large {padding:1em 2em; font-size:1.2em}
.btn.oval {border-radius:50%}
.oval {border-radius:50%}

/*** helpers ***/
.rounded {-moz-border-radius:5px; border-radius:5px}
Expand Down Expand Up @@ -189,7 +189,7 @@ input:invalid, input.error {background:#cc1f00;color:white}
.menu li {position:relative; float:left; margin:0; padding:0}
.menu ul {background:white; border:1px solid #e1e1e1; visibility:hidden; opacity:0; position:absolute; top:110%; padding:0; z-index:1000; transition:all 0.2s ease-out; list-style-type:none; box-shadow:5px 5px 10px #666}
.menu ul a {padding:10px 15px; color:#333; font-weight:700; font-size:12px; line-height:16px; display: block}
.menu ul li {float:none; width:200px}
.menu ul li {float:none}
.menu ul ul {top:0; left:80%; z-index:1100}
.menu li:hover > ul {visibility:visible; opacity:1}
.menu>li>ul>li:first-child:before{content:''; position:absolute; width:1px; height:1px; border:10px solid transparent; left:50px; top:-20px; margin-left:-10px; border-bottom-color:white}
Expand Down
54 changes: 54 additions & 0 deletions static/js/custom.js
@@ -0,0 +1,54 @@
function get_random() {
var d = new Date().getTime();
var uu8 = 'xxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (d + Math.random()*16)%16 | 0;
d = Math.floor(d/16);
return r;
});
return uu8;
};

let NEW_QUESTION = {'preamble':'', 'answers':['','',''], 'algorithm':'simple-majority', 'randomize':true, 'name': null};

let ALGORITHMS = ['simple-majority'] // ,'instant-runoff','borda','schulze'];
let app = {};
let init = (app) => {
app.data = {
questions: [],
algorithms: ALGORITHMS
};
app.methods = {
delete_question: function(q) {
app.vue.questions.splice(q,1);
},
delete_answer: function(q, a) {
app.vue.questions[q].answers.splice(a,1);
},
append_question: function() {
var question = JSON.parse(JSON.stringify(NEW_QUESTION));
question.name = get_random();
app.vue.questions.push(question);
},
append_answer: function(q) {
app.vue.questions[q].answers.push('');
}
};
app.filters = {
};
app.init = () => {
app.vue = new Vue({el: '#target-ballot',
data: app.data,
methods: app.methods,
filters:app.filters});

try {
app.vue.questions = JSON.parse(jQuery('#election_ballot_model').val());
} catch(e) {}
};
app.init();
};

init(app);
jQuery('form').submit(function(){
jQuery('#election_ballot_model').val(JSON.stringify(app.vue.questions));
});
8 changes: 8 additions & 0 deletions static/js/vue.min.js

Large diffs are not rendered by default.

0 comments on commit 5afbf37

Please sign in to comment.