Full end-to-end voting is working using the new binary encoding scheme, ballot combination and trustee partial decryption with tallies working perfectly. This required updating the Node server as well as Django models and views to support this. Emails to voters and trustees have also been updated to be more informative and look more professional. It could probably do at this point with using email templates and in the future HTML emails.
This commit is contained in:
parent
571cd723bc
commit
5b746ad406
14 changed files with 631 additions and 555 deletions
245
Node/index.js
245
Node/index.js
|
@ -40,7 +40,7 @@ app.get('/', function(request, response){
|
||||||
app.get('/param', function(request, response){
|
app.get('/param', function(request, response){
|
||||||
var param = gpGen();
|
var param = gpGen();
|
||||||
|
|
||||||
console.log('Generated Param:' + param);
|
console.log('Generated Group Param');
|
||||||
response.json(param);
|
response.json(param);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -98,164 +98,84 @@ app.post('/cmpkstring', function(request, response){
|
||||||
|
|
||||||
//addition function on homomorphically encrypted variables
|
//addition function on homomorphically encrypted variables
|
||||||
//this may need some work, different method of serialisation maybe?
|
//this may need some work, different method of serialisation maybe?
|
||||||
app.get('/addec', function(request, response){
|
app.post('/add_ciphers', function(request, response){
|
||||||
var c1 = request.query['C1'];
|
console.log("\nEndpoint /add_ciphers called");
|
||||||
var c2 = request.query['C2'];
|
const C1s = request.body.ciphers.c1s;
|
||||||
var number = request.query['number']; //number of ciphertexts to add
|
const C2s = request.body.ciphers.c2s;
|
||||||
//all the list of ciphertext objects to give to the function
|
const CIPHER_COUNT = C1s.length;
|
||||||
var parsed = [];
|
|
||||||
|
|
||||||
|
// Will store a list of parsed ciphers from the C1s and C2s arrays passed in
|
||||||
|
var parsedCiphers = [];
|
||||||
var ctx = new CTX("BN254CX");
|
var ctx = new CTX("BN254CX");
|
||||||
console.log('Addec:');
|
|
||||||
|
|
||||||
if(number == c1.length)
|
if(CIPHER_COUNT > 1)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < c1.length; i++) {
|
console.log("Combining " + CIPHER_COUNT + " ciphers");
|
||||||
console.log(i + ".C1: " + c1[i]);
|
|
||||||
var c1Bytes = Buffer.from(c1[i].split(','), 'hex');
|
for (var i = 0; i < CIPHER_COUNT; i++) {
|
||||||
|
|
||||||
|
var c1Bytes = Buffer.from(C1s[i].split(','), 'hex');
|
||||||
var newC1 = new ctx.ECP.fromBytes(c1Bytes);
|
var newC1 = new ctx.ECP.fromBytes(c1Bytes);
|
||||||
|
|
||||||
var cipher =
|
var c2Bytes = Buffer.from(C2s[i].split(','), 'hex');
|
||||||
{
|
var newC2 = new ctx.ECP.fromBytes(c2Bytes);
|
||||||
|
|
||||||
|
var cipher = {
|
||||||
C1 : newC1,
|
C1 : newC1,
|
||||||
C2:null
|
C2 : newC2
|
||||||
};
|
};
|
||||||
parsed.push(cipher);
|
|
||||||
|
|
||||||
|
parsedCiphers.push(cipher);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var j = 0; j < c2.length; j++) {
|
} else if(CIPHER_COUNT === 1) {
|
||||||
console.log(j + ".C2: " + c2[j]);
|
console.log("Combining only one cipher");
|
||||||
var c2Bytes = Buffer.from(c2[j].split(','), 'hex');
|
|
||||||
var newC2 = new ctx.ECP.fromBytes(c2Bytes);
|
|
||||||
|
|
||||||
parsed[j].C2 = newC2;
|
var c1Bytes = Buffer.from(C1s[0].split(','), 'hex');
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if(number == 1)
|
|
||||||
{
|
|
||||||
console.log("only one cipher");
|
|
||||||
var c1Bytes = Buffer.from(c1.split(','), 'hex');
|
|
||||||
var newC1 = new ctx.ECP.fromBytes(c1Bytes);
|
var newC1 = new ctx.ECP.fromBytes(c1Bytes);
|
||||||
console.log("C1: " + c1);
|
|
||||||
var c2Bytes = Buffer.from(c2.split(','), 'hex');
|
|
||||||
|
var c2Bytes = Buffer.from(C2s[0].split(','), 'hex');
|
||||||
var newC2 = new ctx.ECP.fromBytes(c2Bytes);
|
var newC2 = new ctx.ECP.fromBytes(c2Bytes);
|
||||||
console.log("C2: " + c2);
|
|
||||||
|
|
||||||
var cipher =
|
var cipher =
|
||||||
{
|
{
|
||||||
C1 : newC1,
|
C1 : newC1,
|
||||||
C2 : newC2
|
C2 : newC2
|
||||||
};
|
};
|
||||||
parsed.push(cipher);
|
|
||||||
|
parsedCiphers.push(cipher);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Combine the ciphers here
|
||||||
|
var combinedCipher = add(parsedCiphers);
|
||||||
|
|
||||||
response.json(add(parsed));
|
// Get the byte string of the C1 and C2 part for transmission
|
||||||
});
|
var C1Bytes = [];
|
||||||
|
combinedCipher.C1.toBytes(C1Bytes);
|
||||||
|
|
||||||
|
var C2Bytes = [];
|
||||||
|
combinedCipher.C2.toBytes(C2Bytes);
|
||||||
|
|
||||||
//tally partially decrypted ciphertexts
|
var responseData = {
|
||||||
app.get('/tally', function(request, response){
|
C1: C1Bytes.toString(),
|
||||||
console.log("\nEndpoint /tally called");
|
C2: C2Bytes.toString()
|
||||||
var amount = request.query['number'];//number of decryptions taking in
|
|
||||||
var paramString = request.query['param'];//event group parameter in JSON
|
|
||||||
var partialsStrings = request.query['decs'];//array of partial decryption(s) in bytes
|
|
||||||
var ciphertextString = request.query['cipher'];//ciphertext being decrypted in JSON
|
|
||||||
|
|
||||||
//re-build parameters
|
|
||||||
var tempParams = JSON.parse(JSON.parse(paramString).crypto);
|
|
||||||
var ctx = new CTX("BN254CX"); //new context we can use
|
|
||||||
var n = new ctx.BIG();
|
|
||||||
var g1 = new ctx.ECP();
|
|
||||||
var g2 = new ctx.ECP2();
|
|
||||||
|
|
||||||
//copying the values
|
|
||||||
n.copy(tempParams.n);
|
|
||||||
g1.copy(tempParams.g1);
|
|
||||||
g2.copy(tempParams.g2);
|
|
||||||
|
|
||||||
var params = {
|
|
||||||
n:n,
|
|
||||||
g1:g1,
|
|
||||||
g2:g2
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//re-build partial decryptions
|
response.json(responseData);
|
||||||
var partials = [];
|
|
||||||
if(amount == partialsStrings.length)
|
|
||||||
{
|
|
||||||
console.log(amount + " partial decryptions");
|
|
||||||
for(var i = 0; i < partialsStrings.length; i++)
|
|
||||||
{
|
|
||||||
var bytes = Buffer.from(partialsStrings[i].split(','), 'hex');
|
|
||||||
|
|
||||||
var dec = {
|
|
||||||
D:new ctx.ECP.fromBytes(bytes)
|
|
||||||
};
|
|
||||||
|
|
||||||
partials.push(dec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(amount == 1)
|
|
||||||
{
|
|
||||||
console.log("\nOnly one partial decryption received\n");
|
|
||||||
console.log(JSON.parse(paramString).crypto + "\n");
|
|
||||||
|
|
||||||
var bytes = Buffer.from(partialsStrings.split(','), 'hex');
|
|
||||||
var dec = {
|
|
||||||
D : new ctx.ECP.fromBytes(bytes)
|
|
||||||
};
|
|
||||||
|
|
||||||
partials.push(dec);
|
|
||||||
}
|
|
||||||
|
|
||||||
//re-build combined ciphertext
|
|
||||||
var tempCipher = JSON.parse(ciphertextString);
|
|
||||||
|
|
||||||
var cipher = {
|
|
||||||
C1: new ctx.ECP(),
|
|
||||||
C2: new ctx.ECP()
|
|
||||||
};
|
|
||||||
|
|
||||||
cipher.C1.copy(tempCipher.C1);
|
|
||||||
cipher.C2.copy(tempCipher.C2);
|
|
||||||
|
|
||||||
response.json(tally(params, partials, cipher))
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/comb_sks', function(request, response){
|
|
||||||
console.log("\nEndpoint /comb_sks called");
|
|
||||||
const SKsAsStrings = request.body.SKs;
|
|
||||||
|
|
||||||
// Parse and combine the secret keys
|
|
||||||
var ctx = new CTX("BN254CX");
|
|
||||||
var parsedSKs = [];
|
|
||||||
|
|
||||||
for(var i = 0; i < SKsAsStrings.length; i++) {
|
|
||||||
var skBytes = SKsAsStrings[i].split(",");
|
|
||||||
parsedSKs.push(new ctx.BIG.fromBytes(skBytes));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Combining " + parsedSKs.length + " SKs...");
|
|
||||||
var SKBytes = [];
|
|
||||||
combine_sks(parsedSKs).SK.toBytes(SKBytes);
|
|
||||||
|
|
||||||
response.send(SKBytes.toString());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/get_tally', function(request, response){
|
app.post('/get_tally', function(request, response){
|
||||||
const COUNT = request.body.count;
|
console.log("\nEndpoint /get_tally called");
|
||||||
|
|
||||||
|
// Extract the data from the request
|
||||||
const TEMP_PARAMS = JSON.parse(JSON.parse(request.body.param).crypto);
|
const TEMP_PARAMS = JSON.parse(JSON.parse(request.body.param).crypto);
|
||||||
const C1s = request.body.ciphers.c1s;
|
const BALLOT_CIPHER = request.body.ballot_cipher;
|
||||||
const C2s = request.body.ciphers.c2s;
|
const PART_DECS = request.body.part_decs;
|
||||||
const SK = request.body.sk;
|
const VOTERS_COUNT = request.body.voters_count;
|
||||||
|
|
||||||
console.log("\nFrom /get_tally - C1 array length (num of voters for the opt): " + C1s.length);
|
// Re-build parameters
|
||||||
|
var ctx = new CTX("BN254CX");
|
||||||
//re-build parameters
|
|
||||||
var ctx = new CTX("BN254CX"); //new context we can use
|
|
||||||
var n = new ctx.BIG();
|
var n = new ctx.BIG();
|
||||||
var g1 = new ctx.ECP();
|
var g1 = new ctx.ECP();
|
||||||
var g2 = new ctx.ECP2();
|
var g2 = new ctx.ECP2();
|
||||||
|
@ -270,26 +190,34 @@ app.post('/get_tally', function(request, response){
|
||||||
g2 : g2
|
g2 : g2
|
||||||
};
|
};
|
||||||
|
|
||||||
//rebuild our secret key
|
// Initialise the ballot cipher
|
||||||
var skBytes = SK.split(",");
|
var c1Bytes = Buffer.from(BALLOT_CIPHER.C1.split(','), 'hex');
|
||||||
var sk = new ctx.BIG.fromBytes(skBytes);
|
|
||||||
|
|
||||||
var tally = 0;
|
|
||||||
|
|
||||||
for(var i = 0; i < COUNT; i++) {
|
|
||||||
var c1Bytes = Buffer.from(C1s[i].split(','), 'hex');
|
|
||||||
var newC1 = new ctx.ECP.fromBytes(c1Bytes);
|
var newC1 = new ctx.ECP.fromBytes(c1Bytes);
|
||||||
|
|
||||||
var c2Bytes = Buffer.from(C2s[i].split(','), 'hex');
|
var c2Bytes = Buffer.from(BALLOT_CIPHER.C2.split(','), 'hex');
|
||||||
var newC2 = new ctx.ECP.fromBytes(c2Bytes);
|
var newC2 = new ctx.ECP.fromBytes(c2Bytes);
|
||||||
|
|
||||||
var cipher = {C1: newC1, C2: newC2};
|
var cipher =
|
||||||
tally += decrypt(params, sk, cipher).M;
|
{
|
||||||
|
C1 : newC1,
|
||||||
|
C2 : newC2
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialise all of the partial decryptions
|
||||||
|
var partials = [];
|
||||||
|
for(var i = 0; i < PART_DECS.length; i++)
|
||||||
|
{
|
||||||
|
var bytes = Buffer.from(PART_DECS[i].split(','), 'hex');
|
||||||
|
|
||||||
|
var dec = {
|
||||||
|
D : new ctx.ECP.fromBytes(bytes)
|
||||||
|
};
|
||||||
|
|
||||||
|
partials.push(dec);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Tally: " + tally + "\n");
|
// Send the decrypted cipher value (vote tally for an option)
|
||||||
|
response.send("" + getCipherVal(params, partials, cipher, VOTERS_COUNT).M);
|
||||||
response.send("" + tally);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var server = app.listen(port, function(){
|
var server = app.listen(port, function(){
|
||||||
|
@ -445,16 +373,15 @@ add=function(Ciphers){
|
||||||
var ctx = new CTX("BN254CX");
|
var ctx = new CTX("BN254CX");
|
||||||
var s1 = new ctx.ECP();
|
var s1 = new ctx.ECP();
|
||||||
var s2 = new ctx.ECP();
|
var s2 = new ctx.ECP();
|
||||||
|
|
||||||
//copy the first cipher
|
//copy the first cipher
|
||||||
s1.copy(Ciphers[0].C1);
|
s1.copy(Ciphers[0].C1);
|
||||||
s2.copy(Ciphers[0].C2);
|
s2.copy(Ciphers[0].C2);
|
||||||
|
|
||||||
//multiple the rest ciphertexts
|
//multiple the rest ciphertexts
|
||||||
for(i=1;i<Ciphers.length;i++){
|
for(var i = 1; i < Ciphers.length; i++){
|
||||||
s1.add(Ciphers[i].C1);
|
s1.add(Ciphers[i].C1);
|
||||||
}
|
s2.add(Ciphers[i].C2);
|
||||||
//no idea why I need two loops
|
|
||||||
for(j=1;j<Ciphers.length;j++){
|
|
||||||
s2.add(Ciphers[j].C2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -465,7 +392,7 @@ add=function(Ciphers){
|
||||||
|
|
||||||
|
|
||||||
//ElGamal decryption
|
//ElGamal decryption
|
||||||
decrypt=function(params,SK, C){
|
decrypt = function(params,SK, C, votersCount) {
|
||||||
var ctx = new CTX("BN254CX");
|
var ctx = new CTX("BN254CX");
|
||||||
var D=new ctx.ECP();
|
var D=new ctx.ECP();
|
||||||
D = ctx.PAIR.G1mul(C.C1,SK);
|
D = ctx.PAIR.G1mul(C.C1,SK);
|
||||||
|
@ -474,9 +401,9 @@ decrypt=function(params,SK, C){
|
||||||
gM.copy(C.C2);
|
gM.copy(C.C2);
|
||||||
gM.sub(D);
|
gM.sub(D);
|
||||||
|
|
||||||
//search for message by brute force
|
// Search for value based on the number of voters
|
||||||
var B;
|
var B;
|
||||||
for (j = 0; j < 1000; j++) {
|
for (var j = 0; j <= votersCount; j++) {
|
||||||
//use D as temp var
|
//use D as temp var
|
||||||
B = new ctx.BIG(j);
|
B = new ctx.BIG(j);
|
||||||
D = ctx.PAIR.G1mul(params.g1,B);
|
D = ctx.PAIR.G1mul(params.g1,B);
|
||||||
|
@ -485,8 +412,7 @@ decrypt=function(params,SK, C){
|
||||||
M:j
|
M:j
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
return{
|
return{
|
||||||
M: "Error"
|
M: "Error"
|
||||||
|
@ -507,15 +433,16 @@ partDec=function(SK, C){
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Tally, combine partial decryption
|
// Combines partial decryptions to enable the decryption of a cipher text which will be an int val representing
|
||||||
//Ds is the array of partial decryptions; C is the ciphertext.
|
// a tally of votes for an option. Ds is the array of partial decryptions; C is the ciphertext.
|
||||||
tally=function(params,Ds, C){
|
getCipherVal = function(params, Ds, C, votersCount) {
|
||||||
|
// Create a context and initialise the first decryption part
|
||||||
var ctx = new CTX("BN254CX");
|
var ctx = new CTX("BN254CX");
|
||||||
var D = new ctx.ECP();
|
var D = new ctx.ECP();
|
||||||
D.copy(Ds[0].D);
|
D.copy(Ds[0].D);
|
||||||
|
|
||||||
//combine D
|
// Combine the decryptions (in Ds array) into a single decryption by adding them to D
|
||||||
for(i=1;i<Ds.length;i++){
|
for(var i = 1; i < Ds.length; i++){
|
||||||
D.add(Ds[i].D);
|
D.add(Ds[i].D);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -524,9 +451,9 @@ tally=function(params,Ds, C){
|
||||||
gM.copy(C.C2);
|
gM.copy(C.C2);
|
||||||
gM.sub(D);
|
gM.sub(D);
|
||||||
|
|
||||||
//search for message by brute force
|
// Search for the value based on the number of voters
|
||||||
var B;
|
var B;
|
||||||
for (var j = 0; j < 1000; j++) {
|
for (var j = 0; j <= votersCount; j++) {
|
||||||
//use D as temp var
|
//use D as temp var
|
||||||
B = new ctx.BIG(j);
|
B = new ctx.BIG(j);
|
||||||
D = ctx.PAIR.G1mul(params.g1,B);
|
D = ctx.PAIR.G1mul(params.g1,B);
|
||||||
|
@ -537,7 +464,7 @@ tally=function(params,Ds, C){
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the search failed
|
||||||
return{
|
return{
|
||||||
M: "Error"
|
M: "Error"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.contrib import admin
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|
||||||
from allauthdemo.auth.models import DemoUser
|
from allauthdemo.auth.models import DemoUser
|
||||||
from .models import Event, PollOption, Poll, Organiser
|
from .models import Event, PollOption, Poll
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,12 @@ import urllib2
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
All functions in this file have been re-implemenented by Thomas Smith
|
All functions in this file have been re-implemenented by Vincent de Almeida
|
||||||
|
|
||||||
File then updated by Vincent de Almeida. Changes include:
|
Changes include:
|
||||||
-Update filename to 'crypto_rpc' to reflect the RPC nature of the methods
|
-Update filename to 'crypto_rpc' to reflect the RPC nature of the methods
|
||||||
-Modified RPC calls that send data to POST requests to avoid large query URLs
|
-Modified RPC calls that send data to POST requests to avoid large query URLs (using a helper function)
|
||||||
|
-Added a new cipher combination and tally function
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -39,58 +40,24 @@ def combpk(pks):
|
||||||
return send_post_req(url, data)
|
return send_post_req(url, data)
|
||||||
|
|
||||||
|
|
||||||
def addec(amount, ciphers):
|
def add_ciphers(ciphers):
|
||||||
url = 'http://localhost:8080/addec'
|
url = 'http://localhost:8080/add_ciphers'
|
||||||
querystring = '?number='+str(amount)
|
|
||||||
c1s = ciphers['c1s']
|
|
||||||
c2s = ciphers['c2s']
|
|
||||||
for i, value in enumerate(c1s):
|
|
||||||
querystring += "&C1="+str(c1s[i])
|
|
||||||
querystring += "&C2="+str(c2s[i])
|
|
||||||
|
|
||||||
print(url+querystring)
|
|
||||||
jsondict = json.load(urllib2.urlopen(url+querystring))
|
|
||||||
print(json.dumps(jsondict))
|
|
||||||
return json.dumps(jsondict)
|
|
||||||
|
|
||||||
|
|
||||||
# Deprecated functionality and has been superseded by get_tally
|
|
||||||
def tally(amount, group_param, decs, cipher):
|
|
||||||
url = 'http://localhost:8080/tally'
|
|
||||||
querystring = '?number='+str(amount)
|
|
||||||
querystring += '¶m='+urllib2.quote(str(group_param))
|
|
||||||
|
|
||||||
for i, value in enumerate(decs):
|
|
||||||
querystring += "&decs="+str(value)
|
|
||||||
|
|
||||||
querystring += '&cipher=' + urllib2.quote(str(cipher))
|
|
||||||
|
|
||||||
jsondict = json.load(urllib2.urlopen(url+querystring))
|
|
||||||
|
|
||||||
return str(jsondict['M'])
|
|
||||||
|
|
||||||
|
|
||||||
def combine_sks(sks):
|
|
||||||
url = 'http://localhost:8080/comb_sks'
|
|
||||||
|
|
||||||
# Construct POST data
|
|
||||||
data = {}
|
data = {}
|
||||||
data['SKs'] = sks
|
data['ciphers'] = ciphers
|
||||||
|
|
||||||
# Return the new combined SK
|
return json.loads(send_post_req(url, data))
|
||||||
return send_post_req(url, data)
|
|
||||||
|
|
||||||
|
|
||||||
def get_tally(count, ciphers, sk, group_param):
|
def get_tally(ballot_cipher, part_decs, group_param, voters_count):
|
||||||
url = 'http://localhost:8080/get_tally'
|
url = 'http://localhost:8080/get_tally'
|
||||||
|
|
||||||
# Construct POST data
|
# Construct POST data
|
||||||
data = {}
|
data = {}
|
||||||
data['count'] = count
|
data['ballot_cipher'] = ballot_cipher
|
||||||
data['ciphers'] = ciphers
|
data['part_decs'] = part_decs
|
||||||
data['sk'] = sk
|
|
||||||
data['param'] = group_param
|
data['param'] = group_param
|
||||||
|
data['voters_count'] = voters_count
|
||||||
|
|
||||||
# Return the tally of votes for the option
|
|
||||||
return send_post_req(url, data)
|
return send_post_req(url, data)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ from crispy_forms.layout import LayoutObject, Layout, TEMPLATE_PACK, Fieldset, B
|
||||||
from crispy_forms.bootstrap import StrictButton, TabHolder, Tab, FormActions, PrependedText, PrependedAppendedText, Accordion, AccordionGroup
|
from crispy_forms.bootstrap import StrictButton, TabHolder, Tab, FormActions, PrependedText, PrependedAppendedText, Accordion, AccordionGroup
|
||||||
from captcha.fields import ReCaptchaField
|
from captcha.fields import ReCaptchaField
|
||||||
from allauthdemo.auth.models import DemoUser
|
from allauthdemo.auth.models import DemoUser
|
||||||
from .models import Event, Poll, PollOption, Organiser
|
from .models import Event, Poll, PollOption
|
||||||
|
|
||||||
def is_valid_email(email):
|
def is_valid_email(email):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
|
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -8,6 +9,7 @@ from django.utils import timezone
|
||||||
|
|
||||||
from allauthdemo.auth.models import DemoUser
|
from allauthdemo.auth.models import DemoUser
|
||||||
|
|
||||||
|
|
||||||
class EmailUser(models.Model):
|
class EmailUser(models.Model):
|
||||||
email = models.CharField(max_length=80, unique=True)
|
email = models.CharField(max_length=80, unique=True)
|
||||||
|
|
||||||
|
@ -35,16 +37,23 @@ class Event(models.Model):
|
||||||
creator = models.CharField(max_length=256, blank=True)
|
creator = models.CharField(max_length=256, blank=True)
|
||||||
c_email = models.CharField(max_length=512, blank=True)
|
c_email = models.CharField(max_length=512, blank=True)
|
||||||
trustees = models.CharField(max_length=4096)
|
trustees = models.CharField(max_length=4096)
|
||||||
|
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
|
||||||
# Custom helper methods
|
# Custom helper methods
|
||||||
def EID_hr(self):
|
def EID_hr(self):
|
||||||
|
try:
|
||||||
EID_json = json.loads(self.EID)
|
EID_json = json.loads(self.EID)
|
||||||
return EID_json['hr']
|
return EID_json['hr']
|
||||||
|
except ValueError:
|
||||||
|
return self.EID
|
||||||
|
|
||||||
def EID_crypto(self):
|
def EID_crypto(self):
|
||||||
|
try:
|
||||||
EID_json = json.loads(self.EID)
|
EID_json = json.loads(self.EID)
|
||||||
EID_crypto_str = EID_json['crypto']
|
EID_crypto_str = EID_json['crypto']
|
||||||
return json.loads(EID_crypto_str)
|
return json.loads(EID_crypto_str)
|
||||||
|
except ValueError:
|
||||||
|
return "None - Event not Initialised"
|
||||||
|
|
||||||
def duration(self):
|
def duration(self):
|
||||||
duration_str = self.start_time_formatted()
|
duration_str = self.start_time_formatted()
|
||||||
|
@ -63,26 +72,55 @@ class Event(models.Model):
|
||||||
def end_time_formatted_utc(self):
|
def end_time_formatted_utc(self):
|
||||||
return self.end_time.strftime("%d-%m-%y %H:%M %Z")
|
return self.end_time.strftime("%d-%m-%y %H:%M %Z")
|
||||||
|
|
||||||
|
# Total number of options in all polls
|
||||||
|
def total_num_opts(self):
|
||||||
|
polls = self.polls.all()
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
for poll in polls:
|
||||||
|
count += poll.options.all().count()
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
def total_num_partial_decs(self):
|
||||||
|
polls = self.polls.all()
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
for poll in polls:
|
||||||
|
count += PartialBallotDecryption.objects.filter(poll=poll).count()
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
def all_part_decs_received(self):
|
||||||
|
received = False
|
||||||
|
|
||||||
|
if self.total_num_partial_decs() == self.total_num_opts() * self.users_trustees.all().count():
|
||||||
|
received = True
|
||||||
|
|
||||||
|
return received
|
||||||
|
|
||||||
def status(self):
|
def status(self):
|
||||||
status_str = ""
|
status_str = ""
|
||||||
|
|
||||||
# Get the current date and time to compare against to establish if this is a past, current or
|
# Get the current date and time to compare against to establish if this is a past, current or
|
||||||
# future event
|
# future event. Prepared means the public key has been initialised
|
||||||
present = timezone.now()
|
present = timezone.now()
|
||||||
|
|
||||||
if self.ended is False:
|
if self.ended is False:
|
||||||
if present < self.start_time and self.public_key is None:
|
if present < self.start_time and self.prepared is False:
|
||||||
status_str = "Future"
|
status_str = "Future"
|
||||||
elif present < self.start_time and self.public_key is not None:
|
elif present < self.start_time and self.prepared is True:
|
||||||
status_str = "Prepared"
|
status_str = "Prepared"
|
||||||
elif present >= self.start_time and present <= self.end_time and self.public_key is not None:
|
elif present >= self.start_time and present <= self.end_time and self.prepared is True:
|
||||||
status_str = "Active"
|
status_str = "Active"
|
||||||
elif present > self.end_time and self.public_key is not None:
|
elif present >= self.start_time and present <= self.end_time and self.prepared is False:
|
||||||
|
status_str = "Future"
|
||||||
|
elif present > self.end_time:
|
||||||
status_str = "Expired"
|
status_str = "Expired"
|
||||||
else:
|
else:
|
||||||
if self.event_sk.all().count() == 1:
|
if self.all_part_decs_received():
|
||||||
status_str = "Decrypted"
|
status_str = "Decrypted"
|
||||||
elif self.event_sk.all().count() == 0:
|
else:
|
||||||
status_str = "Ended"
|
status_str = "Ended"
|
||||||
|
|
||||||
return status_str
|
return status_str
|
||||||
|
@ -109,13 +147,12 @@ class TrusteeKey(models.Model):
|
||||||
user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="trustee_keys")
|
user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="trustee_keys")
|
||||||
key = models.CharField(max_length=255, unique=True)
|
key = models.CharField(max_length=255, unique=True)
|
||||||
|
|
||||||
|
|
||||||
class AccessKey(models.Model):
|
class AccessKey(models.Model):
|
||||||
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="keys")
|
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="keys")
|
||||||
user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="keys")
|
user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="keys")
|
||||||
key = models.CharField(max_length=255, unique=True)
|
key = models.CharField(max_length=255, unique=True)
|
||||||
|
|
||||||
#total = models.IntegerField(blank=True, null=True, default=0)
|
|
||||||
|
|
||||||
def has_started(self):
|
def has_started(self):
|
||||||
return timezone.now() >= self.start
|
return timezone.now() >= self.start
|
||||||
|
|
||||||
|
@ -125,60 +162,60 @@ class AccessKey(models.Model):
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
|
||||||
class Poll(models.Model):
|
class Poll(models.Model):
|
||||||
question_text = models.CharField(max_length=200)
|
question_text = models.CharField(max_length=200)
|
||||||
total_votes = models.IntegerField(default=0)
|
total_votes = models.IntegerField(default=0)
|
||||||
min_num_selections = models.IntegerField(default=0)
|
min_num_selections = models.IntegerField(default=0)
|
||||||
max_num_selections = models.IntegerField(default=1)
|
max_num_selections = models.IntegerField(default=1)
|
||||||
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="polls")
|
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="polls")
|
||||||
enc = models.CharField(max_length=4096, null=True)
|
combined_ballots = models.CharField(max_length=4096, null=True)
|
||||||
|
result_json = models.CharField(max_length=4096, null=True)
|
||||||
#index = models.IntegerField()
|
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.question_text
|
return self.question_text
|
||||||
|
|
||||||
|
|
||||||
class PollOption(models.Model):
|
class PollOption(models.Model):
|
||||||
choice_text = models.CharField(max_length=200)
|
choice_text = models.CharField(max_length=200)
|
||||||
votes = models.IntegerField(default=0)
|
votes = models.IntegerField(default=0)
|
||||||
question = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="options")
|
question = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="options")
|
||||||
#index = models.IntegerField()
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.choice_text
|
return self.choice_text
|
||||||
|
|
||||||
class Decryption(models.Model):
|
|
||||||
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="decryptions")
|
|
||||||
poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="decryptions")
|
|
||||||
user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="decryptions")
|
|
||||||
text = models.CharField(max_length=1024)
|
|
||||||
|
|
||||||
class TrusteeSK(models.Model):
|
class CombinedBallot(models.Model):
|
||||||
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="trustee_sk")
|
poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="combined_ballot")
|
||||||
trustee = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="trustee_sk")
|
option = models.ForeignKey(PollOption, on_delete=models.CASCADE, related_name="combined_ballot")
|
||||||
key = models.CharField(max_length=1024)
|
cipher_text_c1 = models.CharField(max_length=4096)
|
||||||
|
cipher_text_c2 = models.CharField(max_length=4096)
|
||||||
|
|
||||||
|
|
||||||
|
# A partial decryption supplied by a trustee for a combined ballot that relates to a poll option
|
||||||
|
class PartialBallotDecryption(models.Model):
|
||||||
|
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="decryption")
|
||||||
|
poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="decryption")
|
||||||
|
option = models.ForeignKey(PollOption, on_delete=models.CASCADE, related_name="decryption")
|
||||||
|
user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="decryption")
|
||||||
|
text = models.CharField(max_length=4096)
|
||||||
|
|
||||||
class EventSK(models.Model):
|
|
||||||
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="event_sk")
|
|
||||||
key = models.CharField(max_length=1024)
|
|
||||||
|
|
||||||
class Ballot(models.Model):
|
class Ballot(models.Model):
|
||||||
voter = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="ballots")
|
voter = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="ballots")
|
||||||
poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="ballots")
|
poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="ballots")
|
||||||
cast = models.BooleanField(default=False)
|
cast = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
# Implements the new binary encoding scheme
|
# Implements the new binary encoding scheme
|
||||||
class EncryptedVote(models.Model):
|
class EncryptedVote(models.Model):
|
||||||
ballot = models.ForeignKey(Ballot, on_delete=models.CASCADE, related_name="encrypted_vote")
|
ballot = models.ForeignKey(Ballot, on_delete=models.CASCADE, related_name="encrypted_vote")
|
||||||
|
|
||||||
|
|
||||||
class VoteFragment(models.Model):
|
class VoteFragment(models.Model):
|
||||||
encrypted_vote = models.ForeignKey(EncryptedVote, on_delete=models.CASCADE, related_name="fragment")
|
encrypted_vote = models.ForeignKey(EncryptedVote, on_delete=models.CASCADE, related_name="fragment")
|
||||||
cipher_text_c1 = models.CharField(max_length=4096)
|
cipher_text_c1 = models.CharField(max_length=4096)
|
||||||
cipher_text_c2 = models.CharField(max_length=4096)
|
cipher_text_c2 = models.CharField(max_length=4096)
|
||||||
|
|
||||||
class Organiser(models.Model):
|
|
||||||
index = models.IntegerField(default=0)
|
|
||||||
email = models.CharField(max_length=100, blank=False, null=False)
|
|
||||||
event = models.ForeignKey(Event, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ from celery import task
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from allauthdemo.polls.models import AccessKey, Ballot, Decryption, TrusteeSK, EventSK
|
from allauthdemo.polls.models import AccessKey, Ballot, CombinedBallot, PartialBallotDecryption
|
||||||
|
|
||||||
from .crypto_rpc import param, combpk, addec, tally, get_tally, combine_sks
|
from .crypto_rpc import param, combpk, add_ciphers, get_tally
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Goal: This py file defines celery tasks that can be initiated
|
Goal: This py file defines celery tasks that can be initiated
|
||||||
|
@ -22,28 +22,85 @@ from .crypto_rpc import param, combpk, addec, tally, get_tally, combine_sks
|
||||||
# Will store the result of the initial cal to param() from .cpp_calls
|
# Will store the result of the initial cal to param() from .cpp_calls
|
||||||
group_param = None
|
group_param = None
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Helper functions
|
Helper functions
|
||||||
|
|
||||||
gen_access_key - Will generate an a key for accessing either the event preparation page, voting page and decryption page
|
gen_access_key - Will generate an a key for accessing either the event preparation page, voting page and decryption page
|
||||||
|
email_trustees_dec - Will email trustees a link to begin decrypting the event
|
||||||
|
|
||||||
'''
|
'''
|
||||||
def gen_access_key():
|
def gen_access_key():
|
||||||
return base64.urlsafe_b64encode(urandom(16)).decode('utf-8')
|
return base64.urlsafe_b64encode(urandom(16)).decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
def email_trustees_dec(event):
|
def email_trustees_dec(event):
|
||||||
email_subject = "Event Ballot Decryption for '" + event.title + "'"
|
email_subject = "Event Ballot Decryption for '" + event.title + "'"
|
||||||
|
|
||||||
# Plain text email - this could be replaced for a HTML-based email in the future
|
# Plain text email - this could be replaced for a HTML-based email in the future
|
||||||
email_body = "Please visit the following URL to submit your trustee secret key to begin event decryption:\n\n"
|
email_body_base = str("")
|
||||||
|
email_body_base += "Dear Trustee,\n\n"
|
||||||
|
email_body_base += "You're now required to decrypt the event: " + event.title + \
|
||||||
|
". This will require uploading your secret key that you have previously backed up.\n\n"
|
||||||
|
email_body_base += "Please visit the following URL to submit your trustee secret key to begin event decryption:\n\n"
|
||||||
url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/decrypt/?key="
|
url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/decrypt/?key="
|
||||||
email_body = email_body + url_base
|
email_body_base += url_base
|
||||||
|
|
||||||
|
sign_off = get_email_sign_off()
|
||||||
|
|
||||||
for trustee in event.users_trustees.all():
|
for trustee in event.users_trustees.all():
|
||||||
# Generate a key and create an AccessKey object
|
# Generate a key and create an AccessKey object
|
||||||
key = gen_access_key()
|
key = gen_access_key()
|
||||||
AccessKey.objects.create(user=trustee, event=event, key=key)
|
AccessKey.objects.create(user=trustee, event=event, key=key)
|
||||||
|
|
||||||
trustee.send_email(email_subject, email_body + key)
|
email_body = str(email_body_base + key)
|
||||||
|
email_body += sign_off
|
||||||
|
|
||||||
|
trustee.send_email(email_subject, email_body)
|
||||||
|
|
||||||
|
def get_email_sign_off():
|
||||||
|
sign_off = str("")
|
||||||
|
sign_off += "\n\nPlease note: This email address is not monitored so please don't reply to this email.\n\n"
|
||||||
|
sign_off += "Kind Regards,\n"
|
||||||
|
sign_off += "DEMOS 2 Admin - Lancaster University"
|
||||||
|
|
||||||
|
return sign_off
|
||||||
|
|
||||||
|
'''
|
||||||
|
Combines all of the voter ballots for a poll option into a single 'CombinedBallot'
|
||||||
|
'''
|
||||||
|
def combine_ballots(polls):
|
||||||
|
for poll in polls:
|
||||||
|
options = poll.options.all()
|
||||||
|
opt_count = len(options)
|
||||||
|
ballots = Ballot.objects.filter(poll=poll)
|
||||||
|
|
||||||
|
for i in range(opt_count):
|
||||||
|
option = options[i]
|
||||||
|
|
||||||
|
# Collect all fragments for this opt
|
||||||
|
frags_c1 = list()
|
||||||
|
frags_c2 = list()
|
||||||
|
|
||||||
|
for ballot in ballots:
|
||||||
|
enc_vote = ballot.encrypted_vote.get()
|
||||||
|
|
||||||
|
if enc_vote is not None:
|
||||||
|
fragments = enc_vote.fragment.all()
|
||||||
|
frags_c1.append(fragments[i].cipher_text_c1)
|
||||||
|
frags_c2.append(fragments[i].cipher_text_c2)
|
||||||
|
|
||||||
|
ciphers = {
|
||||||
|
'c1s': frags_c1,
|
||||||
|
'c2s': frags_c2
|
||||||
|
}
|
||||||
|
|
||||||
|
combined_cipher = add_ciphers(ciphers)
|
||||||
|
|
||||||
|
CombinedBallot.objects.create(poll=poll,
|
||||||
|
option=option,
|
||||||
|
cipher_text_c1=combined_cipher['C1'],
|
||||||
|
cipher_text_c2=combined_cipher['C2'])
|
||||||
|
|
||||||
@task()
|
@task()
|
||||||
def create_ballots(event):
|
def create_ballots(event):
|
||||||
|
@ -53,6 +110,7 @@ def create_ballots(event):
|
||||||
for voter in voters:
|
for voter in voters:
|
||||||
ballot = poll.ballots.create(voter=voter, poll=poll)
|
ballot = poll.ballots.create(voter=voter, poll=poll)
|
||||||
|
|
||||||
|
|
||||||
@task()
|
@task()
|
||||||
def create_ballots_for_poll(poll):
|
def create_ballots_for_poll(poll):
|
||||||
for voter in poll.event.voters.all():
|
for voter in poll.event.voters.all():
|
||||||
|
@ -67,16 +125,29 @@ def email_trustees_prep(trustees, event):
|
||||||
email_subject = "Key Generation and Preparation for Event '" + event.title + "'"
|
email_subject = "Key Generation and Preparation for Event '" + event.title + "'"
|
||||||
|
|
||||||
# Plain text email - this could be replaced for a HTML-based email in the future
|
# Plain text email - this could be replaced for a HTML-based email in the future
|
||||||
email_body = "Please visit the following URL to prepare the event and generate your trustee secret key:\n\n"
|
email_body_base = str("")
|
||||||
|
email_body_base += "Dear Trustee,\n\n"
|
||||||
|
email_body_base += "You have been enrolled as a trustee onto the event: " + event.title + \
|
||||||
|
". You are required to visit the URL below to generate your secret key and associated public" \
|
||||||
|
" key that will be used to encrypt the event.\n\n You will need to ensure that you back up" \
|
||||||
|
" your secret key as this will be needed to decrypt the event - please don't lose this as it" \
|
||||||
|
" cannot be re-generated. DEMOS2 will never and cannot store your secret key.\n\n"
|
||||||
|
email_body_base += "Please visit the following URL to prepare the event and generate your trustee secret key:\n\n"
|
||||||
url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/prepare/?key="
|
url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/prepare/?key="
|
||||||
email_body = email_body + url_base
|
email_body_base += url_base
|
||||||
|
|
||||||
|
sign_off = get_email_sign_off()
|
||||||
|
|
||||||
for trustee in trustees:
|
for trustee in trustees:
|
||||||
# Generate a key and create an AccessKey object
|
# Generate a key and create an AccessKey object
|
||||||
key = gen_access_key()
|
key = gen_access_key()
|
||||||
AccessKey.objects.create(user=trustee, event=event, key=key)
|
AccessKey.objects.create(user=trustee, event=event, key=key)
|
||||||
|
|
||||||
trustee.send_email(email_subject, email_body + key)
|
email_body = str(email_body_base + key)
|
||||||
|
email_body += sign_off
|
||||||
|
|
||||||
|
trustee.send_email(email_subject, email_body)
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Emails a URL containing an access key for all of the voters for an event
|
Emails a URL containing an access key for all of the voters for an event
|
||||||
|
@ -86,13 +157,18 @@ def email_voters_vote_url(voters, event):
|
||||||
email_subject = "Voting Access for Event '" + event.title + "'"
|
email_subject = "Voting Access for Event '" + event.title + "'"
|
||||||
|
|
||||||
# Plain text email - this could be replaced for a HTML-based email in the future
|
# Plain text email - this could be replaced for a HTML-based email in the future
|
||||||
email_body_base = "Please visit the following URL in order to vote on the event '" + event.title + "':\n\n"
|
# TODO: The URL needs updating and it could be replaced with a single UUID that's unique
|
||||||
url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/poll/1/vote/?key="
|
# TODO: for the voter for an event which would shorten the URL
|
||||||
email_body_base = email_body_base + url_base
|
email_body_base = str("")
|
||||||
|
email_body_base += "Dear Voter,\n\n"
|
||||||
|
email_body_base += "You have been enrolled as a voter onto the event: " + event.title + ".\n\nYou can vote between the following dates and times:\n"
|
||||||
|
email_body_base += "Start: " + event.start_time_formatted_utc() + "\n"
|
||||||
|
email_body_base += "End: " + event.end_time_formatted_utc() + "\n\n"
|
||||||
|
email_body_base += "Please visit the following URL in order to vote on the event where further instructions can be found on the page:\n\n"
|
||||||
|
url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/poll/" + str(event.polls.all()[0].uuid) + "/vote/?key="
|
||||||
|
email_body_base += url_base
|
||||||
|
|
||||||
duration_info = "\n\nYou can vote between the following dates and times:\n"
|
sign_off = get_email_sign_off()
|
||||||
duration_info = duration_info + "Start: " + event.start_time_formatted_utc() + "\n"
|
|
||||||
duration_info = duration_info + "End: " + event.end_time_formatted_utc()
|
|
||||||
|
|
||||||
for voter in voters:
|
for voter in voters:
|
||||||
# Generate a key and create an AccessKey object
|
# Generate a key and create an AccessKey object
|
||||||
|
@ -101,10 +177,11 @@ def email_voters_vote_url(voters, event):
|
||||||
|
|
||||||
# Update the email body to incl the access key as well as the duration information
|
# Update the email body to incl the access key as well as the duration information
|
||||||
email_body = str(email_body_base + key)
|
email_body = str(email_body_base + key)
|
||||||
email_body = email_body + duration_info
|
email_body += sign_off
|
||||||
|
|
||||||
voter.send_email(email_subject, email_body)
|
voter.send_email(email_subject, email_body)
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Updates the EID of an event to contain 2 event IDs: a human readable one (hr) and a crypto one (GP from param())
|
Updates the EID of an event to contain 2 event IDs: a human readable one (hr) and a crypto one (GP from param())
|
||||||
'''
|
'''
|
||||||
|
@ -120,95 +197,16 @@ def update_EID(event):
|
||||||
event.EID = json.dumps(EID)
|
event.EID = json.dumps(EID)
|
||||||
event.save()
|
event.save()
|
||||||
|
|
||||||
|
|
||||||
@task()
|
@task()
|
||||||
def event_ended(event):
|
def event_ended(event):
|
||||||
# Email all trustees to request their secret keys
|
# Combine all the ballots for every option in every poll which will be decrypted by the trustees
|
||||||
|
polls = event.polls.all()
|
||||||
|
combine_ballots(polls)
|
||||||
|
|
||||||
|
# Email all trustees to request their partial decryptions using their secret keys
|
||||||
email_trustees_dec(event)
|
email_trustees_dec(event)
|
||||||
|
|
||||||
@task()
|
|
||||||
def gen_event_sk_and_dec(event):
|
|
||||||
trustee_sks = TrusteeSK.objects.filter(event=event)
|
|
||||||
t_sks_count = len(trustee_sks)
|
|
||||||
|
|
||||||
# Combine SKs if there's more than one
|
|
||||||
event_sk = None
|
|
||||||
if t_sks_count == 1:
|
|
||||||
event_sk = trustee_sks.get().key
|
|
||||||
else:
|
|
||||||
t_sks_str_list = list()
|
|
||||||
|
|
||||||
for t_sk in trustee_sks:
|
|
||||||
t_sks_str_list.append(t_sk.key)
|
|
||||||
|
|
||||||
event_sk = combine_sks(t_sks_str_list)
|
|
||||||
|
|
||||||
EventSK.objects.create(event=event, key=event_sk)
|
|
||||||
|
|
||||||
# With the event sk created, we can decrypt the event
|
|
||||||
decrypt_and_tally(event)
|
|
||||||
|
|
||||||
@task()
|
|
||||||
def decrypt_and_tally(event):
|
|
||||||
polls = event.polls.all()
|
|
||||||
sk = EventSK.objects.filter(event=event).get().key
|
|
||||||
|
|
||||||
for i in range(len(polls)):
|
|
||||||
poll = polls[i]
|
|
||||||
result = str("")
|
|
||||||
result += "{\"name\": \"" + poll.question_text + "\","
|
|
||||||
|
|
||||||
# get num of opts and ballots
|
|
||||||
options = poll.options.all()
|
|
||||||
opt_count = len(options)
|
|
||||||
ballots = Ballot.objects.filter(poll=poll, cast=True)
|
|
||||||
|
|
||||||
result += "\"options\": ["
|
|
||||||
for j in range(opt_count):
|
|
||||||
# Collect all fragments for this opt
|
|
||||||
frags_c1 = list()
|
|
||||||
frags_c2 = list()
|
|
||||||
|
|
||||||
for ballot in ballots:
|
|
||||||
enc_vote = ballot.encrypted_vote.get()
|
|
||||||
|
|
||||||
if enc_vote is not None:
|
|
||||||
fragments = enc_vote.fragment.all()
|
|
||||||
frags_c1.append(fragments[j].cipher_text_c1)
|
|
||||||
frags_c2.append(fragments[j].cipher_text_c2)
|
|
||||||
|
|
||||||
ciphers = {
|
|
||||||
'c1s': frags_c1,
|
|
||||||
'c2s': frags_c2
|
|
||||||
}
|
|
||||||
|
|
||||||
count = len(frags_c1)
|
|
||||||
votes = get_tally(count, ciphers, sk, event.EID)
|
|
||||||
|
|
||||||
result += "{\"option\": \"" + str(options[j].choice_text) + "\", \"votes\": " + str(votes) + "}"
|
|
||||||
|
|
||||||
if j != (opt_count-1):
|
|
||||||
result += ","
|
|
||||||
|
|
||||||
result += "]}"
|
|
||||||
|
|
||||||
if i != (len(polls) - 1):
|
|
||||||
result += ","
|
|
||||||
|
|
||||||
poll.enc = result
|
|
||||||
poll.save()
|
|
||||||
|
|
||||||
@task()
|
|
||||||
def tally_results(event):
|
|
||||||
for poll in event.polls.all():
|
|
||||||
decs = list()
|
|
||||||
for dec in poll.decryptions.all():
|
|
||||||
decs.append(dec.text)
|
|
||||||
amount = len(decs)
|
|
||||||
result = tally(amount, event.EID, decs, poll.enc)
|
|
||||||
|
|
||||||
# TODO: Email organisers using email_user method?
|
|
||||||
|
|
||||||
print(poll.question_text + ": " + result)
|
|
||||||
|
|
||||||
@task()
|
@task()
|
||||||
def generate_combpk(event):
|
def generate_combpk(event):
|
||||||
|
@ -222,25 +220,54 @@ def generate_combpk(event):
|
||||||
event.prepared = True
|
event.prepared = True
|
||||||
event.save()
|
event.save()
|
||||||
|
|
||||||
@task
|
|
||||||
def generate_enc(poll):
|
|
||||||
# c1 and c2 components of ciphertexts
|
|
||||||
c1s = list()
|
|
||||||
c2s = list()
|
|
||||||
|
|
||||||
for ballot in poll.ballots.all():
|
@task()
|
||||||
if ballot.cast:
|
def combine_decryptions_and_tally(event):
|
||||||
c1s.append(str(ballot.cipher_text_c1))
|
polls = event.polls.all()
|
||||||
c2s.append(str(ballot.cipher_text_c2))
|
polls_count = len(polls)
|
||||||
|
|
||||||
ciphers = {
|
for i in range(polls_count):
|
||||||
'c1s': c1s,
|
poll = polls[i]
|
||||||
'c2s': c2s
|
result = str("")
|
||||||
}
|
result += "{\"name\": \"" + poll.question_text + "\","
|
||||||
|
|
||||||
count = len(c1s)
|
options = poll.options.all()
|
||||||
|
opt_count = len(options)
|
||||||
|
result += "\"options\": ["
|
||||||
|
for j in range(opt_count):
|
||||||
|
option = options[j]
|
||||||
|
|
||||||
poll.enc = addec(count, ciphers)
|
# Find the combined ballot for the current option of the current poll
|
||||||
|
# and then extract the C1 and C2 components of the cipher that contains the tally
|
||||||
|
combined_ballot = CombinedBallot.objects.filter(poll=poll,
|
||||||
|
option=option)[0]
|
||||||
|
|
||||||
|
ballot_cipher = {}
|
||||||
|
ballot_cipher['C1'] = combined_ballot.cipher_text_c1
|
||||||
|
ballot_cipher['C2'] = combined_ballot.cipher_text_c2
|
||||||
|
|
||||||
|
# Collect all the partial decryptions for the ballot cipher which will decrypt the result
|
||||||
|
part_decs = PartialBallotDecryption.objects.filter(event=event,
|
||||||
|
poll=poll,
|
||||||
|
option=option)
|
||||||
|
|
||||||
|
part_decs_text = list()
|
||||||
|
for part_dec in part_decs:
|
||||||
|
part_decs_text.append(part_dec.text)
|
||||||
|
|
||||||
|
# Get the vote tally for this option and add it to the results
|
||||||
|
voters_count = event.voters.all().count()
|
||||||
|
votes = get_tally(ballot_cipher, part_decs_text, event.EID, voters_count)
|
||||||
|
result += "{\"option\": \"" + str(option.choice_text) + "\", \"votes\": \"" + str(votes) + "\"}"
|
||||||
|
|
||||||
|
if j != (opt_count-1):
|
||||||
|
result += ","
|
||||||
|
|
||||||
|
result += "]}"
|
||||||
|
|
||||||
|
if i != (polls_count - 1):
|
||||||
|
result += ","
|
||||||
|
|
||||||
|
poll.result_json = result
|
||||||
poll.save()
|
poll.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,17 +8,17 @@ app_name = 'polls'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', login_required(views.EventListView.as_view()), name='index'),
|
url(r'^$', login_required(views.EventListView.as_view()), name='index'),
|
||||||
url(r'^create/$', login_required(views.create_event), name='create-event'),
|
url(r'^create/$', login_required(views.create_event), name='create-event'),
|
||||||
url(r'^(?P<pk>[0-9]+)/$', login_required(views.EventDetailView.as_view()), name='view-event'),
|
url(r'^(?P<pk>[0-9a-f-]+)/$', login_required(views.EventDetailView.as_view()), name='view-event'),
|
||||||
url(r'^(?P<pk>[0-9]+)/polls/$', login_required(views.EventDetailPollsView.as_view()), name='event-polls'),
|
url(r'^(?P<pk>[0-9a-f-]+)/polls/$', login_required(views.EventDetailPollsView.as_view()), name='event-polls'),
|
||||||
url(r'^(?P<pk>[0-9]+)/entities/$', login_required(views.EventDetailEntitiesView.as_view()), name='event-entities'),
|
url(r'^(?P<pk>[0-9a-f-]+)/entities/$', login_required(views.EventDetailEntitiesView.as_view()), name='event-entities'),
|
||||||
url(r'^(?P<pk>[0-9]+)/advanced/$', login_required(views.EventDetailAdvancedView.as_view()), name='event-advanced'),
|
url(r'^(?P<pk>[0-9a-f-]+)/advanced/$', login_required(views.EventDetailAdvancedView.as_view()), name='event-advanced'),
|
||||||
url(r'^(?P<event_id>[0-9]+)/end/$', login_required(views.event_end), name='end-event'),
|
url(r'^(?P<event_id>[0-9a-f-]+)/end/$', login_required(views.event_end), name='end-event'),
|
||||||
url(r'^(?P<event_id>[0-9]+)/results/$', login_required(views.results), name='event-results'),
|
url(r'^(?P<event_id>[0-9a-f-]+)/results/$', login_required(views.results), name='event-results'),
|
||||||
url(r'^(?P<event_id>[0-9]+)/edit/$', login_required(views.edit_event), name='edit-event'),
|
url(r'^(?P<event_id>[0-9a-f-]+)/edit/$', login_required(views.edit_event), name='edit-event'),
|
||||||
url(r'^(?P<event_id>[0-9]+)/delete/$', login_required(views.del_event), name='del-event'),
|
url(r'^(?P<event_id>[0-9a-f-]+)/delete/$', login_required(views.del_event), name='del-event'),
|
||||||
url(r'^(?P<event_id>[0-9]+)/decrypt/$', views.event_trustee_decrypt, name='decrypt-event'),
|
url(r'^(?P<event_id>[0-9a-f-]+)/decrypt/$', views.event_trustee_decrypt, name='decrypt-event'),
|
||||||
url(r'^(?P<event_id>[0-9]+)/prepare/$', views.event_trustee_setup, name='prepare-event'),
|
url(r'^(?P<event_id>[0-9a-f-]+)/prepare/$', views.event_trustee_setup, name='prepare-event'),
|
||||||
url(r'^(?P<event_id>[0-9]+)/poll/(?P<poll_num>[0-9]+)/vote/$', views.event_vote, name='event-vote'),
|
url(r'^(?P<event_id>[0-9a-f-]+)/poll/(?P<poll_id>[0-9a-f-]+)/vote/$', views.event_vote, name='event-vote'),
|
||||||
url(r'^(?P<event_id>[0-9]+)/create/poll/$', login_required(views.manage_questions), name='create-poll'),
|
url(r'^(?P<event_id>[0-9a-f-]+)/create/poll/$', login_required(views.manage_questions), name='create-poll'),
|
||||||
url(r'^(?P<event_id>[0-9]+)/poll/(?P<poll_num>[0-9]+)/edit$', login_required(views.edit_poll), name='edit-poll')
|
url(r'^(?P<event_id>[0-9a-f-]+)/poll/(?P<poll_id>[0-9a-f-]+)/edit$', login_required(views.edit_poll), name='edit-poll')
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,10 +11,11 @@ from django.views import generic
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from .forms import PollForm, OptionFormset, VoteForm, EventSetupForm, EventEditForm
|
from .forms import PollForm, OptionFormset, VoteForm, EventSetupForm, EventEditForm
|
||||||
from .models import Event, Poll, Ballot, EncryptedVote, TrusteeKey, TrusteeSK
|
from .models import Event, Poll, Ballot, EncryptedVote, TrusteeKey, PartialBallotDecryption, CombinedBallot
|
||||||
from allauthdemo.auth.models import DemoUser
|
from allauthdemo.auth.models import DemoUser
|
||||||
|
|
||||||
from .tasks import email_trustees_prep, update_EID, generate_combpk, event_ended, create_ballots, create_ballots_for_poll, email_voters_vote_url, gen_event_sk_and_dec
|
from .tasks import email_trustees_prep, update_EID, generate_combpk, event_ended, create_ballots
|
||||||
|
from .tasks import create_ballots_for_poll, email_voters_vote_url, combine_decryptions_and_tally
|
||||||
|
|
||||||
from .utils.EventModelAdaptor import EventModelAdaptor
|
from .utils.EventModelAdaptor import EventModelAdaptor
|
||||||
|
|
||||||
|
@ -25,7 +26,6 @@ class EventListView(generic.ListView):
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(EventListView, self).get_context_data(**kwargs)
|
context = super(EventListView, self).get_context_data(**kwargs)
|
||||||
#context['now'] = timezone.now()
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ class EventDetailView(generic.DetailView):
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(EventDetailView, self).get_context_data(**kwargs)
|
context = super(EventDetailView, self).get_context_data(**kwargs)
|
||||||
context['is_organiser'] = ((not self.request.user.is_anonymous()) and (self.object.users_organisers.filter(email=self.request.user.email).exists()))
|
context['is_organiser'] = (not self.request.user.is_anonymous()) and (self.object.users_organisers.filter(email=self.request.user.email).exists())
|
||||||
context['decrypted'] = self.object.status() == "Decrypted"
|
context['decrypted'] = self.object.status() == "Decrypted"
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
@ -63,20 +63,13 @@ class PollDetailView(generic.View):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
def util_get_poll_by_event_index(event, poll_num):
|
def util_get_poll_by_event_index(event, poll_id):
|
||||||
try:
|
return event.polls.get(uuid=poll_id)
|
||||||
poll_num = int(poll_num)
|
|
||||||
if ((poll_num < 1) or (poll_num > event.polls.all().count())):
|
|
||||||
return None
|
|
||||||
poll = event.polls.filter().order_by('id')[poll_num-1] # index field eventually
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
return poll
|
|
||||||
|
|
||||||
|
|
||||||
def edit_poll(request, event_id, poll_num):
|
def edit_poll(request, event_id, poll_id):
|
||||||
event = get_object_or_404(Event, pk=event_id)
|
event = get_object_or_404(Event, pk=event_id)
|
||||||
poll = util_get_poll_by_event_index(event, poll_num)
|
poll = util_get_poll_by_event_index(event, poll_id)
|
||||||
|
|
||||||
if (poll == None):
|
if (poll == None):
|
||||||
raise Http404("Poll does not exist")
|
raise Http404("Poll does not exist")
|
||||||
|
@ -98,33 +91,50 @@ def edit_poll(request, event_id, poll_num):
|
||||||
return HttpResponseRedirect(reverse('polls:event-polls', args=[poll.event_id]))
|
return HttpResponseRedirect(reverse('polls:event-polls', args=[poll.event_id]))
|
||||||
|
|
||||||
|
|
||||||
def event_vote(request, event_id, poll_num):
|
def event_vote(request, event_id, poll_id):
|
||||||
event = get_object_or_404(Event, pk=event_id)
|
event = get_object_or_404(Event, pk=event_id)
|
||||||
|
|
||||||
if not event.prepared:
|
if not event.prepared:
|
||||||
messages.add_message(request, messages.WARNING, "This Event isn\'t ready for voting yet.")
|
messages.add_message(request, messages.WARNING, "This Event isn\'t ready for voting yet.")
|
||||||
return HttpResponseRedirect(reverse("user_home"))
|
return HttpResponseRedirect(reverse("user_home"))
|
||||||
|
|
||||||
event_poll_count = event.polls.all().count()
|
# Lookup the specified poll
|
||||||
prev_poll_index, next_poll_index = False, False
|
poll = event.polls.get(uuid=poll_id)
|
||||||
can_vote, has_voted, voter_email = False, False, ""
|
|
||||||
poll = util_get_poll_by_event_index(event, poll_num)
|
|
||||||
|
|
||||||
if poll is None:
|
if poll is None:
|
||||||
messages.add_message(request, messages.ERROR, "There was an error loading the voting page.")
|
messages.add_message(request, messages.ERROR, "There was an error loading the voting page.")
|
||||||
return HttpResponseRedirect(reverse("user_home"))
|
return HttpResponseRedirect(reverse("user_home"))
|
||||||
|
|
||||||
poll_num = int(poll_num) # now known to be safe as it succeeded in the util function
|
polls = event.polls.all()
|
||||||
|
event_poll_count = len(polls)
|
||||||
|
prev_poll_uuid, next_poll_uuid, poll_num = False, False, 0
|
||||||
|
can_vote, cant_vote_reason, has_voted, voter_email = False, "", False, ""
|
||||||
|
|
||||||
if poll_num > 1:
|
for i in range(event_poll_count):
|
||||||
prev_poll_index = (poll_num - 1)
|
poll = polls[i]
|
||||||
if poll_num < event_poll_count:
|
poll_uuid = str(poll.uuid)
|
||||||
next_poll_index = (poll_num + 1)
|
req_poll_uuid = str(poll_id)
|
||||||
|
|
||||||
|
if poll_uuid == req_poll_uuid:
|
||||||
|
poll_num = str(i+1)
|
||||||
|
|
||||||
|
# If current voting request isn't for the last poll, then make sure we link to the next
|
||||||
|
if i != event_poll_count - 1:
|
||||||
|
# Only set the previous poll's uuid if we're not looking at the first poll
|
||||||
|
if i != 0:
|
||||||
|
prev_poll_uuid = str(polls[i - 1].uuid)
|
||||||
|
|
||||||
|
next_poll_uuid = str(polls[i + 1].uuid)
|
||||||
|
else:
|
||||||
|
if i != 0:
|
||||||
|
prev_poll_uuid = str(polls[i - 1].uuid)
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
access_key = request.GET.get('key', None)
|
access_key = request.GET.get('key', None)
|
||||||
email_key = event.keys.filter(key=access_key)
|
email_key = event.keys.filter(key=access_key)
|
||||||
|
email_key_str = email_key[0].key
|
||||||
|
|
||||||
ballot = None
|
|
||||||
if email_key.exists() and event.voters.filter(email=email_key[0].user.email).exists():
|
if email_key.exists() and event.voters.filter(email=email_key[0].user.email).exists():
|
||||||
# Passing this test means the user can vote
|
# Passing this test means the user can vote
|
||||||
voter_email = email_key[0].user.email
|
voter_email = email_key[0].user.email
|
||||||
|
@ -135,15 +145,18 @@ def event_vote(request, event_id, poll_num):
|
||||||
if ballot.exists() and ballot[0].cast:
|
if ballot.exists() and ballot[0].cast:
|
||||||
has_voted = True
|
has_voted = True
|
||||||
else:
|
else:
|
||||||
messages.add_message(request, messages.ERROR, "You don\'t have permission to vote in this event.")
|
can_vote = False
|
||||||
return HttpResponseRedirect(reverse("user_home"))
|
cant_vote_reason = "You don't have permission to access this page."
|
||||||
|
|
||||||
|
if event.status() != "Active":
|
||||||
|
can_vote = False
|
||||||
|
cant_vote_reason = "The event either isn't ready for voting or it has expired and therefore you cannot vote."
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if ballot is None:
|
ballot = Ballot.objects.get_or_create(voter=email_key[0].user, poll=poll)[0]
|
||||||
ballot = Ballot.objects.get_or_create(voter=email_key[0].user, poll=poll)
|
|
||||||
|
|
||||||
# Will store the fragments of the encoding scheme that define the vote
|
# Will store the fragments of the encoding scheme that define the vote
|
||||||
encrypted_vote = EncryptedVote.objects.get_or_create(ballot=ballot[0])[0]
|
encrypted_vote = EncryptedVote.objects.get_or_create(ballot=ballot)[0]
|
||||||
|
|
||||||
# Clear any existing fragments - a voter changing their vote
|
# Clear any existing fragments - a voter changing their vote
|
||||||
encrypted_vote.fragment.all().delete()
|
encrypted_vote.fragment.all().delete()
|
||||||
|
@ -160,11 +173,13 @@ def event_vote(request, event_id, poll_num):
|
||||||
cipher_text_c1=cipher_c1,
|
cipher_text_c1=cipher_c1,
|
||||||
cipher_text_c2=cipher_c2)
|
cipher_text_c2=cipher_c2)
|
||||||
|
|
||||||
ballot[0].cast = True
|
ballot.cast = True
|
||||||
ballot[0].save()
|
ballot.save()
|
||||||
|
|
||||||
if next_poll_index:
|
if next_poll_uuid:
|
||||||
return HttpResponseRedirect(reverse('polls:event-vote', kwargs={'event_id': event.id, 'poll_num': next_poll_index }) + "?key=" + email_key[0].key)
|
return HttpResponseRedirect(reverse('polls:event-vote', kwargs={'event_id': event.uuid,
|
||||||
|
'poll_id': next_poll_uuid})
|
||||||
|
+ "?key=" + email_key_str)
|
||||||
else:
|
else:
|
||||||
# The user has finished voting in the event
|
# The user has finished voting in the event
|
||||||
success_msg = 'You have successfully cast your vote(s)!'
|
success_msg = 'You have successfully cast your vote(s)!'
|
||||||
|
@ -175,9 +190,9 @@ def event_vote(request, event_id, poll_num):
|
||||||
return render(request, "polls/event_vote.html",
|
return render(request, "polls/event_vote.html",
|
||||||
{
|
{
|
||||||
"object": poll, "poll_num": poll_num, "event": event, "poll_count": event.polls.all().count(),
|
"object": poll, "poll_num": poll_num, "event": event, "poll_count": event.polls.all().count(),
|
||||||
"prev_index": prev_poll_index, "next_index": next_poll_index, "min_selection": poll.min_num_selections,
|
"prev_uuid": prev_poll_uuid, "next_uuid": next_poll_uuid, "min_selection": poll.min_num_selections,
|
||||||
"max_selection": poll.max_num_selections, "can_vote": can_vote, "voter_email": voter_email,
|
"max_selection": poll.max_num_selections, "can_vote": can_vote, "cant_vote_reason": cant_vote_reason,
|
||||||
"has_voted": has_voted
|
"voter_email": voter_email, "has_voted": has_voted, "a_key": email_key_str
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -245,7 +260,7 @@ def results(request, event_id):
|
||||||
results = ""
|
results = ""
|
||||||
results += "{\"polls\":["
|
results += "{\"polls\":["
|
||||||
for poll in polls:
|
for poll in polls:
|
||||||
results += poll.enc
|
results += poll.result_json
|
||||||
|
|
||||||
results += "]}"
|
results += "]}"
|
||||||
|
|
||||||
|
@ -260,21 +275,63 @@ def event_trustee_decrypt(request, event_id):
|
||||||
email_key = event.keys.filter(key=access_key)
|
email_key = event.keys.filter(key=access_key)
|
||||||
|
|
||||||
if email_key.exists() and event.users_trustees.filter(email=email_key[0].user.email).exists():
|
if email_key.exists() and event.users_trustees.filter(email=email_key[0].user.email).exists():
|
||||||
if TrusteeSK.objects.filter(event=event, trustee=email_key[0].user).exists():
|
|
||||||
messages.add_message(request, messages.WARNING, 'You have already provided your decryption key for this event')
|
if PartialBallotDecryption.objects.filter(event=event, user=email_key[0].user).count() == event.total_num_opts():
|
||||||
|
|
||||||
|
warning_msg = 'You have already provided your decryption key for this event - Thank You'
|
||||||
|
messages.add_message(request, messages.WARNING, warning_msg)
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse("user_home"))
|
return HttpResponseRedirect(reverse("user_home"))
|
||||||
elif request.method == "GET":
|
elif request.method == "GET":
|
||||||
return render(request, "polls/event_decrypt.html", {"event": event, "user_email": email_key[0].user.email})
|
# Gen a list of ciphers from the combined ballots for every opt of every poll
|
||||||
|
polls = event.polls.all()
|
||||||
|
poll_ciphers = []
|
||||||
|
|
||||||
|
for poll in polls:
|
||||||
|
options = poll.options.all()
|
||||||
|
|
||||||
|
options_ciphers = []
|
||||||
|
for option in options:
|
||||||
|
combined_ballot = CombinedBallot.objects.filter(poll=poll, option=option).get()
|
||||||
|
|
||||||
|
cipher = {}
|
||||||
|
cipher['C1'] = combined_ballot.cipher_text_c1
|
||||||
|
cipher['C2'] = combined_ballot.cipher_text_c2
|
||||||
|
options_ciphers.append(cipher)
|
||||||
|
|
||||||
|
poll_ciphers.append(options_ciphers)
|
||||||
|
|
||||||
|
return render(request,
|
||||||
|
"polls/event_decrypt.html",
|
||||||
|
{
|
||||||
|
"event": event,
|
||||||
|
"user_email": email_key[0].user.email,
|
||||||
|
"poll_ciphers": poll_ciphers
|
||||||
|
})
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
sk = request.POST['secret-key']
|
polls = event.polls.all()
|
||||||
|
polls_count = len(polls)
|
||||||
|
|
||||||
TrusteeSK.objects.create(event=event,
|
for i in range(polls_count):
|
||||||
trustee=email_key[0].user,
|
options = polls[i].options.all()
|
||||||
key=sk)
|
options_count = len(options)
|
||||||
|
|
||||||
if event.trustee_sk.count() == event.users_trustees.count():
|
for j in range(options_count):
|
||||||
# Generate the event SK and decrypt the event to tally the results
|
input_name = ""
|
||||||
gen_event_sk_and_dec.delay(event)
|
input_name = "poll-" + str(i) + "-cipher-" + str(j)
|
||||||
|
|
||||||
|
part_dec = request.POST[input_name]
|
||||||
|
|
||||||
|
PartialBallotDecryption.objects.create(event=event,
|
||||||
|
poll=polls[i],
|
||||||
|
option=options[j],
|
||||||
|
user=email_key[0].user,
|
||||||
|
text=part_dec)
|
||||||
|
|
||||||
|
if event.all_part_decs_received():
|
||||||
|
# TODO: Combine partial decryptions and gen results
|
||||||
|
combine_decryptions_and_tally.delay(event)
|
||||||
|
|
||||||
messages.add_message(request, messages.SUCCESS, 'Your secret key has been successfully submitted')
|
messages.add_message(request, messages.SUCCESS, 'Your secret key has been successfully submitted')
|
||||||
return HttpResponseRedirect(reverse("user_home"))
|
return HttpResponseRedirect(reverse("user_home"))
|
||||||
|
@ -434,7 +491,7 @@ def edit_event(request, event_id):
|
||||||
def del_event(request, event_id):
|
def del_event(request, event_id):
|
||||||
event = get_object_or_404(Event, pk=event_id)
|
event = get_object_or_404(Event, pk=event_id)
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return render(request, "polls/del_event.html", {"event_title": event.title, "event_id": event.id})
|
return render(request, "polls/del_event.html", {"event_title": event.title, "event_id": event.uuid})
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
event.delete()
|
event.delete()
|
||||||
return HttpResponseRedirect(reverse('polls:index'))
|
return HttpResponseRedirect(reverse('polls:index'))
|
|
@ -56,7 +56,6 @@
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
|
||||||
{% block app_js_vars %}
|
{% block app_js_vars %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -73,8 +72,33 @@
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
dropDownFragsNotZero = function(frags) {
|
||||||
|
var valid = false;
|
||||||
|
|
||||||
|
for(var i = 0; i < frags.length; i++) {
|
||||||
|
var frag = frags[i];
|
||||||
|
|
||||||
|
if(frag !== "0") {
|
||||||
|
valid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
};
|
||||||
|
|
||||||
//new function
|
//new function
|
||||||
demosEncrypt.encryptAndSubmit = function() {
|
demosEncrypt.encryptAndSubmit = function() {
|
||||||
|
// Drop down option selection validation
|
||||||
|
if(min_selections === 1 && max_selections === 1) {
|
||||||
|
var fragments = $('#poll-options').val().split(",");
|
||||||
|
|
||||||
|
if(!dropDownFragsNotZero(fragments)) {
|
||||||
|
alert("You have to select an option in order to vote.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} // TODO: Checkbox validation goes here
|
||||||
|
|
||||||
// Disable the enc and submit button to prevent fn from being called twice
|
// Disable the enc and submit button to prevent fn from being called twice
|
||||||
$('#keygen-btn').prop("disabled", true);
|
$('#keygen-btn').prop("disabled", true);
|
||||||
|
|
||||||
|
@ -147,9 +171,16 @@
|
||||||
$('#cipher-form').submit();
|
$('#cipher-form').submit();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getBytes(arr) {
|
||||||
|
for(var i = 0; i < arr.length; i++) {
|
||||||
|
arr[i] = parseInt(arr[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint8Array(arr);
|
||||||
|
}
|
||||||
|
|
||||||
//new function
|
//new function
|
||||||
demosEncrypt.decryptCipher = function() {
|
demosEncrypt.decryptSubmitCiphers = function() {
|
||||||
var skString = $('#secret-key').val();
|
var skString = $('#secret-key').val();
|
||||||
if (!skString) {
|
if (!skString) {
|
||||||
alert("Please enter your secret key");
|
alert("Please enter your secret key");
|
||||||
|
@ -161,25 +192,29 @@
|
||||||
var sk = new ctx.BIG.fromBytes(skBytes);
|
var sk = new ctx.BIG.fromBytes(skBytes);
|
||||||
|
|
||||||
var inputs = $("form input[type=text]");
|
var inputs = $("form input[type=text]");
|
||||||
|
|
||||||
inputs.each(function() { //for each ciphertext to decrypt
|
inputs.each(function() { //for each ciphertext to decrypt
|
||||||
var ciphertext = {
|
var ciphertext = {
|
||||||
C1: new ctx.ECP(),
|
C1: null,
|
||||||
C2: new ctx.ECP()
|
C2: null
|
||||||
}
|
};
|
||||||
var temp = JSON.parse($(this).val());
|
|
||||||
ciphertext.C1.copy(temp.C1);
|
|
||||||
ciphertext.C2.copy(temp.C2);
|
|
||||||
|
|
||||||
var partial = partDec(sk, ciphertext);//returns an object containing an ECP()
|
var temp = JSON.parse($(this).val());
|
||||||
|
var c1Bytes = getBytes(temp.C1.split(','));
|
||||||
|
ciphertext.C1 = new ctx.ECP.fromBytes(c1Bytes);
|
||||||
|
|
||||||
|
var c2Bytes = getBytes(temp.C2.split(','));
|
||||||
|
ciphertext.C2 = new ctx.ECP.fromBytes(c2Bytes);
|
||||||
|
|
||||||
|
// Perform partial decryption where the method returns an object containing an ECP()
|
||||||
|
var partial = partDec(sk, ciphertext);
|
||||||
|
|
||||||
var bytes = [];
|
var bytes = [];
|
||||||
partial.D.toBytes(bytes);
|
partial.D.toBytes(bytes);
|
||||||
$(this).val(bytes.toString());//submit in byte array form
|
$(this).val(bytes.toString());//submit in byte array form
|
||||||
})
|
});
|
||||||
|
|
||||||
$('input[type=submit]').prop("disabled", false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
//new function
|
//new function
|
||||||
demosEncrypt.generateKeys = function() {
|
demosEncrypt.generateKeys = function() {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{% extends "bases/bootstrap-with-nav.html" %}
|
{% extends "bases/bootstrap-with-nav.html" %}
|
||||||
{% load staticfiles %}
|
{% load staticfiles %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% comment %} is it safe really? {% endcomment %}
|
|
||||||
{% block sk-file-name %}{{ event.title|safe }}{% endblock %}
|
{% block sk-file-name %}{{ event.title|safe }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -12,18 +11,39 @@
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Submit your Secret Key as '{{ user_email }}'</strong></div>
|
<div class="panel-heading"><strong>Submit your Secret Key as '{{ user_email }}'</strong></div>
|
||||||
<div class="panel panel-body">
|
<div class="panel panel-body">
|
||||||
<form id="sk-form" method="POST">
|
<input id="secret-key" name="secret-key" class="textinput textInput form-control" type="text" disabled/>
|
||||||
{% csrf_token %}
|
|
||||||
<input id="secret-key" name="secret-key" class="textinput textInput form-control" type="text"/>
|
|
||||||
<div class="alert alert-info" role="alert" style="margin-top: 0.75em;">
|
<div class="alert alert-info" role="alert" style="margin-top: 0.75em;">
|
||||||
Your secret key will be used to decrypt the event and get a vote tally for every poll.
|
Your secret key will be used to decrypt the event and get a vote tally for every poll.
|
||||||
|
It won't be sent to the server.
|
||||||
</div>
|
</div>
|
||||||
<label for="files_sk_upload" class="btn btn-primary">
|
<label for="files_sk_upload" class="btn btn-primary">
|
||||||
<span class="glyphicon glyphicon-cloud-upload"></span>
|
<span class="glyphicon glyphicon-cloud-upload"></span>
|
||||||
Upload Key
|
Upload Key
|
||||||
</label>
|
</label>
|
||||||
<input type="file" id="files_sk_upload" name="file" class="btn-info">
|
<input type="file" id="files_sk_upload" name="file" class="btn-info">
|
||||||
<input type="submit" value="Submit" class="btn btn-success"/>
|
</div>
|
||||||
|
<br/>
|
||||||
|
<div class="panel-heading"><strong>Ciphers</strong></div>
|
||||||
|
<div class="panel panel-body">
|
||||||
|
<form id="cipher-form" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for opts_ciphers in poll_ciphers %}
|
||||||
|
{% for cipher in opts_ciphers %}
|
||||||
|
<input id="cipher"
|
||||||
|
name="poll-{{ forloop.parentloop.counter0 }}-cipher-{{ forloop.counter0 }}"
|
||||||
|
class="textinput textInput form-control"
|
||||||
|
type="text"
|
||||||
|
value="{ "C1": "{{ cipher.C1 }}", "C2": "{{ cipher.C2 }}" }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
{% endfor %}
|
||||||
|
<br/>
|
||||||
|
{% endfor %}
|
||||||
|
<button id="decrypt-btn"
|
||||||
|
onclick="demosEncrypt.decryptSubmitCiphers()"
|
||||||
|
class="btn btn-success">
|
||||||
|
Decrypt & Submit</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,16 +15,16 @@
|
||||||
<!-- Edit Button -->
|
<!-- Edit Button -->
|
||||||
<div class="col-xs-5 col-sm-3 col-md-3 marginTopEditButton">
|
<div class="col-xs-5 col-sm-3 col-md-3 marginTopEditButton">
|
||||||
{% if object.has_received_votes and object.ended == False %}
|
{% if object.has_received_votes and object.ended == False %}
|
||||||
<a href="{% url 'polls:end-event' event.id %}" class="btn btn-danger" style="float: right;">
|
<a href="{% url 'polls:end-event' event.uuid %}" class="btn btn-danger" style="float: right;">
|
||||||
<span class="glyphicon glyphicon-stop"></span> End
|
<span class="glyphicon glyphicon-stop"></span> End
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if decrypted == True and object.ended == True %}
|
{% if decrypted == True and object.ended == True %}
|
||||||
<a href="{% url 'polls:event-results' event.id %}" class="btn btn-success" style="float: right;">
|
<a href="{% url 'polls:event-results' event.uuid %}" class="btn btn-success" style="float: right;">
|
||||||
<span class="glyphicon glyphicon-stats"></span> Results
|
<span class="glyphicon glyphicon-stats"></span> Results
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'polls:edit-event' event.id %}" class="btn btn-primary" style="float: right; margin-right: 0.4em;">
|
<a href="{% url 'polls:edit-event' event.uuid %}" class="btn btn-primary" style="float: right; margin-right: 0.4em;">
|
||||||
<span class="fa fa-pencil"></span> Edit
|
<span class="fa fa-pencil"></span> Edit
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,17 +44,17 @@
|
||||||
<br/>
|
<br/>
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li class="{% block event_nav_details %}{% endblock %}">
|
<li class="{% block event_nav_details %}{% endblock %}">
|
||||||
<a href="{% url 'polls:view-event' event.id %}"><strong>Summary</strong></a>
|
<a href="{% url 'polls:view-event' event.uuid %}"><strong>Summary</strong></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="{% block event_nav_polls %}{% endblock %}">
|
<li class="{% block event_nav_polls %}{% endblock %}">
|
||||||
<a href="{% url 'polls:event-polls' event.id %}"><strong>Polls ({{ object.polls.count }})</strong></a>
|
<a href="{% url 'polls:event-polls' event.uuid %}"><strong>Polls ({{ object.polls.count }})</strong></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="{% block event_nav_organisers %}{% endblock %}">
|
<li class="{% block event_nav_organisers %}{% endblock %}">
|
||||||
<a href="{% url 'polls:event-entities' event.id %}"><strong>Entities</strong></a>
|
<a href="{% url 'polls:event-entities' event.uuid %}"><strong>Entities</strong></a>
|
||||||
</li>
|
</li>
|
||||||
{% if is_organiser %}
|
{% if is_organiser %}
|
||||||
<li class="{% block event_nav_launch %}{% endblock %}">
|
<li class="{% block event_nav_launch %}{% endblock %}">
|
||||||
<a href="{% url 'polls:event-advanced' event.id %}"><strong>Advanced</strong></a>
|
<a href="{% url 'polls:event-advanced' event.uuid %}"><strong>Advanced</strong></a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
{% block event_content %}
|
{% block event_content %}
|
||||||
{% if object.polls.all %}
|
{% if object.polls.all %}
|
||||||
{% for poll in object.polls.all %}
|
{% for poll in object.polls.all %}
|
||||||
<h3>Poll: {{ poll.question_text }} (<a href="{% url 'polls:edit-poll' event_id=event.id poll_num=forloop.counter %}">Edit</a>)</h3>
|
<h3>Poll: {{ poll.question_text }} (<a href="{% url 'polls:edit-poll' event_id=event.uuid poll_id=poll.uuid %}">Edit</a>)</h3>
|
||||||
<br/>
|
<br/>
|
||||||
<h4>Poll Options:</h4>
|
<h4>Poll Options:</h4>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
|
@ -21,6 +21,6 @@
|
||||||
<p>No polls are available for this Event.</p>
|
<p>No polls are available for this Event.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_organiser %}
|
{% if is_organiser %}
|
||||||
<a href="{% url 'polls:create-poll' event.id %}" class="btn btn-default" role="button">Add Poll</a>
|
<a href="{% url 'polls:create-poll' event.uuid %}" class="btn btn-default" role="button">Add Poll</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% if object_list %}
|
{% if object_list %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<table id="trustees-input-table" class="table table-hover marginTopEventList">
|
<table id="event-list-table" class="table table-hover marginTopEventList">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-center">Event</th>
|
<th class="text-center">Event</th>
|
||||||
|
@ -33,14 +33,14 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for event in object_list %}
|
{% for event in object_list %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><a href="{% url 'polls:view-event' event.id %}">{{ event.title }}</a></td>
|
<td class="text-center"><a href="{% url 'polls:view-event' event.uuid %}">{{ event.title }}</a></td>
|
||||||
<td class="text-center">{{ event.duration }}</td>
|
<td class="text-center">{{ event.duration }}</td>
|
||||||
<td class="text-center">{{ event.polls.count }}</td>
|
<td class="text-center">{{ event.polls.count }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="{% url 'polls:edit-event' event.id %}">
|
<a href="{% url 'polls:edit-event' event.uuid %}">
|
||||||
<span class="btn btn-default glyphicon glyphicon-pencil"></span>
|
<span class="btn btn-default glyphicon glyphicon-pencil"></span>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'polls:del-event' event.id %}">
|
<a href="{% url 'polls:del-event' event.uuid %}">
|
||||||
<span class="btn btn-default glyphicon glyphicon-trash"></span>
|
<span class="btn btn-default glyphicon glyphicon-trash"></span>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -4,16 +4,21 @@
|
||||||
|
|
||||||
{% block app_js_vars %}
|
{% block app_js_vars %}
|
||||||
var option_count = {{ object.options.count }};
|
var option_count = {{ object.options.count }};
|
||||||
|
var min_selections = {{ min_selection }};
|
||||||
|
var max_selections = {{ max_selection }};
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
{% if can_vote %}
|
||||||
|
<!-- Hidden fields -->
|
||||||
<input id="event-param" type="text" value="{{event.EID}}" hidden/>
|
<input id="event-param" type="text" value="{{event.EID}}" hidden/>
|
||||||
<input id="comb_pk" type="text" value="{{event.public_key}}" hidden/>
|
<input id="comb_pk" type="text" value="{{event.public_key}}" hidden/>
|
||||||
|
|
||||||
<!-- TODO: Add warning not to share the URL-->
|
<!-- Event info and instructions -->
|
||||||
<h2>Event Voting Page for the Event '{{ object.event.title }}'</h2>
|
<h2>Event Voting Page for the Event '{{ object.event.title }}'</h2>
|
||||||
|
<hr/>
|
||||||
<div class="alert alert-warning" role="alert" style="margin-top: 1em;">
|
<div class="alert alert-warning" role="alert" style="margin-top: 1em;">
|
||||||
You are voting as: <strong>{{ voter_email }}</strong> - Ensure this is correct and don't share this URL!
|
You are voting as: <strong>{{ voter_email }}</strong> - Ensure this is correct and don't share this URL!
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,23 +37,13 @@
|
||||||
You will be shown each poll for this event one by one where you will need to make a selection for the current
|
You will be shown each poll for this event one by one where you will need to make a selection for the current
|
||||||
poll before moving onto the next poll. <strong>For this specific poll</strong> you need to make a <strong>
|
poll before moving onto the next poll. <strong>For this specific poll</strong> you need to make a <strong>
|
||||||
minimum</strong> of {{ min_selection }} option selection(s) and a <strong>maximum</strong> of
|
minimum</strong> of {{ min_selection }} option selection(s) and a <strong>maximum</strong> of
|
||||||
{{ max_selection }}. Please make your choice below.
|
{{ max_selection }}. <br/><br/>Please make your choice below.
|
||||||
</span>
|
</span>
|
||||||
<div class="panel panel-body">
|
|
||||||
{% if prev_index %}
|
<!-- Poll Voting Section -->
|
||||||
<a href="{% url 'polls:event-vote' event_id=object.event.id poll_num=prev_index %}" class="btn" role="button">
|
|
||||||
<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if next_index %}
|
|
||||||
<a href="{% url 'polls:event-vote' event_id=object.event.id poll_num=next_index %}" class="btn" role="button">
|
|
||||||
<span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if object.options.all %}
|
|
||||||
<h3>Poll {{ poll_num }} of {{ poll_count }}: {{object.question_text}}</h3>
|
<h3>Poll {{ poll_num }} of {{ poll_count }}: {{object.question_text}}</h3>
|
||||||
{% if can_vote %}
|
<hr/>
|
||||||
|
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Options</strong></div>
|
<div class="panel-heading"><strong>Options</strong></div>
|
||||||
|
@ -67,15 +62,26 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
|
||||||
|
<!-- Poll Navigation -->
|
||||||
|
<div class="panel panel-body">
|
||||||
|
{% if prev_uuid %}
|
||||||
|
<a href="{% url 'polls:event-vote' event_id=object.event.uuid poll_id=prev_uuid %}?key={{ a_key }}" class="btn btn-danger"
|
||||||
|
role="button">
|
||||||
|
<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span> Previous Poll
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if next_uuid %}
|
||||||
|
<a href="{% url 'polls:event-vote' event_id=object.event.uuid poll_id=next_uuid %}?key={{ a_key }}" class="btn btn-primary"
|
||||||
|
role="button" style="float: right;">
|
||||||
|
Next Poll <span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %} <!-- for: { if can_vote %} -->
|
||||||
<div class="alert alert-warning" role="alert">
|
<div class="alert alert-warning" role="alert">
|
||||||
<p>You don't have permission to vote in this event.</p>
|
<p>{{ cant_vote_reason }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
|
||||||
<p>No options are available.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
Reference in a new issue