diff --git a/.gitignore b/.gitignore
index a2dca2bee7e168df398b922f596e4e4a6e230a26..8db764f4eadb6e11a9fc766cea5aa1a491b66aa2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,7 @@ deploy-latest.sh
 .DS_Store
 *~
 media/*
-venv
+venv*
 celerybeat-*
 env.sh
 .cache
diff --git a/.travis.yml b/.travis.yml
index 1505fccb9e5bd689f6964ac911b32fa86dac2b92..476acaf46b8d58147f43cfecad671310e550741f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,42 +1,46 @@
-sudo: false
 language: python
-python:
-  - "2.7"
-
 os: linux
 
-before_install:
-  - export BOTO_CONFIG=/dev/null
-
-install:
-  - pip install --upgrade pip
-  - pip install -r requirements.txt
-
-before_script:
-  - psql -c 'create database helios;' -U postgres
-
-script: "python -Wall manage.py test"
-
 jobs:
   include:
-  - dist: trusty
-    addons:
-      postgresql: "9.3"
-  - dist: trusty
-    addons:
-      postgresql: "9.4"
-  - dist: trusty
+  - python: "3.7"
+    dist: xenial
     addons:
       postgresql: "9.5"
-  - dist: trusty
+  - python: "3.7"
+    dist: xenial
     addons:
       postgresql: "9.6"
-  - dist: xenial
+  - python: "3.7"
+    dist: xenial
     addons:
-      postgresql: "9.4"
-  - dist: xenial
+      postgresql: "10"
+  - python: "3.8"
+    dist: bionic
     addons:
       postgresql: "9.5"
-  - dist: xenial
+  - python: "3.8"
+    dist: bionic
     addons:
       postgresql: "9.6"
+  - python: "3.8"
+    dist: bionic
+    addons:
+      postgresql: "10"
+  - python: "3.8"
+    dist: bionic
+    addons:
+      postgresql: "11"
+
+before_install:
+  - export BOTO_CONFIG=/dev/null
+
+install:
+  - pip3 install --upgrade pip
+  - pip3 install -r requirements.txt
+  - pip3 freeze
+
+before_script:
+  - psql -c 'create database helios;' -U postgres
+
+script: "python3 -Wall manage.py test -v 2"
diff --git a/INSTALL.md b/INSTALL.md
index b76f642a7f50ace2bfc15505418c249e20362060..aff5309427caec9bc59af69dc5041f75e17150c8 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -1,4 +1,4 @@
-* install PostgreSQL 8.3+
+* install PostgreSQL 9.5+
 
 * install Rabbit MQ
   This is needed for celery to work, which does background processing such as
@@ -11,12 +11,22 @@ http://www.virtualenv.org/en/latest/
 
 * cd into the helios-server directory
 
+* install Python3.6 including dev, pip, and venv
+
+```
+sudo apt install python3.6 python3.6-venv python3.6-pip python3.6-venv
+```
+
 * create a virtualenv
-* make sure you use Python2.7 and **not** Python3+
-* The above can be done by typing something similar to
 
 ```
-virtualenv --python=/usr/bin/python2.7 $(pwd)/venv
+python3.6-m venv $(pwd)/venv
+```
+
+* you'll also need Postgres dev libraries. For example on Ubuntu:
+
+```
+sudo apt install libpq-dev 
 ```
 
 * activate virtual environment
diff --git a/helios/__init__.py b/helios/__init__.py
index 2f598298b565f3a883cf9365699a6911031e0120..21f21bc59da930240e0c88612f1afcc9c0a46178 100644
--- a/helios/__init__.py
+++ b/helios/__init__.py
@@ -1,7 +1,7 @@
 from django.conf import settings
 # This will make sure the app is always imported when
 # Django starts so that shared_task will use this app.
-from celery_app import app as celery_app
+from .celery_app import app as celery_app
 
 __all__ = ('celery_app', 'TEMPLATE_BASE', 'ADMIN_ONLY', 'VOTERS_UPLOAD', 'VOTERS_EMAIL',)
 
diff --git a/helios/crypto/algs.py b/helios/crypto/algs.py
index acac4f44b4c935a49f4873e8dea92bcb17da0bbc..b7439d9bd3d50cd738b354fcfbfe64495df0db78 100644
--- a/helios/crypto/algs.py
+++ b/helios/crypto/algs.py
@@ -7,157 +7,61 @@ Ben Adida
 ben@adida.net
 """
 
-import math, hashlib, logging
-import randpool, number
+import logging
 
-import numtheory
+from Crypto.Hash import SHA1
+from Crypto.Util import number
 
-# some utilities
-class Utils:
-    RAND = randpool.RandomPool()
-
-    @classmethod
-    def random_seed(cls, data):
-        cls.RAND.add_event(data)
-
-    @classmethod
-    def random_mpz(cls, n_bits):
-        low = 2**(n_bits-1)
-        high = low * 2
-
-        # increment and find a prime
-        # return randrange(low, high)
-
-        return number.getRandomNumber(n_bits, cls.RAND.get_bytes)
-
-    @classmethod
-    def random_mpz_lt(cls, max):
-        # return randrange(0, max)
-        n_bits = int(math.floor(math.log(max, 2)))
-        return (number.getRandomNumber(n_bits, cls.RAND.get_bytes) % max)
-
-    @classmethod
-    def random_prime(cls, n_bits):
-        return number.getPrime(n_bits, cls.RAND.get_bytes)
-
-    @classmethod
-    def is_prime(cls, mpz):
-        #return numtheory.miller_rabin(mpz)
-        return number.isPrime(mpz)
-
-    @classmethod
-    def xgcd(cls, a, b):
-        """
-        Euclid's Extended GCD algorithm
-        """
-        mod = a%b
-
-        if mod == 0:
-            return 0,1
-        else:
-            x,y = cls.xgcd(b, mod)
-            return y, x-(y*(a/b))
-
-    @classmethod
-    def inverse(cls, mpz, mod):
-        # return cls.xgcd(mpz,mod)[0]
-        return number.inverse(mpz, mod)
-
-    @classmethod
-    def random_safe_prime(cls, n_bits):
-      p = None
-      q = None
-
-      while True:
-        p = cls.random_prime(n_bits)
-        q = (p-1)/2
-        if cls.is_prime(q):
-          return p
-
-    @classmethod
-    def random_special_prime(cls, q_n_bits, p_n_bits):
-        p = None
-        q = None
-
-        z_n_bits = p_n_bits - q_n_bits
-
-        q = cls.random_prime(q_n_bits)
-
-        while True:
-            z = cls.random_mpz(z_n_bits)
-            p = q*z + 1
-            if cls.is_prime(p):
-                return p, q, z
+from helios.crypto.utils import random
+from helios.utils import to_json
 
 
 class ElGamal:
     def __init__(self):
-      self.p = None
-      self.q = None
-      self.g = None
-
-    @classmethod
-    def generate(cls, n_bits):
-      """
-      generate an El-Gamal environment. Returns an instance
-      of ElGamal(), with prime p, group size q, and generator g
-      """
-
-      EG = ElGamal()
-
-      # find a prime p such that (p-1)/2 is prime q
-      EG.p = Utils.random_safe_prime(n_bits)
-
-      # q is the order of the group
-      # FIXME: not always p-1/2
-      EG.q = (EG.p-1)/2
-
-      # find g that generates the q-order subgroup
-      while True:
-        EG.g = Utils.random_mpz_lt(EG.p)
-        if pow(EG.g, EG.q, EG.p) == 1:
-          break
-
-      return EG
+        self.p = None
+        self.q = None
+        self.g = None
 
     def generate_keypair(self):
-      """
-      generates a keypair in the setting
-      """
+        """
+        generates a keypair in the setting
+        """
 
-      keypair = EGKeyPair()
-      keypair.generate(self.p, self.q, self.g)
+        keypair = EGKeyPair()
+        keypair.generate(self.p, self.q, self.g)
 
-      return keypair
+        return keypair
 
     def toJSONDict(self):
-      return {'p': str(self.p), 'q': str(self.q), 'g': str(self.g)}
+        return {'p': str(self.p), 'q': str(self.q), 'g': str(self.g)}
 
     @classmethod
     def fromJSONDict(cls, d):
-      eg = cls()
-      eg.p = int(d['p'])
-      eg.q = int(d['q'])
-      eg.g = int(d['g'])
-      return eg
+        eg = cls()
+        eg.p = int(d['p'])
+        eg.q = int(d['q'])
+        eg.g = int(d['g'])
+        return eg
+
 
 class EGKeyPair:
     def __init__(self):
-      self.pk = EGPublicKey()
-      self.sk = EGSecretKey()
+        self.pk = EGPublicKey()
+        self.sk = EGSecretKey()
 
     def generate(self, p, q, g):
-      """
-      Generate an ElGamal keypair
-      """
-      self.pk.g = g
-      self.pk.p = p
-      self.pk.q = q
+        """
+        Generate an ElGamal keypair
+        """
+        self.pk.g = g
+        self.pk.p = p
+        self.pk.q = q
+
+        self.sk.x = random.mpz_lt(q)
+        self.pk.y = pow(g, self.sk.x, p)
 
-      self.sk.x = Utils.random_mpz_lt(q)
-      self.pk.y = pow(g, self.sk.x, p)
+        self.sk.pk = self.pk
 
-      self.sk.pk = self.pk
 
 class EGPublicKey:
     def __init__(self):
@@ -166,7 +70,7 @@ class EGPublicKey:
         self.g = None
         self.q = None
 
-    def encrypt_with_r(self, plaintext, r, encode_message= False):
+    def encrypt_with_r(self, plaintext, r, encode_message=False):
         """
         expecting plaintext.m to be a big integer
         """
@@ -175,13 +79,13 @@ class EGPublicKey:
 
         # make sure m is in the right subgroup
         if encode_message:
-          y = plaintext.m + 1
-          if pow(y, self.q, self.p) == 1:
-            m = y
-          else:
-            m = -y % self.p
+            y = plaintext.m + 1
+            if pow(y, self.q, self.p) == 1:
+                m = y
+            else:
+                m = -y % self.p
         else:
-          m = plaintext.m
+            m = plaintext.m
 
         ciphertext.alpha = pow(self.g, r, self.p)
         ciphertext.beta = (m * pow(self.y, r, self.p)) % self.p
@@ -192,7 +96,7 @@ class EGPublicKey:
         """
         Encrypt a plaintext and return the randomness just generated and used.
         """
-        r = Utils.random_mpz_lt(self.q)
+        r = random.mpz_lt(self.q)
         ciphertext = self.encrypt_with_r(plaintext, r)
 
         return [ciphertext, r]
@@ -207,70 +111,69 @@ class EGPublicKey:
         """
         Serialize to dictionary.
         """
-        return {'y' : str(self.y), 'p' : str(self.p), 'g' : str(self.g) , 'q' : str(self.q)}
+        return {'y': str(self.y), 'p': str(self.p), 'g': str(self.g), 'q': str(self.q)}
 
     toJSONDict = to_dict
 
     # quick hack FIXME
     def toJSON(self):
-      import utils
-      return utils.to_json(self.toJSONDict())
+        return to_json(self.toJSONDict())
 
-    def __mul__(self,other):
-      if other == 0 or other == 1:
-        return self
+    def __mul__(self, other):
+        if other == 0 or other == 1:
+            return self
 
-      # check p and q
-      if self.p != other.p or self.q != other.q or self.g != other.g:
-        raise Exception("incompatible public keys")
+        # check p and q
+        if self.p != other.p or self.q != other.q or self.g != other.g:
+            raise Exception("incompatible public keys")
 
-      result = EGPublicKey()
-      result.p = self.p
-      result.q = self.q
-      result.g = self.g
-      result.y = (self.y * other.y) % result.p
-      return result
+        result = EGPublicKey()
+        result.p = self.p
+        result.q = self.q
+        result.g = self.g
+        result.y = (self.y * other.y) % result.p
+        return result
 
-    def verify_sk_proof(self, dlog_proof, challenge_generator = None):
-      """
-      verify the proof of knowledge of the secret key
-      g^response = commitment * y^challenge
-      """
-      left_side = pow(self.g, dlog_proof.response, self.p)
-      right_side = (dlog_proof.commitment * pow(self.y, dlog_proof.challenge, self.p)) % self.p
+    def verify_sk_proof(self, dlog_proof, challenge_generator=None):
+        """
+        verify the proof of knowledge of the secret key
+        g^response = commitment * y^challenge
+        """
+        left_side = pow(self.g, dlog_proof.response, self.p)
+        right_side = (dlog_proof.commitment * pow(self.y, dlog_proof.challenge, self.p)) % self.p
 
-      expected_challenge = challenge_generator(dlog_proof.commitment) % self.q
+        expected_challenge = challenge_generator(dlog_proof.commitment) % self.q
 
-      return ((left_side == right_side) and (dlog_proof.challenge == expected_challenge))
+        return (left_side == right_side) and (dlog_proof.challenge == expected_challenge)
 
     def validate_pk_params(self):
-      # check primality of p
-      if not number.isPrime(self.p):
-        raise Exception("p is not prime.")
+        # check primality of p
+        if not number.isPrime(self.p):
+            raise Exception("p is not prime.")
 
-      # check length of p
-      if not (number.size(self.p) >= 2048):
-        raise Exception("p of insufficient length. Should be 2048 bits or greater.")
+        # check length of p
+        if not (number.size(self.p) >= 2048):
+            raise Exception("p of insufficient length. Should be 2048 bits or greater.")
 
-      # check primality of q
-      if not number.isPrime(self.q):
-        raise Exception("q is not prime.")
+        # check primality of q
+        if not number.isPrime(self.q):
+            raise Exception("q is not prime.")
 
-      # check length of q
-      if not (number.size(self.q) >= 256):
-        raise Exception("q of insufficient length. Should be 256 bits or greater.")
+        # check length of q
+        if not (number.size(self.q) >= 256):
+            raise Exception("q of insufficient length. Should be 256 bits or greater.")
 
-      if (pow(self.g,self.q,self.p)!=1):
-        raise Exception("g does not generate subgroup of order q.")
+        if pow(self.g, self.q, self.p) != 1:
+            raise Exception("g does not generate subgroup of order q.")
 
-      if not (1 < self.g < self.p-1):
-        raise Exception("g out of range.")
+        if not (1 < self.g < self.p - 1):
+            raise Exception("g out of range.")
 
-      if not (1 < self.y < self.p-1):
-        raise Exception("y out of range.")
+        if not (1 < self.y < self.p - 1):
+            raise Exception("y out of range.")
 
-      if (pow(self.y,self.q,self.p)!=1):
-        raise Exception("g does not generate proper group.")
+        if pow(self.y, self.q, self.p) != 1:
+            raise Exception("g does not generate proper group.")
 
     @classmethod
     def from_dict(cls, d):
@@ -284,14 +187,15 @@ class EGPublicKey:
         pk.q = int(d['q'])
 
         try:
-          pk.validate_pk_params()
+            pk.validate_pk_params()
         except Exception as e:
-          raise
+            raise e
 
         return pk
 
     fromJSONDict = from_dict
 
+
 class EGSecretKey:
     def __init__(self):
         self.x = None
@@ -317,25 +221,25 @@ class EGSecretKey:
 
         return dec_factor, proof
 
-    def decrypt(self, ciphertext, dec_factor = None, decode_m=False):
+    def decrypt(self, ciphertext, dec_factor=None, decode_m=False):
         """
         Decrypt a ciphertext. Optional parameter decides whether to encode the message into the proper subgroup.
         """
         if not dec_factor:
             dec_factor = self.decryption_factor(ciphertext)
 
-        m = (Utils.inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p
+        m = (number.inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p
 
         if decode_m:
-          # get m back from the q-order subgroup
-          if m < self.pk.q:
-            y = m
-          else:
-            y = -m % self.pk.p
+            # get m back from the q-order subgroup
+            if m < self.pk.q:
+                y = m
+            else:
+                y = -m % self.pk.p
 
-          return EGPlaintext(y-1, self.pk)
+            return EGPlaintext(y - 1, self.pk)
         else:
-          return EGPlaintext(m, self.pk)
+            return EGPlaintext(m, self.pk)
 
     def prove_decryption(self, ciphertext):
         """
@@ -350,66 +254,66 @@ class EGSecretKey:
         and alpha^t = b * beta/m ^ c
         """
 
-        m = (Utils.inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p
-        beta_over_m = (ciphertext.beta * Utils.inverse(m, self.pk.p)) % self.pk.p
+        m = (number.inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p
+        beta_over_m = (ciphertext.beta * number.inverse(m, self.pk.p)) % self.pk.p
 
         # pick a random w
-        w = Utils.random_mpz_lt(self.pk.q)
+        w = random.mpz_lt(self.pk.q)
         a = pow(self.pk.g, w, self.pk.p)
         b = pow(ciphertext.alpha, w, self.pk.p)
 
-        c = int(hashlib.sha1(str(a) + "," + str(b)).hexdigest(),16)
+        c = int(SHA1.new(bytes(str(a) + "," + str(b), 'utf-8')).hexdigest(), 16)
 
         t = (w + self.x * c) % self.pk.q
 
         return m, {
-            'commitment' : {'A' : str(a), 'B': str(b)},
-            'challenge' : str(c),
-            'response' : str(t)
-          }
+            'commitment': {'A': str(a), 'B': str(b)},
+            'challenge': str(c),
+            'response': str(t)
+        }
 
     def to_dict(self):
-        return {'x' : str(self.x), 'public_key' : self.pk.to_dict()}
+        return {'x': str(self.x), 'public_key': self.pk.to_dict()}
 
     toJSONDict = to_dict
 
     def prove_sk(self, challenge_generator):
-      """
-      Generate a PoK of the secret key
-      Prover generates w, a random integer modulo q, and computes commitment = g^w mod p.
-      Verifier provides challenge modulo q.
-      Prover computes response = w + x*challenge mod q, where x is the secret key.
-      """
-      w = Utils.random_mpz_lt(self.pk.q)
-      commitment = pow(self.pk.g, w, self.pk.p)
-      challenge = challenge_generator(commitment) % self.pk.q
-      response = (w + (self.x * challenge)) % self.pk.q
-
-      return DLogProof(commitment, challenge, response)
+        """
+        Generate a PoK of the secret key
+        Prover generates w, a random integer modulo q, and computes commitment = g^w mod p.
+        Verifier provides challenge modulo q.
+        Prover computes response = w + x*challenge mod q, where x is the secret key.
+        """
+        w = random.mpz_lt(self.pk.q)
+        commitment = pow(self.pk.g, w, self.pk.p)
+        challenge = challenge_generator(commitment) % self.pk.q
+        response = (w + (self.x * challenge)) % self.pk.q
 
+        return DLogProof(commitment, challenge, response)
 
     @classmethod
     def from_dict(cls, d):
         if not d:
-          return None
+            return None
 
         sk = cls()
         sk.x = int(d['x'])
-        if d.has_key('public_key'):
-          sk.pk = EGPublicKey.from_dict(d['public_key'])
+        if 'public_key' in d:
+            sk.pk = EGPublicKey.from_dict(d['public_key'])
         else:
-          sk.pk = None
+            sk.pk = None
         return sk
 
     fromJSONDict = from_dict
 
+
 class EGPlaintext:
-    def __init__(self, m = None, pk = None):
+    def __init__(self, m=None, pk=None):
         self.m = m
         self.pk = pk
 
     def to_dict(self):
-        return {'m' : self.m}
+        return {'m': self.m}
 
     @classmethod
     def from_dict(cls, d):
@@ -424,17 +328,17 @@ class EGCiphertext:
         self.alpha = alpha
         self.beta = beta
 
-    def __mul__(self,other):
+    def __mul__(self, other):
         """
         Homomorphic Multiplication of ciphertexts.
         """
-        if type(other) == int and (other == 0 or other == 1):
-          return self
+        if isinstance(other, int) and (other == 0 or other == 1):
+            return self
 
         if self.pk != other.pk:
-          logging.info(self.pk)
-          logging.info(other.pk)
-          raise Exception('different PKs!')
+            logging.info(self.pk)
+            logging.info(other.pk)
+            raise Exception('different PKs!')
 
         new = EGCiphertext()
 
@@ -460,7 +364,7 @@ class EGCiphertext:
         """
         Reencryption with fresh randomness, which is returned.
         """
-        r = Utils.random_mpz_lt(self.pk.q)
+        r = random.mpz_lt(self.pk.q)
         new_c = self.reenc_with_r(r)
         return [new_c, r]
 
@@ -471,189 +375,195 @@ class EGCiphertext:
         return self.reenc_return_r()[0]
 
     def __eq__(self, other):
-      """
-      Check for ciphertext equality.
-      """
-      if other == None:
-        return False
+        """
+        Check for ciphertext equality.
+        """
+        if other is None:
+            return False
 
-      return (self.alpha == other.alpha and self.beta == other.beta)
+        return self.alpha == other.alpha and self.beta == other.beta
 
     def generate_encryption_proof(self, plaintext, randomness, challenge_generator):
-      """
-      Generate the disjunctive encryption proof of encryption
-      """
-      # random W
-      w = Utils.random_mpz_lt(self.pk.q)
+        """
+        Generate the disjunctive encryption proof of encryption
+        """
+        # random W
+        w = random.mpz_lt(self.pk.q)
 
-      # build the proof
-      proof = EGZKProof()
+        # build the proof
+        proof = EGZKProof()
 
-      # compute A=g^w, B=y^w
-      proof.commitment['A'] = pow(self.pk.g, w, self.pk.p)
-      proof.commitment['B'] = pow(self.pk.y, w, self.pk.p)
+        # compute A=g^w, B=y^w
+        proof.commitment['A'] = pow(self.pk.g, w, self.pk.p)
+        proof.commitment['B'] = pow(self.pk.y, w, self.pk.p)
 
-      # generate challenge
-      proof.challenge = challenge_generator(proof.commitment);
+        # generate challenge
+        proof.challenge = challenge_generator(proof.commitment)
 
-      # Compute response = w + randomness * challenge
-      proof.response = (w + (randomness * proof.challenge)) % self.pk.q;
+        # Compute response = w + randomness * challenge
+        proof.response = (w + (randomness * proof.challenge)) % self.pk.q
 
-      return proof;
+        return proof
 
     def simulate_encryption_proof(self, plaintext, challenge=None):
-      # generate a random challenge if not provided
-      if not challenge:
-        challenge = Utils.random_mpz_lt(self.pk.q)
+        # generate a random challenge if not provided
+        if not challenge:
+            challenge = random.mpz_lt(self.pk.q)
 
-      proof = EGZKProof()
-      proof.challenge = challenge
+        proof = EGZKProof()
+        proof.challenge = challenge
 
-      # compute beta/plaintext, the completion of the DH tuple
-      beta_over_plaintext =  (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p
+        # compute beta/plaintext, the completion of the DH tuple
+        beta_over_plaintext = (self.beta * number.inverse(plaintext.m, self.pk.p)) % self.pk.p
 
-      # random response, does not even need to depend on the challenge
-      proof.response = Utils.random_mpz_lt(self.pk.q);
+        # random response, does not even need to depend on the challenge
+        proof.response = random.mpz_lt(self.pk.q)
 
-      # now we compute A and B
-      proof.commitment['A'] = (Utils.inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.g, proof.response, self.pk.p)) % self.pk.p
-      proof.commitment['B'] = (Utils.inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.y, proof.response, self.pk.p)) % self.pk.p
+        # now we compute A and B
+        proof.commitment['A'] = (number.inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p)
+                                 * pow(self.pk.g, proof.response, self.pk.p)
+                                 ) % self.pk.p
+        proof.commitment['B'] = (number.inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow(
+            self.pk.y, proof.response, self.pk.p)) % self.pk.p
 
-      return proof
+        return proof
 
     def generate_disjunctive_encryption_proof(self, plaintexts, real_index, randomness, challenge_generator):
-      # note how the interface is as such so that the result does not reveal which is the real proof.
+        # note how the interface is as such so that the result does not reveal which is the real proof.
 
-      proofs = [None for p in plaintexts]
+        proofs = [None for _ in plaintexts]
 
-      # go through all plaintexts and simulate the ones that must be simulated.
-      for p_num in range(len(plaintexts)):
-        if p_num != real_index:
-          proofs[p_num] = self.simulate_encryption_proof(plaintexts[p_num])
+        # go through all plaintexts and simulate the ones that must be simulated.
+        for p_num in range(len(plaintexts)):
+            if p_num != real_index:
+                proofs[p_num] = self.simulate_encryption_proof(plaintexts[p_num])
 
-      # the function that generates the challenge
-      def real_challenge_generator(commitment):
-        # set up the partial real proof so we're ready to get the hash
-        proofs[real_index] = EGZKProof()
-        proofs[real_index].commitment = commitment
+        # the function that generates the challenge
+        def real_challenge_generator(commitment):
+            # set up the partial real proof so we're ready to get the hash
+            proofs[real_index] = EGZKProof()
+            proofs[real_index].commitment = commitment
 
-        # get the commitments in a list and generate the whole disjunctive challenge
-        commitments = [p.commitment for p in proofs]
-        disjunctive_challenge = challenge_generator(commitments);
+            # get the commitments in a list and generate the whole disjunctive challenge
+            commitments = [p.commitment for p in proofs]
+            disjunctive_challenge = challenge_generator(commitments)
 
-        # now we must subtract all of the other challenges from this challenge.
-        real_challenge = disjunctive_challenge
-        for p_num in range(len(proofs)):
-          if p_num != real_index:
-            real_challenge = real_challenge - proofs[p_num].challenge
+            # now we must subtract all of the other challenges from this challenge.
+            real_challenge = disjunctive_challenge
+            for p_num in range(len(proofs)):
+                if p_num != real_index:
+                    real_challenge = real_challenge - proofs[p_num].challenge
 
-        # make sure we mod q, the exponent modulus
-        return real_challenge % self.pk.q
+            # make sure we mod q, the exponent modulus
+            return real_challenge % self.pk.q
 
-      # do the real proof
-      real_proof = self.generate_encryption_proof(plaintexts[real_index], randomness, real_challenge_generator)
+        # do the real proof
+        real_proof = self.generate_encryption_proof(plaintexts[real_index], randomness, real_challenge_generator)
 
-      # set the real proof
-      proofs[real_index] = real_proof
+        # set the real proof
+        proofs[real_index] = real_proof
 
-      return EGZKDisjunctiveProof(proofs)
+        return EGZKDisjunctiveProof(proofs)
 
     def verify_encryption_proof(self, plaintext, proof):
-      """
-      Checks for the DDH tuple g, y, alpha, beta/plaintext.
-      (PoK of randomness r.)
-
-      Proof contains commitment = {A, B}, challenge, response
-      """
-      # check that A, B are in the correct group
-      if not (pow(proof.commitment['A'],self.pk.q,self.pk.p)==1 and pow(proof.commitment['B'],self.pk.q,self.pk.p)==1):
-        return False
+        """
+        Checks for the DDH tuple g, y, alpha, beta/plaintext.
+        (PoK of randomness r.)
+
+        Proof contains commitment = {A, B}, challenge, response
+        """
+        # check that A, B are in the correct group
+        if not (pow(proof.commitment['A'], self.pk.q, self.pk.p) == 1 and pow(proof.commitment['B'], self.pk.q,
+                                                                              self.pk.p) == 1):
+            return False
 
-      # check that g^response = A * alpha^challenge
-      first_check = (pow(self.pk.g, proof.response, self.pk.p) == ((pow(self.alpha, proof.challenge, self.pk.p) * proof.commitment['A']) % self.pk.p))
+        # check that g^response = A * alpha^challenge
+        first_check = (pow(self.pk.g, proof.response, self.pk.p) == (
+                (pow(self.alpha, proof.challenge, self.pk.p) * proof.commitment['A']) % self.pk.p))
 
-      # check that y^response = B * (beta/m)^challenge
-      beta_over_m = (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p
-      second_check = (pow(self.pk.y, proof.response, self.pk.p) == ((pow(beta_over_m, proof.challenge, self.pk.p) * proof.commitment['B']) % self.pk.p))
+        # check that y^response = B * (beta/m)^challenge
+        beta_over_m = (self.beta * number.inverse(plaintext.m, self.pk.p)) % self.pk.p
+        second_check = (pow(self.pk.y, proof.response, self.pk.p) == (
+                (pow(beta_over_m, proof.challenge, self.pk.p) * proof.commitment['B']) % self.pk.p))
 
-      # print "1,2: %s %s " % (first_check, second_check)
-      return (first_check and second_check)
+        # print "1,2: %s %s " % (first_check, second_check)
+        return first_check and second_check
 
     def verify_disjunctive_encryption_proof(self, plaintexts, proof, challenge_generator):
-      """
-      plaintexts and proofs are all lists of equal length, with matching.
+        """
+        plaintexts and proofs are all lists of equal length, with matching.
 
-      overall_challenge is what all of the challenges combined should yield.
-      """
-      if len(plaintexts) != len(proof.proofs):
-        print("bad number of proofs (expected %s, found %s)" % (len(plaintexts), len(proof.proofs)))
-        return False
+        overall_challenge is what all of the challenges combined should yield.
+        """
+        if len(plaintexts) != len(proof.proofs):
+            print("bad number of proofs (expected %s, found %s)" % (len(plaintexts), len(proof.proofs)))
+            return False
 
-      for i in range(len(plaintexts)):
-        # if a proof fails, stop right there
-        if not self.verify_encryption_proof(plaintexts[i], proof.proofs[i]):
-          print "bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i])
-          return False
+        for i in range(len(plaintexts)):
+            # if a proof fails, stop right there
+            if not self.verify_encryption_proof(plaintexts[i], proof.proofs[i]):
+                print("bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i]))
+                return False
 
-      # logging.info("made it past the two encryption proofs")
+        # logging.info("made it past the two encryption proofs")
 
-      # check the overall challenge
-      return (challenge_generator([p.commitment for p in proof.proofs]) == (sum([p.challenge for p in proof.proofs]) % self.pk.q))
+        # check the overall challenge
+        return (challenge_generator([p.commitment for p in proof.proofs]) == (
+                sum([p.challenge for p in proof.proofs]) % self.pk.q))
 
     def verify_decryption_proof(self, plaintext, proof):
-      """
-      Checks for the DDH tuple g, alpha, y, beta/plaintext
-      (PoK of secret key x.)
-      """
-      return False
+        """
+        Checks for the DDH tuple g, alpha, y, beta/plaintext
+        (PoK of secret key x.)
+        """
+        return False
 
     def verify_decryption_factor(self, dec_factor, dec_proof, public_key):
-      """
-      when a ciphertext is decrypted by a dec factor, the proof needs to be checked
-      """
-      pass
+        """
+        when a ciphertext is decrypted by a dec factor, the proof needs to be checked
+        """
+        pass
 
     def decrypt(self, decryption_factors, public_key):
-      """
-      decrypt a ciphertext given a list of decryption factors (from multiple trustees)
-      For now, no support for threshold
-      """
-      running_decryption = self.beta
-      for dec_factor in decryption_factors:
-        running_decryption = (running_decryption * Utils.inverse(dec_factor, public_key.p)) % public_key.p
+        """
+        decrypt a ciphertext given a list of decryption factors (from multiple trustees)
+        For now, no support for threshold
+        """
+        running_decryption = self.beta
+        for dec_factor in decryption_factors:
+            running_decryption = (running_decryption * number.inverse(dec_factor, public_key.p)) % public_key.p
 
-      return running_decryption
+        return running_decryption
 
     def check_group_membership(self, pk):
-      """
-      checks to see if an ElGamal element belongs to the group in the pk
-      """
-      if not (1 < self.alpha < pk.p-1):
-        return False
-
-      elif not (1 < self.beta < pk.p-1):
-        return False
+        """
+        checks to see if an ElGamal element belongs to the group in the pk
+        """
+        if not (1 < self.alpha < pk.p - 1):
+            return False
 
-      elif (pow(self.alpha, pk.q, pk.p)!=1):
-        return False
+        elif not (1 < self.beta < pk.p - 1):
+            return False
 
-      elif (pow(self.beta, pk.q, pk.p)!=1):
-        return False
+        elif pow(self.alpha, pk.q, pk.p) != 1:
+            return False
 
-      else:
-        return True
+        elif pow(self.beta, pk.q, pk.p) != 1:
+            return False
 
+        else:
+            return True
 
     def to_dict(self):
         return {'alpha': str(self.alpha), 'beta': str(self.beta)}
 
-    toJSONDict= to_dict
+    toJSONDict = to_dict
 
     def to_string(self):
         return "%s,%s" % (self.alpha, self.beta)
 
     @classmethod
-    def from_dict(cls, d, pk = None):
+    def from_dict(cls, d, pk=None):
         result = cls()
         result.alpha = int(d['alpha'])
         result.beta = int(d['beta'])
@@ -668,127 +578,134 @@ class EGCiphertext:
         expects alpha,beta
         """
         split = str.split(",")
-        return cls.from_dict({'alpha' : split[0], 'beta' : split[1]})
+        return cls.from_dict({'alpha': split[0], 'beta': split[1]})
+
 
 class EGZKProof(object):
-  def __init__(self):
-    self.commitment = {'A':None, 'B':None}
-    self.challenge = None
-    self.response = None
+    def __init__(self):
+        self.commitment = {'A': None, 'B': None}
+        self.challenge = None
+        self.response = None
 
-  @classmethod
-  def generate(cls, little_g, little_h, x, p, q, challenge_generator):
-      """
-      generate a DDH tuple proof, where challenge generator is
-      almost certainly EG_fiatshamir_challenge_generator
-      """
+    @classmethod
+    def generate(cls, little_g, little_h, x, p, q, challenge_generator):
+        """
+        generate a DDH tuple proof, where challenge generator is
+        almost certainly EG_fiatshamir_challenge_generator
+        """
 
-      # generate random w
-      w = Utils.random_mpz_lt(q)
+        # generate random w
+        w = random.mpz_lt(q)
 
-      # create proof instance
-      proof = cls()
+        # create proof instance
+        proof = cls()
 
-      # compute A = little_g^w, B=little_h^w
-      proof.commitment['A'] = pow(little_g, w, p)
-      proof.commitment['B'] = pow(little_h, w, p)
+        # compute A = little_g^w, B=little_h^w
+        proof.commitment['A'] = pow(little_g, w, p)
+        proof.commitment['B'] = pow(little_h, w, p)
 
-      # get challenge
-      proof.challenge = challenge_generator(proof.commitment)
+        # get challenge
+        proof.challenge = challenge_generator(proof.commitment)
 
-      # compute response
-      proof.response = (w + (x * proof.challenge)) % q
+        # compute response
+        proof.response = (w + (x * proof.challenge)) % q
 
-      # return proof
-      return proof
+        # return proof
+        return proof
 
-  @classmethod
-  def from_dict(cls, d):
-    p = cls()
-    p.commitment = {'A': int(d['commitment']['A']), 'B': int(d['commitment']['B'])}
-    p.challenge = int(d['challenge'])
-    p.response = int(d['response'])
-    return p
+    @classmethod
+    def from_dict(cls, d):
+        p = cls()
+        p.commitment = {'A': int(d['commitment']['A']), 'B': int(d['commitment']['B'])}
+        p.challenge = int(d['challenge'])
+        p.response = int(d['response'])
+        return p
 
-  fromJSONDict = from_dict
+    fromJSONDict = from_dict
+
+    def to_dict(self):
+        return {
+            'commitment': {'A': str(self.commitment['A']), 'B': str(self.commitment['B'])},
+            'challenge': str(self.challenge),
+            'response': str(self.response)
+        }
 
-  def to_dict(self):
-    return {
-      'commitment' : {'A' : str(self.commitment['A']), 'B' : str(self.commitment['B'])},
-      'challenge': str(self.challenge),
-      'response': str(self.response)
-    }
+    toJSONDict = to_dict
 
-  def verify(self, little_g, little_h, big_g, big_h, p, q, challenge_generator=None):
-    """
-    Verify a DH tuple proof
-    """
-    # check that A, B are in the correct group
-    if not (pow(proof.commitment['A'],self.pk.q,self.pk.p)==1 and pow(proof.commitment['B'],self.pk.q,self.pk.p)==1):
-      return False
+    def verify(self, little_g, little_h, big_g, big_h, p, q, challenge_generator=None):
+        """
+        Verify a DH tuple proof
+        """
+        # check that A, B are in the correct group
+        if not (pow(self.commitment['A'], self.pk.q, self.pk.p) == 1
+                and pow(self.commitment['B'], self.pk.q, self.pk.p) == 1):
+            return False
 
-    # check that little_g^response = A * big_g^challenge
-    first_check = (pow(little_g, self.response, p) == ((pow(big_g, self.challenge, p) * self.commitment['A']) % p))
+        # check that little_g^response = A * big_g^challenge
+        first_check = (pow(little_g, self.response, p) == ((pow(big_g, self.challenge, p) * self.commitment['A']) % p))
 
-    # check that little_h^response = B * big_h^challenge
-    second_check = (pow(little_h, self.response, p) == ((pow(big_h, self.challenge, p) * self.commitment['B']) % p))
+        # check that little_h^response = B * big_h^challenge
+        second_check = (pow(little_h, self.response, p) == ((pow(big_h, self.challenge, p) * self.commitment['B']) % p))
 
-    # check the challenge?
-    third_check = True
+        # check the challenge?
+        third_check = True
 
-    if challenge_generator:
-      third_check = (self.challenge == challenge_generator(self.commitment))
+        if challenge_generator:
+            third_check = (self.challenge == challenge_generator(self.commitment))
 
-    return (first_check and second_check and third_check)
+        return first_check and second_check and third_check
 
-  toJSONDict = to_dict
 
 class EGZKDisjunctiveProof:
-  def __init__(self, proofs = None):
-    self.proofs = proofs
+    def __init__(self, proofs=None):
+        self.proofs = proofs
 
-  @classmethod
-  def from_dict(cls, d):
-    dp = cls()
-    dp.proofs = [EGZKProof.from_dict(p) for p in d]
-    return dp
+    @classmethod
+    def from_dict(cls, d):
+        dp = cls()
+        dp.proofs = [EGZKProof.from_dict(p) for p in d]
+        return dp
 
-  def to_dict(self):
-    return [p.to_dict() for p in self.proofs]
+    def to_dict(self):
+        return [p.to_dict() for p in self.proofs]
+
+    toJSONDict = to_dict
 
-  toJSONDict = to_dict
 
 class DLogProof(object):
-  def __init__(self, commitment, challenge, response):
-    self.commitment = commitment
-    self.challenge = challenge
-    self.response = response
+    def __init__(self, commitment, challenge, response):
+        self.commitment = commitment
+        self.challenge = challenge
+        self.response = response
 
-  def to_dict(self):
-    return {'challenge': str(self.challenge), 'commitment': str(self.commitment), 'response' : str(self.response)}
+    def to_dict(self):
+        return {'challenge': str(self.challenge), 'commitment': str(self.commitment), 'response': str(self.response)}
 
-  toJSONDict = to_dict
+    toJSONDict = to_dict
 
-  @classmethod
-  def from_dict(cls, d):
-    dlp = cls(int(d['commitment']), int(d['challenge']), int(d['response']))
-    return dlp
+    @classmethod
+    def from_dict(cls, d):
+        dlp = cls(int(d['commitment']), int(d['challenge']), int(d['response']))
+        return dlp
+
+    fromJSONDict = from_dict
 
-  fromJSONDict = from_dict
 
 def EG_disjunctive_challenge_generator(commitments):
-  array_to_hash = []
-  for commitment in commitments:
-    array_to_hash.append(str(commitment['A']))
-    array_to_hash.append(str(commitment['B']))
+    array_to_hash = []
+    for commitment in commitments:
+        array_to_hash.append(str(commitment['A']))
+        array_to_hash.append(str(commitment['B']))
+
+    string_to_hash = ",".join(array_to_hash)
+    return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(), 16)
 
-  string_to_hash = ",".join(array_to_hash)
-  return int(hashlib.sha1(string_to_hash).hexdigest(),16)
 
 # a challenge generator for Fiat-Shamir with A,B commitment
 def EG_fiatshamir_challenge_generator(commitment):
-  return EG_disjunctive_challenge_generator([commitment])
+    return EG_disjunctive_challenge_generator([commitment])
+
 
 def DLog_challenge_generator(commitment):
-  string_to_hash = str(commitment)
-  return int(hashlib.sha1(string_to_hash).hexdigest(),16)
+    string_to_hash = str(commitment)
+    return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(), 16)
diff --git a/helios/crypto/electionalgs.py b/helios/crypto/electionalgs.py
index a17ff6a2ea53d4f655ce32405893d838d86fc504..de833f8dad05dee916c2a0a2e82e9242e1beb146 100644
--- a/helios/crypto/electionalgs.py
+++ b/helios/crypto/electionalgs.py
@@ -4,778 +4,807 @@ Election-specific algorithms for Helios
 Ben Adida
 2008-08-30
 """
-
-import algs
-import logging
-import utils
-import uuid
 import datetime
+import uuid
+import logging
 
-class HeliosObject(object):
-  """
-  A base class to ease serialization and de-serialization
-  crypto objects are kept as full-blown crypto objects, serialized to jsonobjects on the way out
-  and deserialized from jsonobjects on the way in
-  """
-  FIELDS = []
-  JSON_FIELDS = None
-
-  def __init__(self, **kwargs):
-    self.set_from_args(**kwargs)
-
-    # generate uuid if need be
-    if 'uuid' in self.FIELDS and (not hasattr(self, 'uuid') or self.uuid == None):
-      self.uuid = str(uuid.uuid4())
-
-  def set_from_args(self, **kwargs):
-    for f in self.FIELDS:
-      if kwargs.has_key(f):
-        new_val = self.process_value_in(f, kwargs[f])
-        setattr(self, f, new_val)
-      else:
-        setattr(self, f, None)
-
-  def set_from_other_object(self, o):
-    for f in self.FIELDS:
-      if hasattr(o, f):
-        setattr(self, f, self.process_value_in(f, getattr(o,f)))
-      else:
-        setattr(self, f, None)
-
-  def toJSON(self):
-    return utils.to_json(self.toJSONDict())
-
-  def toJSONDict(self, alternate_fields=None):
-    val = {}
-    for f in (alternate_fields or self.JSON_FIELDS or self.FIELDS):
-      val[f] = self.process_value_out(f, getattr(self, f))
-    return val
-
-  @classmethod
-  def fromJSONDict(cls, d):
-    # go through the keys and fix them
-    new_d = {}
-    for k in d.keys():
-      new_d[str(k)] = d[k]
-
-    return cls(**new_d)
-
-  @classmethod
-  def fromOtherObject(cls, o):
-    obj = cls()
-    obj.set_from_other_object(o)
-    return obj
-
-  def toOtherObject(self, o):
-    for f in self.FIELDS:
-      # FIXME: why isn't this working?
-      if hasattr(o, f):
-        # BIG HAMMER
-        try:
-          setattr(o, f, self.process_value_out(f, getattr(self,f)))
-        except:
-          pass
-
-  @property
-  def hash(self):
-    s = utils.to_json(self.toJSONDict())
-    return utils.hash_b64(s)
-
-  def process_value_in(self, field_name, field_value):
-    """
-    process some fields on the way into the object
-    """
-    if field_value == None:
-      return None
-
-    val = self._process_value_in(field_name, field_value)
-    if val != None:
-      return val
-    else:
-      return field_value
+from helios.utils import to_json
+from . import algs
+from . import utils
 
-  def _process_value_in(self, field_name, field_value):
-    return None
 
-  def process_value_out(self, field_name, field_value):
+class HeliosObject(object):
     """
-    process some fields on the way out of the object
+    A base class to ease serialization and de-serialization
+    crypto objects are kept as full-blown crypto objects, serialized to jsonobjects on the way out
+    and deserialized from jsonobjects on the way in
     """
-    if field_value == None:
-      return None
-
-    val = self._process_value_out(field_name, field_value)
-    if val != None:
-      return val
-    else:
-      return field_value
-
-  def _process_value_out(self, field_name, field_value):
-    return None
+    FIELDS = []
+    JSON_FIELDS = None
+
+    def __init__(self, **kwargs):
+        self.set_from_args(**kwargs)
+
+        # generate uuid if need be
+        if 'uuid' in self.FIELDS and (not hasattr(self, 'uuid') or self.uuid is None):
+            self.uuid = str(uuid.uuid4())
+
+    def set_from_args(self, **kwargs):
+        for f in self.FIELDS:
+            if f in kwargs:
+                new_val = self.process_value_in(f, kwargs[f])
+                setattr(self, f, new_val)
+            else:
+                setattr(self, f, None)
+
+    def set_from_other_object(self, o):
+        for f in self.FIELDS:
+            if hasattr(o, f):
+                setattr(self, f, self.process_value_in(f, getattr(o, f)))
+            else:
+                setattr(self, f, None)
+
+    def toJSON(self):
+        return to_json(self.toJSONDict())
+
+    def toJSONDict(self, alternate_fields=None):
+        val = {}
+        for f in (alternate_fields or self.JSON_FIELDS or self.FIELDS):
+            val[f] = self.process_value_out(f, getattr(self, f))
+        return val
+
+    @classmethod
+    def fromJSONDict(cls, d):
+        # go through the keys and fix them
+        new_d = {}
+        for k in list(d.keys()):
+            new_d[str(k)] = d[k]
+
+        return cls(**new_d)
+
+    @classmethod
+    def fromOtherObject(cls, o):
+        obj = cls()
+        obj.set_from_other_object(o)
+        return obj
+
+    def toOtherObject(self, o):
+        for f in self.FIELDS:
+            # FIXME: why isn't this working?
+            if hasattr(o, f):
+                # BIG HAMMER
+                try:
+                    setattr(o, f, self.process_value_out(f, getattr(self, f)))
+                except:
+                    pass
+
+    @property
+    def hash(self):
+        s = to_json(self.toJSONDict())
+        return utils.hash_b64(s)
+
+    def process_value_in(self, field_name, field_value):
+        """
+        process some fields on the way into the object
+        """
+        if field_value is None:
+            return None
+
+        val = self._process_value_in(field_name, field_value)
+        if val is not None:
+            return val
+        else:
+            return field_value
+
+    def _process_value_in(self, field_name, field_value):
+        return None
+
+    def process_value_out(self, field_name, field_value):
+        """
+        process some fields on the way out of the object
+        """
+        if field_value is None:
+            return None
+
+        val = self._process_value_out(field_name, field_value)
+        if val is not None:
+            return val
+        else:
+            return field_value
+
+    def _process_value_out(self, field_name, field_value):
+        return None
+
+    def __eq__(self, other):
+        if not hasattr(self, 'uuid'):
+            return super(HeliosObject, self) == other
+
+        return other is not None and self.uuid == other.uuid
 
-  def __eq__(self, other):
-    if not hasattr(self, 'uuid'):
-      return super(HeliosObject,self) == other
-
-    return other != None and self.uuid == other.uuid
 
 class EncryptedAnswer(HeliosObject):
-  """
-  An encrypted answer to a single election question
-  """
-
-  FIELDS = ['choices', 'individual_proofs', 'overall_proof', 'randomness', 'answer']
-
-  # FIXME: remove this constructor and use only named-var constructor from HeliosObject
-  def __init__(self, choices=None, individual_proofs=None, overall_proof=None, randomness=None, answer=None):
-    self.choices = choices
-    self.individual_proofs = individual_proofs
-    self.overall_proof = overall_proof
-    self.randomness = randomness
-    self.answer = answer
-
-  @classmethod
-  def generate_plaintexts(cls, pk, min=0, max=1):
-    plaintexts = []
-    running_product = 1
-
-    # run the product up to the min
-    for i in range(max+1):
-      # if we're in the range, add it to the array
-      if i >= min:
-        plaintexts.append(algs.EGPlaintext(running_product, pk))
-
-      # next value in running product
-      running_product = (running_product * pk.g) % pk.p
-
-    return plaintexts
-
-  def verify_plaintexts_and_randomness(self, pk):
     """
-    this applies only if the explicit answers and randomness factors are given
-    we do not verify the proofs here, that is the verify() method
+    An encrypted answer to a single election question
     """
-    if not hasattr(self, 'answer'):
-      return False
-
-    for choice_num in range(len(self.choices)):
-      choice = self.choices[choice_num]
-      choice.pk = pk
 
-      # redo the encryption
-      # WORK HERE (paste from below encryption)
+    FIELDS = ['choices', 'individual_proofs', 'overall_proof', 'randomness', 'answer']
 
-    return False
+    # FIXME: remove this constructor and use only named-var constructor from HeliosObject
+    def __init__(self, choices=None, individual_proofs=None, overall_proof=None, randomness=None, answer=None):
+        self.choices = choices
+        self.individual_proofs = individual_proofs
+        self.overall_proof = overall_proof
+        self.randomness = randomness
+        self.answer = answer
 
-  def verify(self, pk, min=0, max=1):
-    possible_plaintexts = self.generate_plaintexts(pk)
-    homomorphic_sum = 0
+    @classmethod
+    def generate_plaintexts(cls, pk, min=0, max=1):
+        plaintexts = []
+        running_product = 1
 
-    for choice_num in range(len(self.choices)):
-      choice = self.choices[choice_num]
-      choice.pk = pk
-      individual_proof = self.individual_proofs[choice_num]
+        # run the product up to the min
+        for i in range(max + 1):
+            # if we're in the range, add it to the array
+            if i >= min:
+                plaintexts.append(algs.EGPlaintext(running_product, pk))
 
-      # verify that elements belong to the proper group
-      if not choice.check_group_membership(pk):
-        return False
-
-      # verify the proof on the encryption of that choice
-      if not choice.verify_disjunctive_encryption_proof(possible_plaintexts, individual_proof, algs.EG_disjunctive_challenge_generator):
-        return False
+            # next value in running product
+            running_product = (running_product * pk.g) % pk.p
 
-      # compute homomorphic sum if needed
-      if max != None:
-        homomorphic_sum = choice * homomorphic_sum
+        return plaintexts
 
-    if max != None:
-      # determine possible plaintexts for the sum
-      sum_possible_plaintexts = self.generate_plaintexts(pk, min=min, max=max)
+    def verify_plaintexts_and_randomness(self, pk):
+        """
+        this applies only if the explicit answers and randomness factors are given
+        we do not verify the proofs here, that is the verify() method
+        """
+        if not hasattr(self, 'answer'):
+            return False
 
-      # verify the sum
-      return homomorphic_sum.verify_disjunctive_encryption_proof(sum_possible_plaintexts, self.overall_proof, algs.EG_disjunctive_challenge_generator)
-    else:
-      # approval voting, no need for overall proof verification
-      return True
+        for choice_num in range(len(self.choices)):
+            choice = self.choices[choice_num]
+            choice.pk = pk
 
-  def toJSONDict(self, with_randomness=False):
-    value = {
-      'choices': [c.to_dict() for c in self.choices],
-      'individual_proofs' : [p.to_dict() for p in self.individual_proofs]
-    }
+            # redo the encryption
+            # WORK HERE (paste from below encryption)
 
-    if self.overall_proof:
-      value['overall_proof'] = self.overall_proof.to_dict()
-    else:
-      value['overall_proof'] = None
-
-    if with_randomness:
-      value['randomness'] = [str(r) for r in self.randomness]
-      value['answer'] = self.answer
-
-    return value
-
-  @classmethod
-  def fromJSONDict(cls, d, pk=None):
-    ea = cls()
-
-    ea.choices = [algs.EGCiphertext.from_dict(c, pk) for c in d['choices']]
-    ea.individual_proofs = [algs.EGZKDisjunctiveProof.from_dict(p) for p in d['individual_proofs']]
-
-    if d['overall_proof']:
-      ea.overall_proof = algs.EGZKDisjunctiveProof.from_dict(d['overall_proof'])
-    else:
-      ea.overall_proof = None
+        return False
 
-    if d.has_key('randomness'):
-      ea.randomness = [int(r) for r in d['randomness']]
-      ea.answer = d['answer']
+    def verify(self, pk, min=0, max=1):
+        possible_plaintexts = self.generate_plaintexts(pk)
+        homomorphic_sum = 0
+
+        for choice_num in range(len(self.choices)):
+            choice = self.choices[choice_num]
+            choice.pk = pk
+            individual_proof = self.individual_proofs[choice_num]
+
+            # verify that elements belong to the proper group
+            if not choice.check_group_membership(pk):
+                return False
+
+            # verify the proof on the encryption of that choice
+            if not choice.verify_disjunctive_encryption_proof(possible_plaintexts, individual_proof,
+                                                              algs.EG_disjunctive_challenge_generator):
+                return False
+
+            # compute homomorphic sum if needed
+            if max is not None:
+                homomorphic_sum = choice * homomorphic_sum
+
+        if max is not None:
+            # determine possible plaintexts for the sum
+            sum_possible_plaintexts = self.generate_plaintexts(pk, min=min, max=max)
+
+            # verify the sum
+            return homomorphic_sum.verify_disjunctive_encryption_proof(sum_possible_plaintexts, self.overall_proof,
+                                                                       algs.EG_disjunctive_challenge_generator)
+        else:
+            # approval voting, no need for overall proof verification
+            return True
+
+    def toJSONDict(self, with_randomness=False):
+        value = {
+            'choices': [c.to_dict() for c in self.choices],
+            'individual_proofs': [p.to_dict() for p in self.individual_proofs]
+        }
+
+        if self.overall_proof:
+            value['overall_proof'] = self.overall_proof.to_dict()
+        else:
+            value['overall_proof'] = None
+
+        if with_randomness:
+            value['randomness'] = [str(r) for r in self.randomness]
+            value['answer'] = self.answer
+
+        return value
+
+    @classmethod
+    def fromJSONDict(cls, d, pk=None):
+        ea = cls()
+
+        ea.choices = [algs.EGCiphertext.from_dict(c, pk) for c in d['choices']]
+        ea.individual_proofs = [algs.EGZKDisjunctiveProof.from_dict(p) for p in d['individual_proofs']]
+
+        if d['overall_proof']:
+            ea.overall_proof = algs.EGZKDisjunctiveProof.from_dict(d['overall_proof'])
+        else:
+            ea.overall_proof = None
+
+        if 'randomness' in d:
+            ea.randomness = [int(r) for r in d['randomness']]
+            ea.answer = d['answer']
+
+        return ea
+
+    @classmethod
+    def fromElectionAndAnswer(cls, election, question_num, answer_indexes):
+        """
+        Given an election, a question number, and a list of answers to that question
+        in the form of an array of 0-based indexes into the answer array,
+        produce an EncryptedAnswer that works.
+        """
+        question = election.questions[question_num]
+        answers = question['answers']
+        pk = election.public_key
+
+        # initialize choices, individual proofs, randomness and overall proof
+        choices = [None for _ in range(len(answers))]
+        individual_proofs = [None for _ in range(len(answers))]
+        randomness = [None for _ in range(len(answers))]
+
+        # possible plaintexts [0, 1]
+        plaintexts = cls.generate_plaintexts(pk)
+
+        # keep track of number of options selected.
+        num_selected_answers = 0
+
+        # homomorphic sum of all
+        homomorphic_sum = 0
+        randomness_sum = 0
+
+        # min and max for number of answers, useful later
+        min_answers = 0
+        if 'min' in question:
+            min_answers = question['min']
+        max_answers = question['max']
+
+        # go through each possible answer and encrypt either a g^0 or a g^1.
+        for answer_num in range(len(answers)):
+            plaintext_index = 0
+
+            # assuming a list of answers
+            if answer_num in answer_indexes:
+                plaintext_index = 1
+                num_selected_answers += 1
+
+            # randomness and encryption
+            randomness[answer_num] = utils.random.mpz_lt(pk.q)
+            choices[answer_num] = pk.encrypt_with_r(plaintexts[plaintext_index], randomness[answer_num])
+
+            # generate proof
+            individual_proofs[answer_num] = choices[answer_num].generate_disjunctive_encryption_proof(plaintexts,
+                                                                                                      plaintext_index,
+                                                                                                      randomness[
+                                                                                                          answer_num],
+                                                                                                      algs.EG_disjunctive_challenge_generator)
+
+            # sum things up homomorphically if needed
+            if max_answers is not None:
+                homomorphic_sum = choices[answer_num] * homomorphic_sum
+                randomness_sum = (randomness_sum + randomness[answer_num]) % pk.q
+
+        # prove that the sum is 0 or 1 (can be "blank vote" for this answer)
+        # num_selected_answers is 0 or 1, which is the index into the plaintext that is actually encoded
+
+        if num_selected_answers < min_answers:
+            raise Exception("Need to select at least %s answer(s)" % min_answers)
+
+        if max_answers is not None:
+            sum_plaintexts = cls.generate_plaintexts(pk, min=min_answers, max=max_answers)
+
+            # need to subtract the min from the offset
+            overall_proof = homomorphic_sum.generate_disjunctive_encryption_proof(sum_plaintexts,
+                                                                                  num_selected_answers - min_answers,
+                                                                                  randomness_sum,
+                                                                                  algs.EG_disjunctive_challenge_generator);
+        else:
+            # approval voting
+            overall_proof = None
+
+        return cls(choices, individual_proofs, overall_proof, randomness, answer_indexes)
 
-    return ea
 
-  @classmethod
-  def fromElectionAndAnswer(cls, election, question_num, answer_indexes):
+class EncryptedVote(HeliosObject):
     """
-    Given an election, a question number, and a list of answers to that question
-    in the form of an array of 0-based indexes into the answer array,
-    produce an EncryptedAnswer that works.
+    An encrypted ballot
     """
-    question = election.questions[question_num]
-    answers = question['answers']
-    pk = election.public_key
-
-    # initialize choices, individual proofs, randomness and overall proof
-    choices = [None for a in range(len(answers))]
-    individual_proofs = [None for a in range(len(answers))]
-    overall_proof = None
-    randomness = [None for a in range(len(answers))]
-
-    # possible plaintexts [0, 1]
-    plaintexts = cls.generate_plaintexts(pk)
-
-    # keep track of number of options selected.
-    num_selected_answers = 0;
-
-    # homomorphic sum of all
-    homomorphic_sum = 0
-    randomness_sum = 0
-
-    # min and max for number of answers, useful later
-    min_answers = 0
-    if question.has_key('min'):
-      min_answers = question['min']
-    max_answers = question['max']
+    FIELDS = ['encrypted_answers', 'election_hash', 'election_uuid']
+
+    def verify(self, election):
+        # correct number of answers
+        # noinspection PyUnresolvedReferences
+        n_answers = len(self.encrypted_answers) if self.encrypted_answers is not None else 0
+        n_questions = len(election.questions) if election.questions is not None else 0
+        if n_answers != n_questions:
+            logging.error(f"Incorrect number of answers ({n_answers}) vs questions ({n_questions})")
+            return False
+
+        # check hash
+        # noinspection PyUnresolvedReferences
+        our_election_hash = self.election_hash if isinstance(self.election_hash, str) else self.election_hash.decode()
+        actual_election_hash = election.hash if isinstance(election.hash, str) else election.hash.decode()
+        if our_election_hash != actual_election_hash:
+            logging.error(f"Incorrect election_hash {our_election_hash} vs {actual_election_hash} ")
+            return False
+
+        # check ID
+        # noinspection PyUnresolvedReferences
+        our_election_uuid = self.election_uuid if isinstance(self.election_uuid, str) else self.election_uuid.decode()
+        actual_election_uuid = election.uuid if isinstance(election.uuid, str) else election.uuid.decode()
+        if our_election_uuid != actual_election_uuid:
+            logging.error(f"Incorrect election_uuid {our_election_uuid} vs {actual_election_uuid} ")
+            return False
+
+        # check proofs on all of answers
+        for question_num in range(len(election.questions)):
+            ea = self.encrypted_answers[question_num]
+
+            question = election.questions[question_num]
+            min_answers = 0
+            if 'min' in question:
+                min_answers = question['min']
+
+            if not ea.verify(election.public_key, min=min_answers, max=question['max']):
+                return False
+
+        return True
+
+    def get_hash(self):
+        return utils.hash_b64(to_json(self.toJSONDict()))
+
+    def toJSONDict(self, with_randomness=False):
+        return {
+            'answers': [a.toJSONDict(with_randomness) for a in self.encrypted_answers],
+            'election_hash': self.election_hash,
+            'election_uuid': self.election_uuid
+        }
+
+    @classmethod
+    def fromJSONDict(cls, d, pk=None):
+        ev = cls()
+
+        ev.encrypted_answers = [EncryptedAnswer.fromJSONDict(ea, pk) for ea in d['answers']]
+        ev.election_hash = d['election_hash']
+        ev.election_uuid = d['election_uuid']
+
+        return ev
+
+    @classmethod
+    def fromElectionAndAnswers(cls, election, answers):
+        pk = election.public_key
+
+        # each answer is an index into the answer array
+        encrypted_answers = [EncryptedAnswer.fromElectionAndAnswer(election, answer_num, answers[answer_num]) for
+                             answer_num in range(len(answers))]
+        return cls(encrypted_answers=encrypted_answers, election_hash=election.hash, election_uuid=election.uuid)
 
-    # go through each possible answer and encrypt either a g^0 or a g^1.
-    for answer_num in range(len(answers)):
-      plaintext_index = 0
 
-      # assuming a list of answers
-      if answer_num in answer_indexes:
-        plaintext_index = 1
-        num_selected_answers += 1
-
-      # randomness and encryption
-      randomness[answer_num] = algs.Utils.random_mpz_lt(pk.q)
-      choices[answer_num] = pk.encrypt_with_r(plaintexts[plaintext_index], randomness[answer_num])
-
-      # generate proof
-      individual_proofs[answer_num] = choices[answer_num].generate_disjunctive_encryption_proof(plaintexts, plaintext_index,
-                                                randomness[answer_num], algs.EG_disjunctive_challenge_generator)
-
-      # sum things up homomorphically if needed
-      if max_answers != None:
-        homomorphic_sum = choices[answer_num] * homomorphic_sum
-        randomness_sum = (randomness_sum + randomness[answer_num]) % pk.q
-
-    # prove that the sum is 0 or 1 (can be "blank vote" for this answer)
-    # num_selected_answers is 0 or 1, which is the index into the plaintext that is actually encoded
-
-    if num_selected_answers < min_answers:
-      raise Exception("Need to select at least %s answer(s)" % min_answers)
-
-    if max_answers != None:
-      sum_plaintexts = cls.generate_plaintexts(pk, min=min_answers, max=max_answers)
+def one_question_winner(question, result, num_cast_votes):
+    """
+    determining the winner for one question
+    """
+    # sort the answers , keep track of the index
+    counts = sorted(enumerate(result), key=lambda x: x[1])
+    counts.reverse()
 
-      # need to subtract the min from the offset
-      overall_proof = homomorphic_sum.generate_disjunctive_encryption_proof(sum_plaintexts, num_selected_answers - min_answers, randomness_sum, algs.EG_disjunctive_challenge_generator);
-    else:
-      # approval voting
-      overall_proof = None
+    # if there's a max > 1, we assume that the top MAX win
+    if question['max'] > 1:
+        return [c[0] for c in counts[:question['max']]]
 
-    return cls(choices, individual_proofs, overall_proof, randomness, answer_indexes)
+    # if max = 1, then depends on absolute or relative
+    if question['result_type'] == 'absolute':
+        if counts[0][1] >= (num_cast_votes / 2 + 1):
+            return [counts[0][0]]
+        else:
+            return []
 
-class EncryptedVote(HeliosObject):
-  """
-  An encrypted ballot
-  """
-  FIELDS = ['encrypted_answers', 'election_hash', 'election_uuid']
-
-  def verify(self, election):
-    # right number of answers
-    if len(self.encrypted_answers) != len(election.questions):
-      return False
-
-    # check hash
-    if self.election_hash != election.hash:
-      # print "%s / %s " % (self.election_hash, election.hash)
-      return False
-
-    # check ID
-    if self.election_uuid != election.uuid:
-      return False
-
-    # check proofs on all of answers
-    for question_num in range(len(election.questions)):
-      ea = self.encrypted_answers[question_num]
-
-      question = election.questions[question_num]
-      min_answers = 0
-      if question.has_key('min'):
-        min_answers = question['min']
-
-      if not ea.verify(election.public_key, min=min_answers, max=question['max']):
-        return False
+    if question['result_type'] == 'relative':
+        return [counts[0][0]]
 
-    return True
-
-  def get_hash(self):
-    return utils.hash_b64(utils.to_json(self.toJSONDict(), no_whitespace=True))
-
-  def toJSONDict(self, with_randomness=False):
-    return {
-      'answers': [a.toJSONDict(with_randomness) for a in self.encrypted_answers],
-      'election_hash': self.election_hash,
-      'election_uuid': self.election_uuid
-    }
+    
+class Election(HeliosObject):
+    FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg',
+              'frozen_at', 'public_key', 'private_key', 'cast_url', 'result', 'result_proof', 'use_voter_aliases',
+              'voting_starts_at', 'voting_ends_at', 'election_type']
 
-  @classmethod
-  def fromJSONDict(cls, d, pk=None):
-    ev = cls()
+    JSON_FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg',
+                   'frozen_at', 'public_key', 'cast_url', 'use_voter_aliases', 'voting_starts_at', 'voting_ends_at']
 
-    ev.encrypted_answers = [EncryptedAnswer.fromJSONDict(ea, pk) for ea in d['answers']]
-    ev.election_hash = d['election_hash']
-    ev.election_uuid = d['election_uuid']
+    # need to add in v3.1: use_advanced_audit_features, election_type, and probably more
 
-    return ev
+    def init_tally(self):
+        return Tally(election=self)
 
-  @classmethod
-  def fromElectionAndAnswers(cls, election, answers):
-    pk = election.public_key
+    def _process_value_in(self, field_name, field_value):
+        if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at':
+            if isinstance(field_value, str):
+                return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S')
 
-    # each answer is an index into the answer array
-    encrypted_answers = [EncryptedAnswer.fromElectionAndAnswer(election, answer_num, answers[answer_num]) for answer_num in range(len(answers))]
-    return cls(encrypted_answers=encrypted_answers, election_hash=election.hash, election_uuid = election.uuid)
+        if field_name == 'public_key':
+            return algs.EGPublicKey.fromJSONDict(field_value)
 
+        if field_name == 'private_key':
+            return algs.EGSecretKey.fromJSONDict(field_value)
 
-def one_question_winner(question, result, num_cast_votes):
-  """
-  determining the winner for one question
-  """
-  # sort the answers , keep track of the index
-  counts = sorted(enumerate(result), key=lambda(x): x[1])
-  counts.reverse()
-
-  # if there's a max > 1, we assume that the top MAX win
-  if question['max'] > 1:
-    return [c[0] for c in counts[:question['max']]]
-
-  # if max = 1, then depends on absolute or relative
-  if question['result_type'] == 'absolute':
-    if counts[0][1] >=  (num_cast_votes/2 + 1):
-      return [counts[0][0]]
-    else:
-      return []
-
-  if question['result_type'] == 'relative':
-    return [counts[0][0]]
+    def _process_value_out(self, field_name, field_value):
+        # the date
+        if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at':
+            return str(field_value)
 
-class Election(HeliosObject):
+        if field_name == 'public_key' or field_name == 'private_key':
+            return field_value.toJSONDict()
 
-  FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg',
-      'frozen_at', 'public_key', 'private_key', 'cast_url', 'result', 'result_proof', 'use_voter_aliases', 'voting_starts_at', 'voting_ends_at', 'election_type']
+    @property
+    def registration_status_pretty(self):
+        if self.openreg:
+            return "Open"
+        else:
+            return "Closed"
 
-  JSON_FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg',
-      'frozen_at', 'public_key', 'cast_url', 'use_voter_aliases', 'voting_starts_at', 'voting_ends_at']
+    @property
+    def winners(self):
+        """
+        Depending on the type of each question, determine the winners
+        returns an array of winners for each question, aka an array of arrays.
+        assumes that if there is a max to the question, that's how many winners there are.
+        """
+        return [one_question_winner(self.questions[i], self.result[i], self.num_cast_votes) for i in
+                range(len(self.questions))]
 
-  # need to add in v3.1: use_advanced_audit_features, election_type, and probably more
+    @property
+    def pretty_result(self):
+        if not self.result:
+            return None
 
-  def init_tally(self):
-    return Tally(election=self)
+        # get the winners
+        winners = self.winners
 
-  def _process_value_in(self, field_name, field_value):
-    if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at':
-      if type(field_value) == str or type(field_value) == unicode:
-        return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S')
+        raw_result = self.result
+        prettified_result = []
 
-    if field_name == 'public_key':
-      return algs.EGPublicKey.fromJSONDict(field_value)
+        # loop through questions
+        for i in range(len(self.questions)):
+            q = self.questions[i]
+            pretty_question = []
 
-    if field_name == 'private_key':
-      return algs.EGSecretKey.fromJSONDict(field_value)
+            # go through answers
+            for j in range(len(q['answers'])):
+                a = q['answers'][j]
+                count = raw_result[i][j]
+                pretty_question.append({'answer': a, 'count': count, 'winner': (j in winners[i])})
 
-  def _process_value_out(self, field_name, field_value):
-    # the date
-    if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at':
-      return str(field_value)
+            prettified_result.append({'question': q['short_name'], 'answers': pretty_question})
 
-    if field_name == 'public_key' or field_name == 'private_key':
-      return field_value.toJSONDict()
+        return prettified_result
 
-  @property
-  def registration_status_pretty(self):
-    if self.openreg:
-      return "Open"
-    else:
-      return "Closed"
 
-  @property
-  def winners(self):
+class Voter(HeliosObject):
     """
-    Depending on the type of each question, determine the winners
-    returns an array of winners for each question, aka an array of arrays.
-    assumes that if there is a max to the question, that's how many winners there are.
+    A voter in an election
     """
-    return [one_question_winner(self.questions[i], self.result[i], self.num_cast_votes) for i in range(len(self.questions))]
+    FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id', 'name', 'alias']
+    JSON_FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id_hash', 'name']
 
-  @property
-  def pretty_result(self):
-    if not self.result:
-      return None
+    # alternative, for when the voter is aliased
+    ALIASED_VOTER_JSON_FIELDS = ['election_uuid', 'uuid', 'alias']
 
-    # get the winners
-    winners = self.winners
+    def toJSONDict(self):
+        if self.alias is not None:
+            return super(Voter, self).toJSONDict(self.ALIASED_VOTER_JSON_FIELDS)
+        else:
+            return super(Voter, self).toJSONDict()
 
-    raw_result = self.result
-    prettified_result = []
+    @property
+    def voter_id_hash(self):
+        if self.voter_login_id:
+            # for backwards compatibility with v3.0, and since it doesn't matter
+            # too much if we hash the email or the unique login ID here.
+            return utils.hash_b64(self.voter_login_id)
+        else:
+            return utils.hash_b64(self.voter_id)
 
-    # loop through questions
-    for i in range(len(self.questions)):
-      q = self.questions[i]
-      pretty_question = []
-
-      # go through answers
-      for j in range(len(q['answers'])):
-        a = q['answers'][j]
-        count = raw_result[i][j]
-        pretty_question.append({'answer': a, 'count': count, 'winner': (j in winners[i])})
-
-      prettified_result.append({'question': q['short_name'], 'answers': pretty_question})
-
-    return prettified_result
-
-
-class Voter(HeliosObject):
-  """
-  A voter in an election
-  """
-  FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id', 'name', 'alias']
-  JSON_FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id_hash', 'name']
-
-  # alternative, for when the voter is aliased
-  ALIASED_VOTER_JSON_FIELDS = ['election_uuid', 'uuid', 'alias']
-
-  def toJSONDict(self):
-    fields = None
-    if self.alias != None:
-      return super(Voter, self).toJSONDict(self.ALIASED_VOTER_JSON_FIELDS)
-    else:
-      return super(Voter,self).toJSONDict()
-
-  @property
-  def voter_id_hash(self):
-    if self.voter_login_id:
-      # for backwards compatibility with v3.0, and since it doesn't matter
-      # too much if we hash the email or the unique login ID here.
-      return utils.hash_b64(self.voter_login_id)
-    else:
-      return utils.hash_b64(self.voter_id)
 
 class Trustee(HeliosObject):
-  """
-  a trustee
-  """
-  FIELDS = ['uuid', 'public_key', 'public_key_hash', 'pok', 'decryption_factors', 'decryption_proofs', 'email']
-
-  def _process_value_in(self, field_name, field_value):
-    if field_name == 'public_key':
-      return algs.EGPublicKey.fromJSONDict(field_value)
-
-    if field_name == 'pok':
-      return algs.DLogProof.fromJSONDict(field_value)
-
-  def _process_value_out(self, field_name, field_value):
-    if field_name == 'public_key' or field_name == 'pok':
-      return field_value.toJSONDict()
-
-class CastVote(HeliosObject):
-  """
-  A cast vote, which includes an encrypted vote and some cast metadata
-  """
-  FIELDS = ['vote', 'cast_at', 'voter_uuid', 'voter_hash', 'vote_hash']
-
-  def __init__(self, *args, **kwargs):
-    super(CastVote, self).__init__(*args, **kwargs)
-    self.election = None
-
-  @classmethod
-  def fromJSONDict(cls, d, election=None):
-    o = cls()
-    o.election = election
-    o.set_from_args(**d)
-    return o
-
-  def toJSONDict(self, include_vote=True):
-    result = super(CastVote,self).toJSONDict()
-    if not include_vote:
-      del result['vote']
-    return result
-
-  @classmethod
-  def fromOtherObject(cls, o, election):
-    obj = cls()
-    obj.election = election
-    obj.set_from_other_object(o)
-    return obj
-
-  def _process_value_in(self, field_name, field_value):
-    if field_name == 'cast_at':
-      if type(field_value) == str:
-        return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S')
-
-    if field_name == 'vote':
-      return EncryptedVote.fromJSONDict(field_value, self.election.public_key)
-
-  def _process_value_out(self, field_name, field_value):
-    # the date
-    if field_name == 'cast_at':
-      return str(field_value)
-
-    if field_name == 'vote':
-      return field_value.toJSONDict()
-
-  def issues(self, election):
     """
-    Look for consistency problems
+    a trustee
     """
-    issues = []
-
-    # check the election
-    if self.vote.election_uuid != election.uuid:
-      issues.append("the vote's election UUID does not match the election for which this vote is being cast")
-
-    return issues
-
-class DLogTable(object):
-  """
-  Keeping track of discrete logs
-  """
-
-  def __init__(self, base, modulus):
-    self.dlogs = {}
-    self.dlogs[1] = 0
-    self.last_dlog_result = 1
-    self.counter = 0
-
-    self.base = base
-    self.modulus = modulus
+    FIELDS = ['uuid', 'public_key', 'public_key_hash', 'pok', 'decryption_factors', 'decryption_proofs', 'email']
 
-  def increment(self):
-    self.counter += 1
+    def _process_value_in(self, field_name, field_value):
+        if field_name == 'public_key':
+            return algs.EGPublicKey.fromJSONDict(field_value)
 
-    # new value
-    new_value = (self.last_dlog_result * self.base) % self.modulus
+        if field_name == 'pok':
+            return algs.DLogProof.fromJSONDict(field_value)
 
-    # record the discrete log
-    self.dlogs[new_value] = self.counter
+    def _process_value_out(self, field_name, field_value):
+        if field_name == 'public_key' or field_name == 'pok':
+            return field_value.toJSONDict()
 
-    # record the last value
-    self.last_dlog_result = new_value
-
-  def precompute(self, up_to):
-    while self.counter < up_to:
-      self.increment()
-
-  def lookup(self, value):
-    return self.dlogs.get(value, None)
 
+class CastVote(HeliosObject):
+    """
+    A cast vote, which includes an encrypted vote and some cast metadata
+    """
+    FIELDS = ['vote', 'cast_at', 'voter_uuid', 'voter_hash', 'vote_hash']
 
-class Tally(HeliosObject):
-  """
-  A running homomorphic tally
-  """
+    def __init__(self, *args, **kwargs):
+        super(CastVote, self).__init__(*args, **kwargs)
+        self.election = None
 
-  FIELDS = ['num_tallied', 'tally']
-  JSON_FIELDS = ['num_tallied', 'tally']
+    @classmethod
+    def fromJSONDict(cls, d, election=None):
+        o = cls()
+        o.election = election
+        o.set_from_args(**d)
+        return o
 
-  def __init__(self, *args, **kwargs):
-    super(Tally, self).__init__(*args, **kwargs)
+    def toJSONDict(self, include_vote=True):
+        result = super(CastVote, self).toJSONDict()
+        if not include_vote:
+            del result['vote']
+        return result
 
-    self.election = kwargs.get('election',None)
+    @classmethod
+    def fromOtherObject(cls, o, election):
+        obj = cls()
+        obj.election = election
+        obj.set_from_other_object(o)
+        return obj
 
-    if self.election:
-      self.init_election(self.election)
-    else:
-      self.questions = None
-      self.public_key = None
+    def _process_value_in(self, field_name, field_value):
+        if field_name == 'cast_at':
+            if isinstance(field_value, str):
+                return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S')
 
-      if not self.tally:
-        self.tally = None
+        if field_name == 'vote':
+            return EncryptedVote.fromJSONDict(field_value, self.election.public_key)
 
-    # initialize
-    if self.num_tallied == None:
-      self.num_tallied = 0
+    def _process_value_out(self, field_name, field_value):
+        # the date
+        if field_name == 'cast_at':
+            return str(field_value)
 
-  def init_election(self, election):
-    """
-    given the election, initialize some params
-    """
-    self.questions = election.questions
-    self.public_key = election.public_key
+        if field_name == 'vote':
+            return field_value.toJSONDict()
 
-    if not self.tally:
-      self.tally = [[0 for a in q['answers']] for q in self.questions]
+    def issues(self, election):
+        """
+        Look for consistency problems
+        """
+        issues = []
 
-  def add_vote_batch(self, encrypted_votes, verify_p=True):
-    """
-    Add a batch of votes. Eventually, this will be optimized to do an aggregate proof verification
-    rather than a whole proof verif for each vote.
-    """
-    for vote in encrypted_votes:
-      self.add_vote(vote, verify_p)
-
-  def add_vote(self, encrypted_vote, verify_p=True):
-    # do we verify?
-    if verify_p:
-      if not encrypted_vote.verify(self.election):
-        raise Exception('Bad Vote')
-
-    # for each question
-    for question_num in range(len(self.questions)):
-      question = self.questions[question_num]
-      answers = question['answers']
-
-      # for each possible answer to each question
-      for answer_num in range(len(answers)):
-        # do the homomorphic addition into the tally
-        enc_vote_choice = encrypted_vote.encrypted_answers[question_num].choices[answer_num]
-        enc_vote_choice.pk = self.public_key
-        self.tally[question_num][answer_num] = encrypted_vote.encrypted_answers[question_num].choices[answer_num] * self.tally[question_num][answer_num]
-
-    self.num_tallied += 1
-
-  def decryption_factors_and_proofs(self, sk):
-    """
-    returns an array of decryption factors and a corresponding array of decryption proofs.
-    makes the decryption factors into strings, for general Helios / JS compatibility.
-    """
-    # for all choices of all questions (double list comprehension)
-    decryption_factors = []
-    decryption_proof = []
+        # check the election
+        if self.vote.election_uuid != election.uuid:
+            issues.append("the vote's election UUID does not match the election for which this vote is being cast")
 
-    for question_num, question in enumerate(self.questions):
-      answers = question['answers']
-      question_factors = []
-      question_proof = []
+        return issues
 
-      for answer_num, answer in enumerate(answers):
-        # do decryption and proof of it
-        dec_factor, proof = sk.decryption_factor_and_proof(self.tally[question_num][answer_num])
 
-        # look up appropriate discrete log
-        # this is the string conversion
-        question_factors.append(str(dec_factor))
-        question_proof.append(proof.toJSONDict())
-
-      decryption_factors.append(question_factors)
-      decryption_proof.append(question_proof)
-
-    return decryption_factors, decryption_proof
-
-  def decrypt_and_prove(self, sk, discrete_logs=None):
+class DLogTable(object):
     """
-    returns an array of tallies and a corresponding array of decryption proofs.
+    Keeping track of discrete logs
     """
 
-    # who's keeping track of discrete logs?
-    if not discrete_logs:
-      discrete_logs = self.discrete_logs
+    def __init__(self, base, modulus):
+        self.dlogs = {1: 0}
+        self.last_dlog_result = 1
+        self.counter = 0
 
-    # for all choices of all questions (double list comprehension)
-    decrypted_tally = []
-    decryption_proof = []
+        self.base = base
+        self.modulus = modulus
 
-    for question_num in range(len(self.questions)):
-      question = self.questions[question_num]
-      answers = question['answers']
-      question_tally = []
-      question_proof = []
+    def increment(self):
+        self.counter += 1
 
-      for answer_num in range(len(answers)):
-        # do decryption and proof of it
-        plaintext, proof = sk.prove_decryption(self.tally[question_num][answer_num])
+        # new value
+        new_value = (self.last_dlog_result * self.base) % self.modulus
 
-        # look up appropriate discrete log
-        question_tally.append(discrete_logs[plaintext])
-        question_proof.append(proof)
+        # record the discrete log
+        self.dlogs[new_value] = self.counter
 
-      decrypted_tally.append(question_tally)
-      decryption_proof.append(question_proof)
+        # record the last value
+        self.last_dlog_result = new_value
 
-    return decrypted_tally, decryption_proof
+    def precompute(self, up_to):
+        while self.counter < up_to:
+            self.increment()
 
-  def verify_decryption_proofs(self, decryption_factors, decryption_proofs, public_key, challenge_generator):
-    """
-    decryption_factors is a list of lists of dec factors
-    decryption_proofs are the corresponding proofs
-    public_key is, of course, the public key of the trustee
-    """
+    def lookup(self, value):
+        return self.dlogs.get(value, None)
 
-    # go through each one
-    for q_num, q in enumerate(self.tally):
-      for a_num, answer_tally in enumerate(q):
-        # parse the proof
-        proof = algs.EGZKProof.fromJSONDict(decryption_proofs[q_num][a_num])
 
-        # check that g, alpha, y, dec_factor is a DH tuple
-        if not proof.verify(public_key.g, answer_tally.alpha, public_key.y, int(decryption_factors[q_num][a_num]), public_key.p, public_key.q, challenge_generator):
-          return False
-
-    return True
-
-  def decrypt_from_factors(self, decryption_factors, public_key):
+class Tally(HeliosObject):
     """
-    decrypt a tally given decryption factors
-
-    The decryption factors are a list of decryption factor sets, for each trustee.
-    Each decryption factor set is a list of lists of decryption factors (questions/answers).
+    A running homomorphic tally
     """
 
-    # pre-compute a dlog table
-    dlog_table = DLogTable(base = public_key.g, modulus = public_key.p)
-    dlog_table.precompute(self.num_tallied)
-
-    result = []
+    FIELDS = ['num_tallied', 'tally']
+    JSON_FIELDS = ['num_tallied', 'tally']
+
+    def __init__(self, *args, **kwargs):
+        super(Tally, self).__init__(*args, **kwargs)
+
+        self.election = kwargs.get('election', None)
+
+        if self.election:
+            self.init_election(self.election)
+        else:
+            self.questions = None
+            self.public_key = None
+
+            if not self.tally:
+                self.tally = None
+
+        # initialize
+        if self.num_tallied is None:
+            self.num_tallied = 0
+
+    def init_election(self, election):
+        """
+        given the election, initialize some params
+        """
+        self.questions = election.questions
+        self.public_key = election.public_key
+
+        if not self.tally:
+            self.tally = [[0 for _ in q['answers']] for q in self.questions]
+
+    def add_vote_batch(self, encrypted_votes, verify_p=True):
+        """
+        Add a batch of votes. Eventually, this will be optimized to do an aggregate proof verification
+        rather than a whole proof verif for each vote.
+        """
+        for vote in encrypted_votes:
+            self.add_vote(vote, verify_p)
+
+    def add_vote(self, encrypted_vote, verify_p=True):
+        # do we verify?
+        if verify_p:
+            if not encrypted_vote.verify(self.election):
+                raise Exception('Bad Vote')
+
+        # for each question
+        for question_num in range(len(self.questions)):
+            question = self.questions[question_num]
+            answers = question['answers']
+
+            # for each possible answer to each question
+            for answer_num in range(len(answers)):
+                # do the homomorphic addition into the tally
+                enc_vote_choice = encrypted_vote.encrypted_answers[question_num].choices[answer_num]
+                enc_vote_choice.pk = self.public_key
+                self.tally[question_num][answer_num] = encrypted_vote.encrypted_answers[question_num].choices[
+                                                           answer_num] * self.tally[question_num][answer_num]
+
+        self.num_tallied += 1
+
+    def decryption_factors_and_proofs(self, sk):
+        """
+        returns an array of decryption factors and a corresponding array of decryption proofs.
+        makes the decryption factors into strings, for general Helios / JS compatibility.
+        """
+        # for all choices of all questions (double list comprehension)
+        decryption_factors = []
+        decryption_proof = []
+
+        for question_num, question in enumerate(self.questions):
+            answers = question['answers']
+            question_factors = []
+            question_proof = []
+
+            for answer_num, answer in enumerate(answers):
+                # do decryption and proof of it
+                dec_factor, proof = sk.decryption_factor_and_proof(self.tally[question_num][answer_num])
+
+                # look up appropriate discrete log
+                # this is the string conversion
+                question_factors.append(str(dec_factor))
+                question_proof.append(proof.toJSONDict())
+
+            decryption_factors.append(question_factors)
+            decryption_proof.append(question_proof)
+
+        return decryption_factors, decryption_proof
+
+    def decrypt_and_prove(self, sk, discrete_logs=None):
+        """
+        returns an array of tallies and a corresponding array of decryption proofs.
+        """
+
+        # who's keeping track of discrete logs?
+        if not discrete_logs:
+            discrete_logs = self.discrete_logs
+
+        # for all choices of all questions (double list comprehension)
+        decrypted_tally = []
+        decryption_proof = []
+
+        for question_num in range(len(self.questions)):
+            question = self.questions[question_num]
+            answers = question['answers']
+            question_tally = []
+            question_proof = []
+
+            for answer_num in range(len(answers)):
+                # do decryption and proof of it
+                plaintext, proof = sk.prove_decryption(self.tally[question_num][answer_num])
+
+                # look up appropriate discrete log
+                question_tally.append(discrete_logs[plaintext])
+                question_proof.append(proof)
+
+            decrypted_tally.append(question_tally)
+            decryption_proof.append(question_proof)
+
+        return decrypted_tally, decryption_proof
+
+    def verify_decryption_proofs(self, decryption_factors, decryption_proofs, public_key, challenge_generator):
+        """
+        decryption_factors is a list of lists of dec factors
+        decryption_proofs are the corresponding proofs
+        public_key is, of course, the public key of the trustee
+        """
+
+        # go through each one
+        for q_num, q in enumerate(self.tally):
+            for a_num, answer_tally in enumerate(q):
+                # parse the proof
+                proof = algs.EGZKProof.fromJSONDict(decryption_proofs[q_num][a_num])
+
+                # check that g, alpha, y, dec_factor is a DH tuple
+                if not proof.verify(public_key.g, answer_tally.alpha, public_key.y,
+                                    int(decryption_factors[q_num][a_num]), public_key.p, public_key.q,
+                                    challenge_generator):
+                    return False
+
+        return True
+
+    def decrypt_from_factors(self, decryption_factors, public_key):
+        """
+        decrypt a tally given decryption factors
+
+        The decryption factors are a list of decryption factor sets, for each trustee.
+        Each decryption factor set is a list of lists of decryption factors (questions/answers).
+        """
+
+        # pre-compute a dlog table
+        dlog_table = DLogTable(base=public_key.g, modulus=public_key.p)
+        dlog_table.precompute(self.num_tallied)
+
+        result = []
 
-    # go through each one
-    for q_num, q in enumerate(self.tally):
-      q_result = []
+        # go through each one
+        for q_num, q in enumerate(self.tally):
+            q_result = []
 
-      for a_num, a in enumerate(q):
-        # coalesce the decryption factors into one list
-        dec_factor_list = [df[q_num][a_num] for df in decryption_factors]
-        raw_value = self.tally[q_num][a_num].decrypt(dec_factor_list, public_key)
+            for a_num, a in enumerate(q):
+                # coalesce the decryption factors into one list
+                dec_factor_list = [df[q_num][a_num] for df in decryption_factors]
+                raw_value = self.tally[q_num][a_num].decrypt(dec_factor_list, public_key)
 
-        q_result.append(dlog_table.lookup(raw_value))
+                q_result.append(dlog_table.lookup(raw_value))
 
-      result.append(q_result)
+            result.append(q_result)
 
-    return result
+        return result
 
-  def _process_value_in(self, field_name, field_value):
-    if field_name == 'tally':
-      return [[algs.EGCiphertext.fromJSONDict(a) for a in q] for q in field_value]
+    def _process_value_in(self, field_name, field_value):
+        if field_name == 'tally':
+            return [[algs.EGCiphertext.fromJSONDict(a) for a in q] for q in field_value]
 
-  def _process_value_out(self, field_name, field_value):
-    if field_name == 'tally':
-      return [[a.toJSONDict() for a in q] for q in field_value]
+    def _process_value_out(self, field_name, field_value):
+        if field_name == 'tally':
+            return [[a.toJSONDict() for a in q] for q in field_value]
diff --git a/helios/crypto/elgamal.py b/helios/crypto/elgamal.py
index 88a08c01ba6c7ab7366281faf989d72427c1d4a8..33eb03083319aa0b7f9ab61defbca3a50bd57440 100644
--- a/helios/crypto/elgamal.py
+++ b/helios/crypto/elgamal.py
@@ -8,12 +8,13 @@ Ben Adida
 ben@adida.net
 """
 
-import math, hashlib, logging
-import randpool, number
+import logging
 
-import numtheory
+from Crypto.Hash import SHA1
+from Crypto.Util.number import inverse
+
+from helios.crypto.utils import random
 
-from algs import Utils
 
 class Cryptosystem(object):
     def __init__(self):
@@ -21,30 +22,6 @@ class Cryptosystem(object):
       self.q = None
       self.g = None
 
-    @classmethod
-    def generate(cls, n_bits):
-      """
-      generate an El-Gamal environment. Returns an instance
-      of ElGamal(), with prime p, group size q, and generator g
-      """
-      
-      EG = cls()
-      
-      # find a prime p such that (p-1)/2 is prime q
-      EG.p = Utils.random_safe_prime(n_bits)
-
-      # q is the order of the group
-      # FIXME: not always p-1/2
-      EG.q = (EG.p-1)/2
-  
-      # find g that generates the q-order subgroup
-      while True:
-        EG.g = Utils.random_mpz_lt(EG.p)
-        if pow(EG.g, EG.q, EG.p) == 1:
-          break
-
-      return EG
-
     def generate_keypair(self):
       """
       generates a keypair in the setting
@@ -68,7 +45,7 @@ class KeyPair(object):
       self.pk.p = p
       self.pk.q = q
       
-      self.sk.x = Utils.random_mpz_lt(q)
+      self.sk.x = random.mpz_lt(q)
       self.pk.y = pow(g, self.sk.x, p)
       
       self.sk.public_key = self.pk
@@ -106,7 +83,7 @@ class PublicKey:
         """
         Encrypt a plaintext and return the randomness just generated and used.
         """
-        r = Utils.random_mpz_lt(self.q)
+        r = random.mpz_lt(self.q)
         ciphertext = self.encrypt_with_r(plaintext, r)
         
         return [ciphertext, r]
@@ -181,7 +158,7 @@ class SecretKey:
         if not dec_factor:
             dec_factor = self.decryption_factor(ciphertext)
 
-        m = (Utils.inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p
+        m = (inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p
 
         if decode_m:
           # get m back from the q-order subgroup
@@ -207,15 +184,15 @@ class SecretKey:
         and alpha^t = b * beta/m ^ c
         """
         
-        m = (Utils.inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p
-        beta_over_m = (ciphertext.beta * Utils.inverse(m, self.pk.p)) % self.pk.p
+        m = (inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p
+        beta_over_m = (ciphertext.beta * inverse(m, self.pk.p)) % self.pk.p
 
         # pick a random w
-        w = Utils.random_mpz_lt(self.pk.q)
+        w = random.mpz_lt(self.pk.q)
         a = pow(self.pk.g, w, self.pk.p)
         b = pow(ciphertext.alpha, w, self.pk.p)
 
-        c = int(hashlib.sha1(str(a) + "," + str(b)).hexdigest(),16)
+        c = int(SHA1.new(bytes(str(a) + "," + str(b), 'utf-8')).hexdigest(),16)
 
         t = (w + self.x * c) % self.pk.q
 
@@ -232,7 +209,7 @@ class SecretKey:
       Verifier provides challenge modulo q.
       Prover computes response = w + x*challenge mod q, where x is the secret key.
       """
-      w = Utils.random_mpz_lt(self.pk.q)
+      w = random.mpz_lt(self.pk.q)
       commitment = pow(self.pk.g, w, self.pk.p)
       challenge = challenge_generator(commitment) % self.pk.q
       response = (w + (self.x * challenge)) % self.pk.q
@@ -255,7 +232,7 @@ class Ciphertext:
         """
         Homomorphic Multiplication of ciphertexts.
         """
-        if type(other) == int and (other == 0 or other == 1):
+        if isinstance(other, int) and (other == 0 or other == 1):
           return self
           
         if self.pk != other.pk:
@@ -287,7 +264,7 @@ class Ciphertext:
         """
         Reencryption with fresh randomness, which is returned.
         """
-        r = Utils.random_mpz_lt(self.pk.q)
+        r = random.mpz_lt(self.pk.q)
         new_c = self.reenc_with_r(r)
         return [new_c, r]
     
@@ -301,17 +278,17 @@ class Ciphertext:
       """
       Check for ciphertext equality.
       """
-      if other == None:
+      if other is None:
         return False
         
-      return (self.alpha == other.alpha and self.beta == other.beta)
+      return self.alpha == other.alpha and self.beta == other.beta
     
     def generate_encryption_proof(self, plaintext, randomness, challenge_generator):
       """
       Generate the disjunctive encryption proof of encryption
       """
       # random W
-      w = Utils.random_mpz_lt(self.pk.q)
+      w = random.mpz_lt(self.pk.q)
 
       # build the proof
       proof = ZKProof()
@@ -331,20 +308,20 @@ class Ciphertext:
     def simulate_encryption_proof(self, plaintext, challenge=None):
       # generate a random challenge if not provided
       if not challenge:
-        challenge = Utils.random_mpz_lt(self.pk.q)
+        challenge = random.mpz_lt(self.pk.q)
         
       proof = ZKProof()
       proof.challenge = challenge
 
       # compute beta/plaintext, the completion of the DH tuple
-      beta_over_plaintext =  (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p
+      beta_over_plaintext =  (self.beta * inverse(plaintext.m, self.pk.p)) % self.pk.p
       
       # random response, does not even need to depend on the challenge
-      proof.response = Utils.random_mpz_lt(self.pk.q);
+      proof.response = random.mpz_lt(self.pk.q);
 
       # now we compute A and B
-      proof.commitment['A'] = (Utils.inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.g, proof.response, self.pk.p)) % self.pk.p
-      proof.commitment['B'] = (Utils.inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.y, proof.response, self.pk.p)) % self.pk.p
+      proof.commitment['A'] = (inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.g, proof.response, self.pk.p)) % self.pk.p
+      proof.commitment['B'] = (inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.y, proof.response, self.pk.p)) % self.pk.p
 
       return proof
     
@@ -397,7 +374,7 @@ class Ciphertext:
       first_check = (pow(self.pk.g, proof.response, self.pk.p) == ((pow(self.alpha, proof.challenge, self.pk.p) * proof.commitment['A']) % self.pk.p))
       
       # check that y^response = B * (beta/m)^challenge
-      beta_over_m = (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p
+      beta_over_m = (self.beta * inverse(plaintext.m, self.pk.p)) % self.pk.p
       second_check = (pow(self.pk.y, proof.response, self.pk.p) == ((pow(beta_over_m, proof.challenge, self.pk.p) * proof.commitment['B']) % self.pk.p))
       
       # print "1,2: %s %s " % (first_check, second_check)
@@ -416,7 +393,7 @@ class Ciphertext:
       for i in range(len(plaintexts)):
         # if a proof fails, stop right there
         if not self.verify_encryption_proof(plaintexts[i], proof.proofs[i]):
-          print "bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i])
+          print("bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i]))
           return False
           
       # logging.info("made it past the two encryption proofs")
@@ -444,7 +421,7 @@ class Ciphertext:
       """
       running_decryption = self.beta
       for dec_factor in decryption_factors:
-        running_decryption = (running_decryption * Utils.inverse(dec_factor, public_key.p)) % public_key.p
+        running_decryption = (running_decryption * inverse(dec_factor, public_key.p)) % public_key.p
         
       return running_decryption
 
@@ -473,7 +450,7 @@ class ZKProof(object):
       """
 
       # generate random w
-      w = Utils.random_mpz_lt(q)
+      w = random.mpz_lt(q)
       
       # create proof instance
       proof = cls()
@@ -526,7 +503,7 @@ def disjunctive_challenge_generator(commitments):
     array_to_hash.append(str(commitment['B']))
 
   string_to_hash = ",".join(array_to_hash)
-  return int(hashlib.sha1(string_to_hash).hexdigest(),16)
+  return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(),16)
   
 # a challenge generator for Fiat-Shamir with A,B commitment
 def fiatshamir_challenge_generator(commitment):
@@ -534,5 +511,5 @@ def fiatshamir_challenge_generator(commitment):
 
 def DLog_challenge_generator(commitment):
   string_to_hash = str(commitment)
-  return int(hashlib.sha1(string_to_hash).hexdigest(),16)
+  return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(),16)
 
diff --git a/helios/crypto/number.py b/helios/crypto/number.py
deleted file mode 100644
index 9d50563e904ab50a5446916953b0091c2f228031..0000000000000000000000000000000000000000
--- a/helios/crypto/number.py
+++ /dev/null
@@ -1,201 +0,0 @@
-#
-#   number.py : Number-theoretic functions
-#
-#  Part of the Python Cryptography Toolkit
-#
-# Distribute and use freely; there are no restrictions on further
-# dissemination and usage except those imposed by the laws of your
-# country of residence.  This software is provided "as is" without
-# warranty of fitness for use or suitability for any purpose, express
-# or implied. Use at your own risk or not at all.
-#
-
-__revision__ = "$Id: number.py,v 1.13 2003/04/04 18:21:07 akuchling Exp $"
-
-bignum = long
-try:
-    from Crypto.PublicKey import _fastmath
-except ImportError:
-    _fastmath = None
-
-# Commented out and replaced with faster versions below
-## def long2str(n):
-##     s=''
-##     while n>0:
-##         s=chr(n & 255)+s
-##         n=n>>8
-##     return s
-
-## import types
-## def str2long(s):
-##     if type(s)!=types.StringType: return s   # Integers will be left alone
-##     return reduce(lambda x,y : x*256+ord(y), s, 0L)
-
-def size (N):
-    """size(N:long) : int
-    Returns the size of the number N in bits.
-    """
-    bits, power = 0,1L
-    while N >= power:
-        bits += 1
-        power = power << 1
-    return bits
-
-def getRandomNumber(N, randfunc):
-    """getRandomNumber(N:int, randfunc:callable):long
-    Return an N-bit random number."""
-
-    S = randfunc(N/8)
-    odd_bits = N % 8
-    if odd_bits != 0:
-        char = ord(randfunc(1)) >> (8-odd_bits)
-        S = chr(char) + S
-    value = bytes_to_long(S)
-    value |= 2L ** (N-1)                # Ensure high bit is set
-    assert size(value) >= N
-    return value
-
-def GCD(x,y):
-    """GCD(x:long, y:long): long
-    Return the GCD of x and y.
-    """
-    x = abs(x) ; y = abs(y)
-    while x > 0:
-        x, y = y % x, x
-    return y
-
-def inverse(u, v):
-    """inverse(u:long, u:long):long
-    Return the inverse of u mod v.
-    """
-    u3, v3 = long(u), long(v)
-    u1, v1 = 1L, 0L
-    while v3 > 0:
-        q=u3 / v3
-        u1, v1 = v1, u1 - v1*q
-        u3, v3 = v3, u3 - v3*q
-    while u1<0:
-        u1 = u1 + v
-    return u1
-
-# Given a number of bits to generate and a random generation function,
-# find a prime number of the appropriate size.
-
-def getPrime(N, randfunc):
-    """getPrime(N:int, randfunc:callable):long
-    Return a random N-bit prime number.
-    """
-
-    number=getRandomNumber(N, randfunc) | 1
-    while (not isPrime(number)):
-        number=number+2
-    return number
-
-def isPrime(N):
-    """isPrime(N:long):bool
-    Return true if N is prime.
-    """
-    if N == 1:
-        return 0
-    if N in sieve:
-        return 1
-    for i in sieve:
-        if (N % i)==0:
-            return 0
-
-    # Use the accelerator if available
-    if _fastmath is not None:
-        return _fastmath.isPrime(N)
-
-    # Compute the highest bit that's set in N
-    N1 = N - 1L
-    n = 1L
-    while (n<N):
-        n=n<<1L
-    n = n >> 1L
-
-    # Rabin-Miller test
-    for c in sieve[:7]:
-        a=long(c) ; d=1L ; t=n
-        while (t):  # Iterate over the bits in N1
-            x=(d*d) % N
-            if x==1L and d!=1L and d!=N1:
-                return 0  # Square root of 1 found
-            if N1 & t:
-                d=(x*a) % N
-            else:
-                d=x
-            t = t >> 1L
-        if d!=1L:
-            return 0
-    return 1
-
-# Small primes used for checking primality; these are all the primes
-# less than 256.  This should be enough to eliminate most of the odd
-# numbers before needing to do a Rabin-Miller test at all.
-
-sieve=[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59,
-       61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127,
-       131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193,
-       197, 199, 211, 223, 227, 229, 233, 239, 241, 251]
-
-# Improved conversion functions contributed by Barry Warsaw, after
-# careful benchmarking
-
-import struct
-
-def long_to_bytes(n, blocksize=0):
-    """long_to_bytes(n:long, blocksize:int) : string
-    Convert a long integer to a byte string.
-
-    If optional blocksize is given and greater than zero, pad the front of the
-    byte string with binary zeros so that the length is a multiple of
-    blocksize.
-    """
-    # after much testing, this algorithm was deemed to be the fastest
-    s = ''
-    n = long(n)
-    pack = struct.pack
-    while n > 0:
-        s = pack('>I', n & 0xffffffffL) + s
-        n = n >> 32
-    # strip off leading zeros
-    for i in range(len(s)):
-        if s[i] != '\000':
-            break
-    else:
-        # only happens when n == 0
-        s = '\000'
-        i = 0
-    s = s[i:]
-    # add back some pad bytes.  this could be done more efficiently w.r.t. the
-    # de-padding being done above, but sigh...
-    if blocksize > 0 and len(s) % blocksize:
-        s = (blocksize - len(s) % blocksize) * '\000' + s
-    return s
-
-def bytes_to_long(s):
-    """bytes_to_long(string) : long
-    Convert a byte string to a long integer.
-
-    This is (essentially) the inverse of long_to_bytes().
-    """
-    acc = 0L
-    unpack = struct.unpack
-    length = len(s)
-    if length % 4:
-        extra = (4 - length % 4)
-        s = '\000' * extra + s
-        length = length + extra
-    for i in range(0, length, 4):
-        acc = (acc << 32) + unpack('>I', s[i:i+4])[0]
-    return acc
-
-# For backwards compatibility...
-import warnings
-def long2str(n, blocksize=0):
-    warnings.warn("long2str() has been replaced by long_to_bytes()")
-    return long_to_bytes(n, blocksize)
-def str2long(s):
-    warnings.warn("str2long() has been replaced by bytes_to_long()")
-    return bytes_to_long(s)
diff --git a/helios/crypto/numtheory.py b/helios/crypto/numtheory.py
index 16fcf9aa0c2fe0017471546d0f353c9cb60878bd..e16691745cf7f9dd2713274b67c1a1a0091df12e 100644
--- a/helios/crypto/numtheory.py
+++ b/helios/crypto/numtheory.py
@@ -103,7 +103,7 @@ def trial_division(n, bound=None):
     if n == 1: return 1
     for p in [2, 3, 5]:
         if n%p == 0: return p
-    if bound == None: bound = n
+    if bound is None: bound = n
     dif = [6, 4, 2, 4, 2, 4, 6, 2]
     m = 7; i = 1
     while m <= bound and m*m <= n:
@@ -207,7 +207,7 @@ def inversemod(a, n):
     """
     g, x, y = xgcd(a, n)
     if g != 1:
-        raise ZeroDivisionError, (a,n)
+        raise ZeroDivisionError(a,n)
     assert g == 1, "a must be coprime to n."
     return x%n
 
@@ -225,7 +225,7 @@ def solve_linear(a,b,n):
     Examples:
     >>> solve_linear(4, 2, 10)
     8
-    >>> solve_linear(2, 1, 4) == None
+    >>> solve_linear(2, 1, 4) is None
     True
     """
     g, c, _ = xgcd(a,n)                 # (1)
@@ -1014,7 +1014,7 @@ def elliptic_curve_method(N, m, tries=5):
         E, P = randcurve(N)                    # (2)
         try:                                   # (3)
             Q = ellcurve_mul(E, m, P)          # (4)
-        except ZeroDivisionError, x:           # (5)
+        except ZeroDivisionError as x:         # (5)
             g = gcd(x[0],N)                    # (6)
             if g != 1 or g != N: return g      # (7)
     return N             
@@ -1153,7 +1153,7 @@ class Poly:                                     # (1)
         return r
     def __neg__(self):
         v = {}
-        for m in self.v.keys():
+        for m in list(self.v.keys()):
             v[m] = -self.v[m]
         return Poly(v)
     def __div__(self, other):
@@ -1161,7 +1161,7 @@ class Poly:                                     # (1)
 
     def __getitem__(self, m):                   # (6)
         m = tuple(m)
-        if not self.v.has_key(m): self.v[m] = 0
+        if m not in self.v: self.v[m] = 0
         return self.v[m]
     def __setitem__(self, m, c):
         self.v[tuple(m)] = c
@@ -1169,7 +1169,7 @@ class Poly:                                     # (1)
         del self.v[tuple(m)]
 
     def monomials(self):                        # (7)
-        return self.v.keys()
+        return list(self.v.keys())
     def normalize(self):                        # (8)
         while True:
             finished = True
@@ -1244,8 +1244,8 @@ def prove_associative():                        # (15)
                    - (x3 + x4)*(x3 - x4)*(x3 - x4))
     s2 = (x3 - x4)*(x3 - x4)*((y1 - y5)*(y1 - y5) \
                    - (x1 + x5)*(x1 - x5)*(x1 - x5))
-    print "Associative?"
-    print s1 == s2                              # (17)
+    print("Associative?")
+    print(s1 == s2)                              # (17)
 
 
 
diff --git a/helios/crypto/randpool.py b/helios/crypto/randpool.py
deleted file mode 100644
index 53a8acc035c225b23ea9e6edd2a8b27b39b75082..0000000000000000000000000000000000000000
--- a/helios/crypto/randpool.py
+++ /dev/null
@@ -1,422 +0,0 @@
-#
-#  randpool.py : Cryptographically strong random number generation
-#
-# Part of the Python Cryptography Toolkit
-#
-# Distribute and use freely; there are no restrictions on further
-# dissemination and usage except those imposed by the laws of your
-# country of residence.  This software is provided "as is" without
-# warranty of fitness for use or suitability for any purpose, express
-# or implied. Use at your own risk or not at all.
-#
-
-__revision__ = "$Id: randpool.py,v 1.14 2004/05/06 12:56:54 akuchling Exp $"
-
-import time, array, types, warnings, os.path
-from number import long_to_bytes
-try:
-    import Crypto.Util.winrandom as winrandom
-except:
-    winrandom = None
-
-STIRNUM = 3
-
-class RandomPool:
-    """randpool.py : Cryptographically strong random number generation.
-
-    The implementation here is similar to the one in PGP.  To be
-    cryptographically strong, it must be difficult to determine the RNG's
-    output, whether in the future or the past.  This is done by using
-    a cryptographic hash function to "stir" the random data.
-
-    Entropy is gathered in the same fashion as PGP; the highest-resolution
-    clock around is read and the data is added to the random number pool.
-    A conservative estimate of the entropy is then kept.
-
-    If a cryptographically secure random source is available (/dev/urandom
-    on many Unixes, Windows CryptGenRandom on most Windows), then use
-    it.
-
-    Instance Attributes:
-    bits : int
-      Maximum size of pool in bits
-    bytes : int
-      Maximum size of pool in bytes
-    entropy : int
-      Number of bits of entropy in this pool.
-
-    Methods:
-    add_event([s]) : add some entropy to the pool
-    get_bytes(int) : get N bytes of random data
-    randomize([N]) : get N bytes of randomness from external source
-    """
-
-
-    def __init__(self, numbytes = 160, cipher=None, hash=None):
-        if hash is None:
-            from hashlib import sha1 as hash
-
-        # The cipher argument is vestigial; it was removed from
-        # version 1.1 so RandomPool would work even in the limited
-        # exportable subset of the code
-        if cipher is not None:
-            warnings.warn("'cipher' parameter is no longer used")
-
-        if isinstance(hash, types.StringType):
-            # ugly hack to force __import__ to give us the end-path module
-            hash = __import__('Crypto.Hash.'+hash,
-                              None, None, ['new'])
-            warnings.warn("'hash' parameter should now be a hashing module")
-
-        self.bytes = numbytes
-        self.bits = self.bytes*8
-        self.entropy = 0
-        self._hash = hash
-
-        # Construct an array to hold the random pool,
-        # initializing it to 0.
-        self._randpool = array.array('B', [0]*self.bytes)
-
-        self._event1 = self._event2 = 0
-        self._addPos = 0
-        self._getPos = hash().digest_size
-        self._lastcounter=time.time()
-        self.__counter = 0
-
-        self._measureTickSize()        # Estimate timer resolution
-        self._randomize()
-
-    def _updateEntropyEstimate(self, nbits):
-        self.entropy += nbits
-        if self.entropy < 0:
-            self.entropy = 0
-        elif self.entropy > self.bits:
-            self.entropy = self.bits
-
-    def _randomize(self, N = 0, devname = '/dev/urandom'):
-        """_randomize(N, DEVNAME:device-filepath)
-        collects N bits of randomness from some entropy source (e.g.,
-        /dev/urandom on Unixes that have it, Windows CryptoAPI
-        CryptGenRandom, etc)
-        DEVNAME is optional, defaults to /dev/urandom.  You can change it
-        to /dev/random if you want to block till you get enough
-        entropy.
-        """
-        data = ''
-        if N <= 0:
-            nbytes = int((self.bits - self.entropy)/8+0.5)
-        else:
-            nbytes = int(N/8+0.5)
-        if winrandom:
-            # Windows CryptGenRandom provides random data.
-            data = winrandom.new().get_bytes(nbytes)
-        # GAE fix, benadida
-        #elif os.path.exists(devname):
-        #    # Many OSes support a /dev/urandom device
-        #    try:
-        #        f=open(devname)
-        #        data=f.read(nbytes)
-        #        f.close()
-        #    except IOError, (num, msg):
-        #        if num!=2: raise IOError, (num, msg)
-        #        # If the file wasn't found, ignore the error
-        if data:
-            self._addBytes(data)
-            # Entropy estimate: The number of bits of
-            # data obtained from the random source.
-            self._updateEntropyEstimate(8*len(data))
-        self.stir_n()                   # Wash the random pool
-
-    def randomize(self, N=0):
-        """randomize(N:int)
-        use the class entropy source to get some entropy data.
-        This is overridden by KeyboardRandomize().
-        """
-        return self._randomize(N)
-
-    def stir_n(self, N = STIRNUM):
-        """stir_n(N)
-        stirs the random pool N times
-        """
-        for i in xrange(N):
-            self.stir()
-
-    def stir (self, s = ''):
-        """stir(s:string)
-        Mix up the randomness pool.  This will call add_event() twice,
-        but out of paranoia the entropy attribute will not be
-        increased.  The optional 's' parameter is a string that will
-        be hashed with the randomness pool.
-        """
-
-        entropy=self.entropy            # Save inital entropy value
-        self.add_event()
-
-        # Loop over the randomness pool: hash its contents
-        # along with a counter, and add the resulting digest
-        # back into the pool.
-        for i in range(self.bytes / self._hash().digest_size):
-            h = self._hash(self._randpool)
-            h.update(str(self.__counter) + str(i) + str(self._addPos) + s)
-            self._addBytes( h.digest() )
-            self.__counter = (self.__counter + 1) & 0xFFFFffffL
-
-        self._addPos, self._getPos = 0, self._hash().digest_size
-        self.add_event()
-
-        # Restore the old value of the entropy.
-        self.entropy=entropy
-
-
-    def get_bytes (self, N):
-        """get_bytes(N:int) : string
-        Return N bytes of random data.
-        """
-
-        s=''
-        i, pool = self._getPos, self._randpool
-        h=self._hash()
-        dsize = self._hash().digest_size
-        num = N
-        while num > 0:
-            h.update( self._randpool[i:i+dsize] )
-            s = s + h.digest()
-            num = num - dsize
-            i = (i + dsize) % self.bytes
-            if i<dsize:
-                self.stir()
-                i=self._getPos
-
-        self._getPos = i
-        self._updateEntropyEstimate(- 8*N)
-        return s[:N]
-
-
-    def add_event(self, s=''):
-        """add_event(s:string)
-        Add an event to the random pool.  The current time is stored
-        between calls and used to estimate the entropy.  The optional
-        's' parameter is a string that will also be XORed into the pool.
-        Returns the estimated number of additional bits of entropy gain.
-        """
-        event = time.time()*1000
-        delta = self._noise()
-        s = (s + long_to_bytes(event) +
-             4*chr(0xaa) + long_to_bytes(delta) )
-        self._addBytes(s)
-        if event==self._event1 and event==self._event2:
-            # If events are coming too closely together, assume there's
-            # no effective entropy being added.
-            bits=0
-        else:
-            # Count the number of bits in delta, and assume that's the entropy.
-            bits=0
-            while delta:
-                delta, bits = delta>>1, bits+1
-            if bits>8: bits=8
-
-        self._event1, self._event2 = event, self._event1
-
-        self._updateEntropyEstimate(bits)
-        return bits
-
-    # Private functions
-    def _noise(self):
-        # Adds a bit of noise to the random pool, by adding in the
-        # current time and CPU usage of this process.
-        # The difference from the previous call to _noise() is taken
-        # in an effort to estimate the entropy.
-        t=time.time()
-        delta = (t - self._lastcounter)/self._ticksize*1e6
-        self._lastcounter = t
-        self._addBytes(long_to_bytes(long(1000*time.time())))
-        self._addBytes(long_to_bytes(long(1000*time.clock())))
-        self._addBytes(long_to_bytes(long(1000*time.time())))
-        self._addBytes(long_to_bytes(long(delta)))
-
-        # Reduce delta to a maximum of 8 bits so we don't add too much
-        # entropy as a result of this call.
-        delta=delta % 0xff
-        return int(delta)
-
-
-    def _measureTickSize(self):
-        # _measureTickSize() tries to estimate a rough average of the
-        # resolution of time that you can see from Python.  It does
-        # this by measuring the time 100 times, computing the delay
-        # between measurements, and taking the median of the resulting
-        # list.  (We also hash all the times and add them to the pool)
-        interval = [None] * 100
-        h = self._hash(`(id(self),id(interval))`)
-
-        # Compute 100 differences
-        t=time.time()
-        h.update(`t`)
-        i = 0
-        j = 0
-        while i < 100:
-            t2=time.time()
-            h.update(`(i,j,t2)`)
-            j += 1
-            delta=int((t2-t)*1e6)
-            if delta:
-                interval[i] = delta
-                i += 1
-                t=t2
-
-        # Take the median of the array of intervals
-        interval.sort()
-        self._ticksize=interval[len(interval)/2]
-        h.update(`(interval,self._ticksize)`)
-        # mix in the measurement times and wash the random pool
-        self.stir(h.digest())
-
-    def _addBytes(self, s):
-        "XOR the contents of the string S into the random pool"
-        i, pool = self._addPos, self._randpool
-        for j in range(0, len(s)):
-            pool[i]=pool[i] ^ ord(s[j])
-            i=(i+1) % self.bytes
-        self._addPos = i
-
-    # Deprecated method names: remove in PCT 2.1 or later.
-    def getBytes(self, N):
-        warnings.warn("getBytes() method replaced by get_bytes()",
-                      DeprecationWarning)
-        return self.get_bytes(N)
-
-    def addEvent (self, event, s=""):
-        warnings.warn("addEvent() method replaced by add_event()",
-                      DeprecationWarning)
-        return self.add_event(s + str(event))
-
-class PersistentRandomPool (RandomPool):
-    def __init__ (self, filename=None, *args, **kwargs):
-        RandomPool.__init__(self, *args, **kwargs)
-        self.filename = filename
-        if filename:
-            try:
-                # the time taken to open and read the file might have
-                # a little disk variability, modulo disk/kernel caching...
-                f=open(filename, 'rb')
-                self.add_event()
-                data = f.read()
-                self.add_event()
-                # mix in the data from the file and wash the random pool
-                self.stir(data)
-                f.close()
-            except IOError:
-                # Oh, well; the file doesn't exist or is unreadable, so
-                # we'll just ignore it.
-                pass
-
-    def save(self):
-        if self.filename == "":
-            raise ValueError, "No filename set for this object"
-        # wash the random pool before save, provides some forward secrecy for
-        # old values of the pool.
-        self.stir_n()
-        f=open(self.filename, 'wb')
-        self.add_event()
-        f.write(self._randpool.tostring())
-        f.close()
-        self.add_event()
-        # wash the pool again, provide some protection for future values
-        self.stir()
-
-# non-echoing Windows keyboard entry
-_kb = 0
-if not _kb:
-    try:
-        import msvcrt
-        class KeyboardEntry:
-            def getch(self):
-                c = msvcrt.getch()
-                if c in ('\000', '\xe0'):
-                    # function key
-                    c += msvcrt.getch()
-                return c
-            def close(self, delay = 0):
-                if delay:
-                    time.sleep(delay)
-                    while msvcrt.kbhit():
-                        msvcrt.getch()
-        _kb = 1
-    except:
-        pass
-
-# non-echoing Posix keyboard entry
-if not _kb:
-    try:
-        import termios
-        class KeyboardEntry:
-            def __init__(self, fd = 0):
-                self._fd = fd
-                self._old = termios.tcgetattr(fd)
-                new = termios.tcgetattr(fd)
-                new[3]=new[3] & ~termios.ICANON & ~termios.ECHO
-                termios.tcsetattr(fd, termios.TCSANOW, new)
-            def getch(self):
-                termios.tcflush(0, termios.TCIFLUSH) # XXX Leave this in?
-                return os.read(self._fd, 1)
-            def close(self, delay = 0):
-                if delay:
-                    time.sleep(delay)
-                    termios.tcflush(self._fd, termios.TCIFLUSH)
-                termios.tcsetattr(self._fd, termios.TCSAFLUSH, self._old)
-        _kb = 1
-    except:
-        pass
-
-class KeyboardRandomPool (PersistentRandomPool):
-    def __init__(self, *args, **kwargs):
-        PersistentRandomPool.__init__(self, *args, **kwargs)
-
-    def randomize(self, N = 0):
-        "Adds N bits of entropy to random pool.  If N is 0, fill up pool."
-        import os, string, time
-        if N <= 0:
-            bits = self.bits - self.entropy
-        else:
-            bits = N*8
-        if bits == 0:
-            return
-        print bits,'bits of entropy are now required.  Please type on the keyboard'
-        print 'until enough randomness has been accumulated.'
-        kb = KeyboardEntry()
-        s=''    # We'll save the characters typed and add them to the pool.
-        hash = self._hash
-        e = 0
-        try:
-            while e < bits:
-                temp=str(bits-e).rjust(6)
-                os.write(1, temp)
-                s=s+kb.getch()
-                e += self.add_event(s)
-                os.write(1, 6*chr(8))
-            self.add_event(s+hash.new(s).digest() )
-        finally:
-            kb.close()
-        print '\n\007 Enough.  Please wait a moment.\n'
-        self.stir_n()   # wash the random pool.
-        kb.close(4)
-
-if __name__ == '__main__':
-    pool = RandomPool()
-    print 'random pool entropy', pool.entropy, 'bits'
-    pool.add_event('something')
-    print `pool.get_bytes(100)`
-    import tempfile, os
-    fname = tempfile.mktemp()
-    pool = KeyboardRandomPool(filename=fname)
-    print 'keyboard random pool entropy', pool.entropy, 'bits'
-    pool.randomize()
-    print 'keyboard random pool entropy', pool.entropy, 'bits'
-    pool.randomize(128)
-    pool.save()
-    saved = open(fname, 'rb').read()
-    print 'saved', `saved`
-    print 'pool ', `pool._randpool.tostring()`
-    newpool = PersistentRandomPool(fname)
-    print 'persistent random pool entropy', pool.entropy, 'bits'
-    os.remove(fname)
diff --git a/helios/crypto/utils.py b/helios/crypto/utils.py
index 9953bdc6aee3c80a3c668dbd0b892a490c879fe6..d854c886bd6e6a0128fbdeec232305ee97870a2a 100644
--- a/helios/crypto/utils.py
+++ b/helios/crypto/utils.py
@@ -1,34 +1,31 @@
 """
 Crypto Utils
 """
-import hashlib
-import hmac, base64, json
+import base64
+import math
+
+from Crypto.Hash import SHA256
+from Crypto.Random.random import StrongRandom
+
+random = StrongRandom()
+
+
+def random_mpz_lt(maximum, strong_random=random):
+    n_bits = int(math.floor(math.log(maximum, 2)))
+    res = strong_random.getrandbits(n_bits)
+    while res >= maximum:
+        res = strong_random.getrandbits(n_bits)
+    return res
+
+
+random.mpz_lt = random_mpz_lt
+
 
-from hashlib import sha256
-  
 def hash_b64(s):
-  """
-  hash the string using sha1 and produce a base64 output
-  removes the trailing "="
-  """
-  hasher = sha256(s)
-  result= base64.b64encode(hasher.digest())[:-1]
-  return result
-
-def to_json(d, no_whitespace=False):
-  if no_whitespace:
-    return json.dumps(d, sort_keys=True, separators=(',',':'))
-  else:
-    return json.dumps(d, sort_keys=True)
-
-def from_json(json_str):
-  if not json_str: return None
-  return json.loads(json_str)
-
-
-def do_hmac(k,s):
-  """
-  HMAC a value with a key, hex output
-  """
-  mac = hmac.new(k, s, hashlib.sha1)
-  return mac.hexdigest()
\ No newline at end of file
+    """
+    hash the string using sha256 and produce a base64 output
+    removes the trailing "="
+    """
+    hasher = SHA256.new(s.encode('utf-8'))
+    result = base64.b64encode(hasher.digest())[:-1]
+    return result
diff --git a/helios/datatypes/__init__.py b/helios/datatypes/__init__.py
index 93bca2442f0dbe792bf879afd3e3c704c617a28d..574f8eb9355bcec57a4a60e40f9f8c2154a2af25 100644
--- a/helios/datatypes/__init__.py
+++ b/helios/datatypes/__init__.py
@@ -25,6 +25,7 @@ And when data comes in:
   # but is not necessary for full JSON-LD objects.
   LDObject.deserialize(json_string, type=...)
 """
+import importlib
 
 from helios import utils
 from helios.crypto import utils as cryptoutils
@@ -36,21 +37,21 @@ def recursiveToDict(obj):
     if obj is None:
         return None
 
-    if type(obj) == list:
+    if isinstance(obj, list):
         return [recursiveToDict(el) for el in obj]
     else:
         return obj.toDict()
 
 def get_class(datatype):
     # already done?
-    if not isinstance(datatype, basestring):
+    if not isinstance(datatype, str):
         return datatype
 
     # parse datatype string "v31/Election" --> from v31 import Election
     parsed_datatype = datatype.split("/")
     
     # get the module
-    dynamic_module = __import__(".".join(parsed_datatype[:-1]), globals(), locals(), [], level=-1)
+    dynamic_module = importlib.import_module("helios.datatypes." + (".".join(parsed_datatype[:-1])))
     
     if not dynamic_module:
         raise Exception("no module for %s" % datatype)
@@ -58,7 +59,7 @@ def get_class(datatype):
     # go down the attributes to get to the class
     try:
         dynamic_ptr = dynamic_module
-        for attr in parsed_datatype[1:]:
+        for attr in parsed_datatype[-1:]:
             dynamic_ptr = getattr(dynamic_ptr, attr)
         dynamic_cls = dynamic_ptr
     except AttributeError:
@@ -119,7 +120,7 @@ class LDObject(object):
 
     @classmethod
     def instantiate(cls, obj, datatype=None):
-        "FIXME: should datatype override the object's internal datatype? probably not"
+        """FIXME: should datatype override the object's internal datatype? probably not"""
         if isinstance(obj, LDObject):
             return obj
 
@@ -149,9 +150,11 @@ class LDObject(object):
         setattr(self.wrapped_obj, attr, val)
 
     def loadData(self):
-        "load data using from the wrapped object"
+        """
+        load data using from the wrapped object
+        """
         # go through the subfields and instantiate them too
-        for subfield_name, subfield_type in self.STRUCTURED_FIELDS.iteritems():
+        for subfield_name, subfield_type in self.STRUCTURED_FIELDS.items():
             self.structured_fields[subfield_name] = self.instantiate(self._getattr_wrapped(subfield_name), datatype = subfield_type)
         
     def loadDataFromDict(self, d):
@@ -160,7 +163,7 @@ class LDObject(object):
         """
 
         # the structured fields
-        structured_fields = self.STRUCTURED_FIELDS.keys()
+        structured_fields = list(self.STRUCTURED_FIELDS.keys())
 
         # go through the fields and set them properly
         # on the newly instantiated object
@@ -195,7 +198,7 @@ class LDObject(object):
 
         for f in (alternate_fields or fields):
             # is it a structured subfield?
-            if self.structured_fields.has_key(f):
+            if f in self.structured_fields:
                 val[f] = recursiveToDict(self.structured_fields[f])
             else:
                 val[f] = self.process_value_out(f, self._getattr_wrapped(f))
@@ -274,8 +277,10 @@ class LDObject(object):
             return field_value
   
     def _process_value_out(self, field_name, field_value):
+        if isinstance(field_value, bytes):
+            return field_value.decode('utf-8')
         return None
-    
+
     def __eq__(self, other):
         if not hasattr(self, 'uuid'):
             return super(LDObject, self) == other
diff --git a/helios/datatypes/djangofield.py b/helios/datatypes/djangofield.py
index 05f8cf355845ddf0eed23d4efc93526a4a06e269..a299ace6c6a7b8f55e231a5e645d238aceb7f51c 100644
--- a/helios/datatypes/djangofield.py
+++ b/helios/datatypes/djangofield.py
@@ -6,9 +6,9 @@ http://www.djangosnippets.org/snippets/377/
 and adapted to LDObject
 """
 
-import json
 from django.db import models
 
+from helios import utils
 from . import LDObject
 
 
@@ -28,36 +28,26 @@ class LDObjectField(models.TextField):
         """Convert our string value to LDObject after we load it from the DB"""
 
         # did we already convert this?
-        if not isinstance(value, basestring):
+        if not isinstance(value, str):
             return value
 
         return self.from_db_value(value)
 
     # noinspection PyUnusedLocal
     def from_db_value(self, value, *args, **kwargs):
-        if value is None:
-            return None
-
         # in some cases, we're loading an existing array or dict,
-        # we skip this part but instantiate the LD object
-        if isinstance(value, basestring):
-            try:
-                parsed_value = json.loads(value)
-            except:
-                raise Exception("value is not JSON parseable, that's bad news")
-        else:
-            parsed_value = value
-
-        if parsed_value is not None:
-            # we give the wrapped object back because we're not dealing with serialization types
-            return_val = LDObject.fromDict(parsed_value, type_hint=self.type_hint).wrapped_obj
-            return return_val
-        else:
+        # from_json takes care of this duality
+        parsed_value = utils.from_json(value)
+        if parsed_value is None:
             return None
 
+        # we give the wrapped object back because we're not dealing with serialization types
+        return_val = LDObject.fromDict(parsed_value, type_hint=self.type_hint).wrapped_obj
+        return return_val
+
     def get_prep_value(self, value):
         """Convert our JSON object to a string before we save"""
-        if isinstance(value, basestring):
+        if isinstance(value, str):
             return value
 
         if value is None:
diff --git a/helios/datetimewidget.py b/helios/datetimewidget.py
index 81d81e0172f7538df77a8d8be8f95774c8bd4e99..dfd7ec04a42118460a216988fa2a9f5a323c1093 100644
--- a/helios/datetimewidget.py
+++ b/helios/datetimewidget.py
@@ -14,7 +14,7 @@ import datetime, time
 from django.utils.safestring import mark_safe
 
 # DATETIMEWIDGET
-calbtn = u'''<img src="%smedia/admin/img/admin/icon_calendar.gif" alt="calendar" id="%s_btn" style="cursor: pointer;" title="Select date" />
+calbtn = '''<img src="%smedia/admin/img/admin/icon_calendar.gif" alt="calendar" id="%s_btn" style="cursor: pointer;" title="Select date" />
 <script type="text/javascript">
     Calendar.setup({
         inputField     :    "%s",
@@ -51,13 +51,13 @@ class DateTimeWidget(forms.widgets.TextInput):
             except:
                 final_attrs['value'] = \
                                    force_unicode(value)
-        if not final_attrs.has_key('id'):
-            final_attrs['id'] = u'%s_id' % (name)
+        if 'id' not in final_attrs:
+            final_attrs['id'] = '%s_id' % (name)
         id = final_attrs['id']
 
         jsdformat = self.dformat #.replace('%', '%%')
         cal = calbtn % (settings.MEDIA_URL, id, id, jsdformat, id)
-        a = u'<input%s />%s%s' % (forms.util.flatatt(final_attrs), self.media, cal)
+        a = '<input%s />%s%s' % (forms.util.flatatt(final_attrs), self.media, cal)
         return mark_safe(a)
 
     def value_from_datadict(self, data, files, name):
@@ -84,12 +84,12 @@ class DateTimeWidget(forms.widgets.TextInput):
         Copy of parent's method, but modify value with strftime function before final comparsion
         """
         if data is None:
-            data_value = u''
+            data_value = ''
         else:
             data_value = data
 
         if initial is None:
-            initial_value = u''
+            initial_value = ''
         else:
             initial_value = initial
 
diff --git a/helios/fields.py b/helios/fields.py
index cf2ad6c3e7c2c6b506c0080b432f6207f4e37807..8d8e885fec3c13f72a0c24cd6ea0d7a39ab80d5c 100644
--- a/helios/fields.py
+++ b/helios/fields.py
@@ -1,9 +1,9 @@
-from time import strptime, strftime
 import datetime
-from django import forms
-from django.db import models
+
 from django.forms import fields
-from widgets import SplitSelectDateTimeWidget
+
+from .widgets import SplitSelectDateTimeWidget
+
 
 class SplitDateTimeField(fields.MultiValueField):
     widget = SplitSelectDateTimeWidget
diff --git a/helios/forms.py b/helios/forms.py
index 86fc18e5e3a0e88e4a65f32b62ef12b2bc1dea56..d7919675863641cd24abd5ba3082b0e6014d4e84 100644
--- a/helios/forms.py
+++ b/helios/forms.py
@@ -3,11 +3,12 @@ Forms for Helios
 """
 
 from django import forms
-from models import Election
-from widgets import SplitSelectDateTimeWidget
-from fields import SplitDateTimeField
 from django.conf import settings
 
+from .fields import SplitDateTimeField
+from .models import Election
+from .widgets import SplitSelectDateTimeWidget
+
 
 class ElectionForm(forms.Form):
   short_name = forms.SlugField(max_length=40, help_text='no spaces, will be part of the URL for your election, e.g. my-club-2010')
diff --git a/helios/management/commands/load_voter_files.py b/helios/management/commands/load_voter_files.py
index c34e682aba306f9bd1380a5ed1dafa91cb247b8b..1e3a79beeecf537b24963a5f2b80f1b46f653dfe 100644
--- a/helios/management/commands/load_voter_files.py
+++ b/helios/management/commands/load_voter_files.py
@@ -28,7 +28,7 @@ def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs):
                             dialect=dialect, **kwargs)
     for row in csv_reader:
         # decode UTF-8 back to Unicode, cell by cell:
-        yield [unicode(cell, 'utf-8') for cell in row]
+        yield [str(cell, 'utf-8') for cell in row]
 
 
 def utf_8_encoder(unicode_csv_data):
diff --git a/helios/migrations/0001_initial.py b/helios/migrations/0001_initial.py
index 8ba6efbb125a7b1c4e1e3ed6e74b831bedaf25f1..886b370219ab7fd2fc6fb218148761ae5c4d026c 100644
--- a/helios/migrations/0001_initial.py
+++ b/helios/migrations/0001_initial.py
@@ -1,10 +1,10 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals
 
 from django.db import models, migrations
+
+import helios.datatypes
 import helios.datatypes.djangofield
 import helios_auth.jsonfield
-import helios.datatypes
 
 
 class Migration(migrations.Migration):
diff --git a/helios/migrations/0002_castvote_cast_ip.py b/helios/migrations/0002_castvote_cast_ip.py
index 47db5b169eccee318b2ccb4fc18f125e984a9c83..bb7a422639f66cd81974549a95c000204ebe13da 100644
--- a/helios/migrations/0002_castvote_cast_ip.py
+++ b/helios/migrations/0002_castvote_cast_ip.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals
 
 from django.db import models, migrations
 
diff --git a/helios/migrations/0003_auto_20160507_1948.py b/helios/migrations/0003_auto_20160507_1948.py
index 162d6bb54dcb6c1e74c99e859615d7fe51dc179d..8e6f65266e5c6f04bf3cb07b8927ac94ea2ece95 100644
--- a/helios/migrations/0003_auto_20160507_1948.py
+++ b/helios/migrations/0003_auto_20160507_1948.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals
 
 from django.db import models, migrations
 
diff --git a/helios/migrations/0004_auto_20170528_2025.py b/helios/migrations/0004_auto_20170528_2025.py
index d49bb63753f4438ab79c0ce4101f7e56f7bcf8be..a437c5f11f03a5a0766ecd970a031bda409fe3fd 100644
--- a/helios/migrations/0004_auto_20170528_2025.py
+++ b/helios/migrations/0004_auto_20170528_2025.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals
 
 from django.db import migrations, models
 
diff --git a/helios/migrations/0005_auto_20210123_0941.py b/helios/migrations/0005_auto_20210123_0941.py
new file mode 100644
index 0000000000000000000000000000000000000000..355687c60a0129c2ed948815d938bfb1d67ed581
--- /dev/null
+++ b/helios/migrations/0005_auto_20210123_0941.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.28 on 2021-01-23 09:41
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('helios', '0004_auto_20170528_2025'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='election',
+            name='datatype',
+            field=models.CharField(default='legacy/Election', max_length=250),
+        ),
+        migrations.AlterField(
+            model_name='election',
+            name='election_type',
+            field=models.CharField(choices=[('election', 'Election'), ('referendum', 'Referendum')], default='election', max_length=250),
+        ),
+        migrations.AlterField(
+            model_name='voterfile',
+            name='voter_file',
+            field=models.FileField(max_length=250, null=True, upload_to='voters/%Y/%m/%d'),
+        ),
+    ]
diff --git a/helios/models.py b/helios/models.py
index 96e83ddc5755b02ff4fe1baf1384f8db03844df7..83344076f7d11068c3d85eec34b5c79613f6154c 100644
--- a/helios/models.py
+++ b/helios/models.py
@@ -6,25 +6,26 @@ Ben Adida
 (ben@adida.net)
 """
 
-import datetime
-
-import bleach
 import copy
 import csv
+import datetime
 import io
-import random
-import unicodecsv
 import uuid
+
+import bleach
+import unicodecsv
 from django.conf import settings
 from django.db import models, transaction
 
-from crypto import algs, utils
 from helios import datatypes
-from helios import utils as heliosutils
+from helios import utils
 from helios.datatypes.djangofield import LDObjectField
 # useful stuff in helios_auth
 from helios_auth.jsonfield import JSONField
 from helios_auth.models import User, AUTH_SYSTEMS
+from .crypto import algs
+from .crypto.elgamal import Cryptosystem
+from .crypto.utils import random, hash_b64
 
 
 class HeliosModel(models.Model, datatypes.LDObjectContainer):
@@ -181,22 +182,26 @@ class Election(HeliosModel):
     if not self.use_voter_aliases:
       return None
     
-    return heliosutils.one_val_raw_sql("select max(cast(substring(alias, 2) as integer)) from " + Voter._meta.db_table + " where election_id = %s", [self.id]) or 0
+    return utils.one_val_raw_sql("select max(cast(substring(alias, 2) as integer)) from " + Voter._meta.db_table + " where election_id = %s", [self.id]) or 0
 
   @property
   def encrypted_tally_hash(self):
     if not self.encrypted_tally:
       return None
 
-    return utils.hash_b64(self.encrypted_tally.toJSON())
+    return hash_b64(self.encrypted_tally.toJSON())
 
   @property
   def is_archived(self):
-    return self.archived_at != None
+    return self.archived_at is not None
 
   @property
   def description_bleached(self):
-    return bleach.clean(self.description, tags = bleach.ALLOWED_TAGS + ['p', 'h4', 'h5', 'h3', 'h2', 'br', 'u'])
+    return bleach.clean(self.description,
+                        tags=bleach.ALLOWED_TAGS + ['p', 'h4', 'h5', 'h3', 'h2', 'br', 'u'],
+                        strip=True,
+                        strip_comments=True,
+                        )
 
   @classmethod
   def get_featured(cls):
@@ -209,9 +214,9 @@ class Election(HeliosModel):
   @classmethod
   def get_by_user_as_admin(cls, user, archived_p=None, limit=None):
     query = cls.objects.filter(admin = user)
-    if archived_p == True:
+    if archived_p is True:
       query = query.exclude(archived_at= None)
-    if archived_p == False:
+    if archived_p is False:
       query = query.filter(archived_at= None)
     query = query.order_by('-created_at')
     if limit:
@@ -222,9 +227,9 @@ class Election(HeliosModel):
   @classmethod
   def get_by_user_as_voter(cls, user, archived_p=None, limit=None):
     query = cls.objects.filter(voter__user = user)
-    if archived_p == True:
+    if archived_p is True:
       query = query.exclude(archived_at= None)
-    if archived_p == False:
+    if archived_p is False:
       query = query.filter(archived_at= None)
     query = query.order_by('-created_at')
     if limit:
@@ -285,7 +290,7 @@ class Election(HeliosModel):
     if not self.openreg:
       return False
     
-    if self.eligibility == None:
+    if self.eligibility is None:
       return True
       
     # is the user eligible for one of these cases?
@@ -300,7 +305,7 @@ class Election(HeliosModel):
       return []
 
     # constraints that are relevant
-    relevant_constraints = [constraint['constraint'] for constraint in self.eligibility if constraint['auth_system'] == user_type and constraint.has_key('constraint')]
+    relevant_constraints = [constraint['constraint'] for constraint in self.eligibility if constraint['auth_system'] == user_type and 'constraint' in constraint]
     if len(relevant_constraints) > 0:
       return relevant_constraints[0]
     else:
@@ -326,7 +331,7 @@ class Election(HeliosModel):
       return_val = "<ul>"
       
       for constraint in self.eligibility:
-        if constraint.has_key('constraint'):
+        if 'constraint' in constraint:
           for one_constraint in constraint['constraint']:
             return_val += "<li>%s</li>" % AUTH_SYSTEMS[constraint['auth_system']].pretty_eligibility(one_constraint)
         else:
@@ -340,7 +345,7 @@ class Election(HeliosModel):
     """
     has voting begun? voting begins if the election is frozen, at the prescribed date or at the date that voting was forced to start
     """
-    return self.frozen_at != None and (self.voting_starts_at == None or (datetime.datetime.utcnow() >= (self.voting_started_at or self.voting_starts_at)))
+    return self.frozen_at is not None and (self.voting_starts_at is None or (datetime.datetime.utcnow() >= (self.voting_started_at or self.voting_starts_at)))
     
   def voting_has_stopped(self):
     """
@@ -348,12 +353,12 @@ class Election(HeliosModel):
     or failing that the date voting was extended until, or failing that the date voting is scheduled to end at.
     """
     voting_end = self.voting_ended_at or self.voting_extended_until or self.voting_ends_at
-    return (voting_end != None and datetime.datetime.utcnow() >= voting_end) or self.encrypted_tally
+    return (voting_end is not None and datetime.datetime.utcnow() >= voting_end) or self.encrypted_tally
 
   @property
   def issues_before_freeze(self):
     issues = []
-    if self.questions == None or len(self.questions) == 0:
+    if self.questions is None or len(self.questions) == 0:
       issues.append(
         {'type': 'questions',
          'action': "add questions to the ballot"}
@@ -367,7 +372,7 @@ class Election(HeliosModel):
           })
 
     for t in trustees:
-      if t.public_key == None:
+      if t.public_key is None:
         issues.append({
             'type': 'trustee keypairs',
             'action': 'have trustee %s generate a keypair' % t.name
@@ -396,8 +401,8 @@ class Election(HeliosModel):
     self.save()    
   
   def ready_for_decryption(self):
-    return self.encrypted_tally != None
-    
+    return self.encrypted_tally is not None
+
   def ready_for_decryption_combination(self):
     """
     do we have a tally from all trustees?
@@ -445,7 +450,7 @@ class Election(HeliosModel):
     else:
       voters = Voter.get_by_election(self)
       voters_json = utils.to_json([v.toJSONDict() for v in voters])
-      self.voters_hash = utils.hash_b64(voters_json)
+      self.voters_hash = hash_b64(voters_json)
     
   def increment_voters(self):
     ## FIXME
@@ -469,15 +474,16 @@ class Election(HeliosModel):
     """
 
     # don't override existing eligibility
-    if self.eligibility != None:
+    if self.eligibility is not None:
       return
 
     # enable this ONLY once the cast_confirm screen makes sense
     #if self.voter_set.count() == 0:
     #  return
 
-    auth_systems = copy.copy(settings.AUTH_ENABLED_AUTH_SYSTEMS)
-    voter_types = [r['user__user_type'] for r in self.voter_set.values('user__user_type').distinct() if r['user__user_type'] != None]
+    auth_systems = copy.copy(settings.AUTH_ENABLED_SYSTEMS)
+    voter_types = [r['user__user_type'] for r in self.voter_set.values('user__user_type').distinct() if
+                   r['user__user_type'] is not None]
 
     # password is now separate, not an explicit voter type
     if self.voter_set.filter(user=None).count() > 0:
@@ -526,6 +532,7 @@ class Election(HeliosModel):
     """
     generate a trustee including the secret key,
     thus a helios-based trustee
+    :type params: Cryptosystem
     """
     # FIXME: generate the keypair
     keypair = params.generate_keypair()
@@ -553,7 +560,7 @@ class Election(HeliosModel):
       return None
     
   def has_helios_trustee(self):
-    return self.get_helios_trustee() != None
+    return self.get_helios_trustee() is not None
 
   def helios_trustee_decrypt(self):
     tally = self.encrypted_tally
@@ -597,7 +604,7 @@ class Election(HeliosModel):
     determining the winner for one question
     """
     # sort the answers , keep track of the index
-    counts = sorted(enumerate(result), key=lambda(x): x[1])
+    counts = sorted(enumerate(result), key=lambda x: x[1])
     counts.reverse()
     
     the_max = question['max'] or 1
@@ -680,9 +687,9 @@ def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs):
     for row in csv_reader:
       # decode UTF-8 back to Unicode, cell by cell:
       try:
-        yield [unicode(cell, 'utf-8') for cell in row]
+        yield [str(cell, 'utf-8') for cell in row]
       except:
-        yield [unicode(cell, 'latin-1') for cell in row]        
+        yield [str(cell, 'latin-1') for cell in row]        
 
 def utf_8_encoder(unicode_csv_data):
     for line in unicode_csv_data:
@@ -713,20 +720,25 @@ class VoterFile(models.Model):
 
   def itervoters(self):
     if self.voter_file_content:
-      if type(self.voter_file_content) == unicode:
-        content = self.voter_file_content.encode('utf-8')
-      else:
+      if isinstance(self.voter_file_content, str):
+        content = self.voter_file_content.encode(encoding='utf-8')
+      elif isinstance(self.voter_file_content, bytes):
         content = self.voter_file_content
+      else:
+        raise TypeError("voter_file_content is of type {0} instead of str or bytes"
+                        .format(str(type(self.voter_file_content))))
 
       # now we have to handle non-universal-newline stuff
       # we do this in a simple way: replace all \r with \n
       # then, replace all double \n with single \n
       # this should leave us with only \n
-      content = content.replace('\r','\n').replace('\n\n','\n')
+      content = content.replace(b'\r',b'\n').replace(b'\n\n',b'\n')
 
+      close = False
       voter_stream = io.BytesIO(content)
     else:
-      voter_stream = open(self.voter_file.path, "rU")
+      close = True
+      voter_stream = open(self.voter_file.path, "rb")
 
     #reader = unicode_csv_reader(voter_stream)
     reader = unicodecsv.reader(voter_stream, encoding='utf-8')
@@ -750,6 +762,8 @@ class VoterFile(models.Model):
         return_dict['name'] = return_dict['email']
 
       yield return_dict
+    if close:
+      voter_stream.close()
     
   def process(self):
     self.processing_started_at = datetime.datetime.utcnow()
@@ -776,7 +790,7 @@ class VoterFile(models.Model):
         existing_voter.save()
 
     if election.use_voter_aliases:
-      voter_alias_integers = range(last_alias_num+1, last_alias_num+1+num_voters)
+      voter_alias_integers = list(range(last_alias_num+1, last_alias_num+1+num_voters))
       random.shuffle(voter_alias_integers)
       for i, voter in enumerate(new_voters):
         voter.alias = 'V%s' % voter_alias_integers[i]
@@ -814,8 +828,7 @@ class Voter(HeliosModel):
   alias = models.CharField(max_length = 100, null=True)
   
   # we keep a copy here for easy tallying
-  vote = LDObjectField(type_hint = 'legacy/EncryptedVote',
-                       null=True)
+  vote = LDObjectField(type_hint = 'legacy/EncryptedVote', null=True)
   vote_hash = models.CharField(max_length = 100, null=True)
   cast_at = models.DateTimeField(auto_now_add=False, null=True)
 
@@ -838,7 +851,7 @@ class Voter(HeliosModel):
 
     # do we need to generate an alias?
     if election.use_voter_aliases:
-      heliosutils.lock_row(Election, election.id)
+      utils.lock_row(Election, election.id)
       alias_num = election.last_alias_num + 1
       voter.alias = "V%s" % alias_num
 
@@ -854,14 +867,14 @@ class Voter(HeliosModel):
     
     # the boolean check is not stupid, this is ternary logic
     # none means don't care if it's cast or not
-    if cast == True:
+    if cast is True:
       query = query.exclude(cast_at = None)
-    elif cast == False:
+    elif cast is False:
       query = query.filter(cast_at = None)
 
     # little trick to get around GAE limitation
     # order by uuid only when no inequality has been added
-    if cast == None or order_by == 'cast_at' or order_by =='-cast_at':
+    if cast is None or order_by == 'cast_at' or order_by == '-cast_at':
       query = query.order_by(order_by)
       
       # if we want the list after a certain UUID, add the inequality here
@@ -945,12 +958,12 @@ class Voter(HeliosModel):
       value_to_hash = self.voter_id
 
     try:
-      return utils.hash_b64(value_to_hash)
+      return hash_b64(value_to_hash)
     except:
       try:
-        return utils.hash_b64(value_to_hash.encode('latin-1'))
+        return hash_b64(value_to_hash.encode('latin-1'))
       except:
-        return utils.hash_b64(value_to_hash.encode('utf-8'))        
+        return hash_b64(value_to_hash.encode('utf-8'))        
 
   @property
   def voter_type(self):
@@ -970,7 +983,7 @@ class Voter(HeliosModel):
     if self.voter_password:
       raise Exception("password already exists")
     
-    self.voter_password = heliosutils.random_string(length, alphabet='abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789')
+    self.voter_password = utils.random_string(length, alphabet='abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789')
 
   def store_vote(self, cast_vote):
     # only store the vote if it's cast later than the current one
@@ -1035,9 +1048,9 @@ class CastVote(HeliosModel):
     """
     find a tiny version of the hash for a URL slug.
     """
-    safe_hash = self.vote_hash
-    for c in ['/', '+']:
-      safe_hash = safe_hash.replace(c,'')
+    safe_hash = self.vote_hash.decode() if isinstance(self.vote_hash, bytes) else self.vote_hash
+    for c in ['/', '+', '#']:
+      safe_hash = safe_hash.replace(c, '')
     
     length = 8
     while True:
@@ -1164,7 +1177,7 @@ class Trustee(HeliosModel):
     """
     # not saved yet?
     if not self.secret:
-      self.secret = heliosutils.random_string(12)
+      self.secret = utils.random_string(12)
       self.election.append_log("Trustee %s added" % self.name)
       
     super(Trustee, self).save(*args, **kwargs)
diff --git a/helios/security.py b/helios/security.py
index 88dc6754d0bea4e046b6a3e2bb3f8e7c13bb63b5..2f0852d95eb92485c155f359463162b665c1e00a 100644
--- a/helios/security.py
+++ b/helios/security.py
@@ -4,21 +4,19 @@ Helios Security -- mostly access control
 Ben Adida (ben@adida.net)
 """
 
+import urllib.parse
 # nicely update the wrapper function
 from functools import update_wrapper
 
-from django.urls import reverse
+from django.conf import settings
 from django.core.exceptions import PermissionDenied
 from django.http import Http404
-from django.conf import settings
-
-from models import Voter, Trustee, Election
-from helios_auth.security import get_user
-
 from django.http import HttpResponseRedirect
-import urllib
+from django.urls import reverse
 
 import helios
+from helios_auth.security import get_user
+from .models import Voter, Trustee, Election
 
 
 class HSTSMiddleware:
@@ -45,7 +43,7 @@ def get_voter(request, user, election):
   return the current voter
   """
   voter = None
-  if request.session.has_key('CURRENT_VOTER_ID'):
+  if 'CURRENT_VOTER_ID' in request.session:
     voter = Voter.objects.get(id=request.session['CURRENT_VOTER_ID'])
     if voter.election != election:
       voter = None
@@ -59,7 +57,7 @@ def get_voter(request, user, election):
 # a function to check if the current user is a trustee
 HELIOS_TRUSTEE_UUID = 'helios_trustee_uuid'
 def get_logged_in_trustee(request):
-  if request.session.has_key(HELIOS_TRUSTEE_UUID):
+  if HELIOS_TRUSTEE_UUID in request.session:
     return Trustee.get_by_uuid(request.session[HELIOS_TRUSTEE_UUID])
   else:
     return None
@@ -72,26 +70,26 @@ def set_logged_in_trustee(request, trustee):
 #
 def do_election_checks(election, props):
   # frozen
-  if props.has_key('frozen'):
+  if 'frozen' in props:
     frozen = props['frozen']
   else:
     frozen = None
   
   # newvoters (open for registration)
-  if props.has_key('newvoters'):
+  if 'newvoters' in props:
     newvoters = props['newvoters']
   else:
     newvoters = None
   
   # frozen check
-  if frozen != None:
+  if frozen is not None:
     if frozen and not election.frozen_at:
       raise PermissionDenied()
     if not frozen and election.frozen_at:
       raise PermissionDenied()
     
   # open for new voters check
-  if newvoters != None:
+  if newvoters is not None:
     if election.can_add_voters() != newvoters:
       raise PermissionDenied()
 
@@ -120,10 +118,10 @@ def election_view(**checks):
 
       # if private election, only logged in voters
       if election.private_p and not checks.get('allow_logins',False):
-        from views import password_voter_login
+        from .views import password_voter_login
         if not user_can_see_election(request, election):
           return_url = request.get_full_path()
-          return HttpResponseRedirect("%s?%s" % (reverse(password_voter_login, args=[election.uuid]), urllib.urlencode({
+          return HttpResponseRedirect("%s?%s" % (reverse(password_voter_login, args=[election.uuid]), urllib.parse.urlencode({
                   'return_url' : return_url
                   })))
     
@@ -158,11 +156,13 @@ def user_can_see_election(request, election):
     return True
 
   # then this user has to be a voter
-  return (get_voter(request, user, election) != None)
+  return get_voter(request, user, election) is not None
+
 
 def api_client_can_admin_election(api_client, election):
-  return election.api_client == api_client and api_client != None
-  
+  return election.api_client == api_client and api_client is not None
+
+
 # decorator for checking election admin access, and some properties of the election
 # frozen - is the election frozen
 # newvoters - does the election accept new voters
diff --git a/helios/stats_views.py b/helios/stats_views.py
index 5e71a32e8dca7cf20a191aeffe9cc8dc5bda3492..e506637fa7b8ba224bf96ce6a544758dbdaacd70 100644
--- a/helios/stats_views.py
+++ b/helios/stats_views.py
@@ -12,8 +12,8 @@ from django.http import HttpResponseRedirect
 from helios import tasks, url_names
 from helios.models import CastVote, Election
 from helios_auth.security import get_user
-from security import PermissionDenied
-from view_utils import render_template
+from .security import PermissionDenied
+from .view_utils import render_template
 
 
 def require_admin(request):
diff --git a/helios/tasks.py b/helios/tasks.py
index 8610097182707b2aa40abc68e79c148fa664b19d..a1079c1d33c321747d05d5d8b317015f7c928ba1 100644
--- a/helios/tasks.py
+++ b/helios/tasks.py
@@ -8,9 +8,9 @@ import copy
 from celery import shared_task
 from celery.utils.log import get_logger
 
-import signals
-from models import CastVote, Election, Voter, VoterFile
-from view_utils import render_template_raw
+from . import signals
+from .models import CastVote, Election, Voter, VoterFile
+from .view_utils import render_template_raw
 
 
 @shared_task
diff --git a/helios/tests.py b/helios/tests.py
index 157ce2823da9cc332790cea5561cd11d8704730d..60103c8d3c9b9aa4193d71ea9156d804f313da64 100644
--- a/helios/tests.py
+++ b/helios/tests.py
@@ -3,11 +3,12 @@ Unit Tests for Helios
 """
 
 import datetime
+import logging
 import re
-import urllib
+import uuid
+from urllib.parse import urlencode
 
 import django_webtest
-import uuid
 from django.conf import settings
 from django.core import mail
 from django.core.files import File
@@ -53,45 +54,46 @@ class ElectionModelTests(TestCase):
         self.assertTrue(self.created_p)
 
         # should have a creation time
-        self.assertNotEquals(self.election.created_at, None)
+        self.assertNotEqual(self.election.created_at, None)
         self.assertTrue(self.election.created_at < datetime.datetime.utcnow())
 
     def test_find_election(self):
         election = models.Election.get_by_user_as_admin(self.user)[0]
-        self.assertEquals(self.election, election)
+        self.assertEqual(self.election, election)
 
         election = models.Election.get_by_uuid(self.election.uuid)
-        self.assertEquals(self.election, election)
+        self.assertEqual(self.election, election)
 
         election = models.Election.get_by_short_name(self.election.short_name)
-        self.assertEquals(self.election, election)
+        self.assertEqual(self.election, election)
         
     def test_setup_trustee(self):
         self.setup_trustee()
-        self.assertEquals(self.election.num_trustees, 1)
+        self.assertEqual(self.election.num_trustees, 1)
 
     def test_add_voters_file(self):
         election = self.election
 
         FILE = "helios/fixtures/voter-file.csv"
-        vf = models.VoterFile.objects.create(election = election, voter_file = File(open(FILE), "voter_file.css"))
-        vf.process()
+        with open(FILE, 'r', encoding='utf-8') as f:
+            vf = models.VoterFile.objects.create(election = election, voter_file = File(f, "voter_file.css"))
+            vf.process()
 
         # make sure that we stripped things correctly
         voter = election.voter_set.get(voter_login_id = 'benadida5')
-        self.assertEquals(voter.voter_email, 'ben5@adida.net')
-        self.assertEquals(voter.voter_name, 'Ben5 Adida')
+        self.assertEqual(voter.voter_email, 'ben5@adida.net')
+        self.assertEqual(voter.voter_name, 'Ben5 Adida')
 
     def test_check_issues_before_freeze(self):
         # should be three issues: no trustees, and no questions, and no voters
         issues = self.election.issues_before_freeze
-        self.assertEquals(len(issues), 3)
+        self.assertEqual(len(issues), 3)
 
         self.setup_questions()
 
         # should be two issues: no trustees, and no voters
         issues = self.election.issues_before_freeze
-        self.assertEquals(len(issues), 2)
+        self.assertEqual(len(issues), 2)
 
         self.election.questions = None
 
@@ -99,7 +101,7 @@ class ElectionModelTests(TestCase):
 
         # should be two issues: no questions, and no voters
         issues = self.election.issues_before_freeze
-        self.assertEquals(len(issues), 2)
+        self.assertEqual(len(issues), 2)
         
         self.setup_questions()
 
@@ -107,7 +109,7 @@ class ElectionModelTests(TestCase):
         self.setup_openreg()
 
         issues = self.election.issues_before_freeze
-        self.assertEquals(len(issues), 0)
+        self.assertEqual(len(issues), 0)
         
     def test_helios_trustee(self):
         self.election.generate_trustee(views.ELGAMAL_PARAMS)
@@ -115,7 +117,7 @@ class ElectionModelTests(TestCase):
         self.assertTrue(self.election.has_helios_trustee())
 
         trustee = self.election.get_helios_trustee()
-        self.assertNotEquals(trustee, None)
+        self.assertNotEqual(trustee, None)
 
     def test_log(self):
         LOGS = ["testing 1", "testing 2", "testing 3"]
@@ -126,7 +128,7 @@ class ElectionModelTests(TestCase):
         pulled_logs = [l.log for l in self.election.get_log().all()]
         pulled_logs.reverse()
 
-        self.assertEquals(LOGS,pulled_logs)
+        self.assertEqual(LOGS,pulled_logs)
 
     def test_eligibility(self):
         self.election.eligibility = [{'auth_system': self.user.user_type}]
@@ -137,7 +139,7 @@ class ElectionModelTests(TestCase):
         # what about after saving?
         self.election.save()
         e = models.Election.objects.get(uuid = self.election.uuid)
-        self.assertEquals(e.eligibility, [{'auth_system': self.user.user_type}])
+        self.assertEqual(e.eligibility, [{'auth_system': self.user.user_type}])
 
         self.election.openreg = True
 
@@ -150,6 +152,13 @@ class ElectionModelTests(TestCase):
     def test_facebook_eligibility(self):
         self.election.eligibility = [{'auth_system': 'facebook', 'constraint':[{'group': {'id': '123', 'name':'Fake Group'}}]}]
 
+        import settings
+        fb_enabled = 'facebook' in settings.AUTH_ENABLED_SYSTEMS
+        if not fb_enabled:
+            logging.error("'facebook' not enabled for auth, cannot its constraints.")
+            self.assertFalse(self.election.user_eligible_p(self.fb_user))
+            return
+
         # without openreg, this should be false
         self.assertFalse(self.election.user_eligible_p(self.fb_user))
         
@@ -166,7 +175,7 @@ class ElectionModelTests(TestCase):
         self.assertTrue(self.election.user_eligible_p(self.fb_user))
 
         # also check that eligibility_category_id does the right thing
-        self.assertEquals(self.election.eligibility_category_id('facebook'), '123')
+        self.assertEqual(self.election.eligibility_category_id('facebook'), '123')
 
     def test_freeze(self):
         # freezing without trustees and questions, no good
@@ -194,7 +203,7 @@ class ElectionModelTests(TestCase):
     def test_voter_registration(self):
         # before adding a voter
         voters = models.Voter.get_by_election(self.election)
-        self.assertEquals(0, len(voters))
+        self.assertEqual(0, len(voters))
 
         # make sure no voter yet
         voter = models.Voter.get_by_election_and_user(self.election, self.user)
@@ -202,7 +211,7 @@ class ElectionModelTests(TestCase):
 
         # make sure no voter at all across all elections
         voters = models.Voter.get_by_user(self.user)
-        self.assertEquals(0, len(voters))
+        self.assertEqual(0, len(voters))
 
         # register the voter
         voter = models.Voter.register_user_in_election(self.user, self.election)
@@ -212,17 +221,17 @@ class ElectionModelTests(TestCase):
 
         self.assertIsNotNone(voter)
         self.assertIsNotNone(voter_2)
-        self.assertEquals(voter, voter_2)
+        self.assertEqual(voter, voter_2)
 
         # make sure voter is there in this call too
         voters = models.Voter.get_by_user(self.user)
-        self.assertEquals(1, len(voters))
-        self.assertEquals(voter, voters[0])
+        self.assertEqual(1, len(voters))
+        self.assertEqual(voter, voters[0])
 
         voter_2 = models.Voter.get_by_election_and_uuid(self.election, voter.uuid)
-        self.assertEquals(voter, voter_2)
+        self.assertEqual(voter, voter_2)
 
-        self.assertEquals(voter.user, self.user)
+        self.assertEqual(voter.user, self.user)
 
 
 
@@ -241,13 +250,13 @@ class VoterModelTests(TestCase):
         v.save()
         
         # password has been generated!
-        self.assertFalse(v.voter_password == None)
+        self.assertFalse(v.voter_password is None)
 
         # can't generate passwords twice
         self.assertRaises(Exception, lambda: v.generate_password())
         
         # check that you can get at the voter user structure
-        self.assertEquals(v.get_user().user_id, v.voter_email)
+        self.assertEqual(v.get_user().user_id, v.voter_email)
 
 
 class CastVoteModelTests(TestCase):
@@ -289,7 +298,7 @@ class DatatypeTests(TestCase):
             'B' : '234324243'}
         ld_obj = datatypes.LDObject.fromDict(original_dict, type_hint = 'legacy/EGZKProofCommitment')
 
-        self.assertEquals(original_dict, ld_obj.toDict())
+        self.assertEqual(original_dict, ld_obj.toDict())
         
         
         
@@ -303,9 +312,8 @@ class DataFormatBlackboxTests(object):
         self.election = models.Election.objects.all()[0]
 
     def assertEqualsToFile(self, response, file_path):
-        expected = open(file_path)
-        self.assertEquals(response.content, expected.read())
-        expected.close()
+        with open(file_path) as expected:
+            self.assertEqual(response.content, expected.read().encode('utf-8'))
 
     def test_election(self):
         response = self.client.get("/helios/elections/%s" % self.election.uuid, follow=False)
@@ -349,23 +357,27 @@ class LegacyElectionBlackboxTests(DataFormatBlackboxTests, TestCase):
 
 class WebTest(django_webtest.WebTest):
     def assertStatusCode(self, response, status_code):
-        if hasattr(response, 'status_code'):
-            assert response.status_code == status_code, response.status_code
+        actual_code = response.status_code if hasattr(response, 'status_code') else response.status_int
+        if isinstance(status_code, (list, tuple)):
+            assert actual_code in status_code, "%s instad of %s" % (actual_code, status_code)
         else:
-            assert response.status_int == status_code, response.status_int
+            assert actual_code == status_code, "%s instad of %s" % (actual_code, status_code)
 
 
-    def assertRedirects(self, response, url):
+    def assertRedirects(self, response, url=None):
         """
         reimplement this in case it's a WebOp response
         and it seems to be screwing up in a few places too
         thus the localhost exception
         """
+        self.assertStatusCode(response, (301, 302))
+        location = None
         if hasattr(response, 'location'):
-            assert url in response.location, response.location
+            location = response.location
         else:
-            assert url in response['location'], response['location']
-        self.assertStatusCode(response, 302)
+            location = response['location']
+        if url is not None:
+            assert url in location, location
         #return super(django_webtest.WebTest, self).assertRedirects(response, url)
         #assert url in response.location, "redirected to %s instead of %s" % (response.location, url)
 
@@ -374,11 +386,17 @@ class WebTest(django_webtest.WebTest):
         self.assertStatusCode(response, 200)
 
         if hasattr(response, "testbody"):
-            assert text in response.testbody, "missing text %s" % text
+            t = response.testbody
         elif hasattr(response, "body"):
-            assert text in response.body, "missing text %s" % text
+            t = response.body
         else:
-            assert text in response.content, "missing text %s" % text
+            t = response.content
+
+        if isinstance(text, bytes):
+            text = text.decode()
+        if isinstance(t, bytes):
+            t = t.decode()
+        assert text in t, "missing text %s" % text
 
 
 ##
@@ -418,7 +436,7 @@ class ElectionBlackboxTests(WebTest):
 
     def test_election_params(self):
         response = self.client.get("/helios/elections/params")
-        self.assertEquals(response.content, views.ELGAMAL_PARAMS_LD_OBJECT.serialize())
+        self.assertEqual(response.content, views.ELGAMAL_PARAMS_LD_OBJECT.serialize().encode('utf-8'))
 
     def test_election_404(self):
         response = self.client.get("/helios/elections/foobar")
@@ -430,15 +448,15 @@ class ElectionBlackboxTests(WebTest):
 
     def test_get_election_shortcut(self):
         response = self.client.get("/helios/e/%s" % self.election.short_name, follow=True)
-        self.assertContains(response, self.election.description)
+        self.assertContains(response, self.election.description_bleached)
         
     def test_get_election_raw(self):
         response = self.client.get("/helios/elections/%s" % self.election.uuid, follow=False)
-        self.assertEquals(response.content, self.election.toJSON())
+        self.assertEqual(response.content, self.election.toJSON().encode('utf-8'))
     
     def test_get_election(self):
         response = self.client.get("/helios/elections/%s/view" % self.election.uuid, follow=False)
-        self.assertContains(response, self.election.description)
+        self.assertContains(response, self.election.description_bleached)
 
     def test_get_election_questions(self):
         response = self.client.get("/helios/elections/%s/questions" % self.election.uuid, follow=False)
@@ -460,7 +478,7 @@ class ElectionBlackboxTests(WebTest):
 
     def test_get_election_voters_raw(self):
         response = self.client.get("/helios/elections/%s/voters/" % self.election.uuid, follow=False)
-        self.assertEquals(len(utils.from_json(response.content)), self.election.num_voters)
+        self.assertEqual(len(response.json()), self.election.num_voters)
         
     def test_election_creation_not_logged_in(self):
         response = self.client.post("/helios/elections/new", {
@@ -489,7 +507,7 @@ class ElectionBlackboxTests(WebTest):
         self.assertRedirects(response, "/helios/elections/%s/view" % self.election.uuid)
 
         new_election = models.Election.objects.get(uuid = self.election.uuid)
-        self.assertEquals(new_election.short_name, self.election.short_name + "-2")
+        self.assertEqual(new_election.short_name, self.election.short_name + "-2")
 
     def test_get_election_stats(self):
         self.setup_login(from_scratch=True, user_id='mccio@github.com', user_type='google')
@@ -533,10 +551,11 @@ class ElectionBlackboxTests(WebTest):
         full_election_params.update(election_params or {})
 
         response = self.client.post("/helios/elections/new", full_election_params)
+        self.assertRedirects(response)
 
         # we are redirected to the election, let's extract the ID out of the URL
-        election_id = re.search('/elections/([^/]+)/', str(response['Location']))
-        self.assertIsNotNone(election_id, "Election id not found in redirect: %s" % str(response['Location']))
+        election_id = re.search('/elections/([^/]+)/', str(response['location']))
+        self.assertIsNotNone(election_id, "Election id not found in redirect: %s" % str(response['location']))
         election_id = election_id.group(1)
 
         # helios is automatically added as a trustee
@@ -569,7 +588,7 @@ class ElectionBlackboxTests(WebTest):
         # and we want to check that there are now voters
         response = self.client.get("/helios/elections/%s/voters/" % election_id)
         NUM_VOTERS = 4
-        self.assertEquals(len(utils.from_json(response.content)), NUM_VOTERS)
+        self.assertEqual(len(response.json()), NUM_VOTERS)
 
         # let's get a single voter
         single_voter = models.Election.objects.get(uuid = election_id).voter_set.all()[0]
@@ -602,7 +621,7 @@ class ElectionBlackboxTests(WebTest):
                 })
         self.assertRedirects(response, "/helios/elections/%s/view" % election_id)
         num_messages_after = len(mail.outbox)
-        self.assertEquals(num_messages_after - num_messages_before, NUM_VOTERS)
+        self.assertEqual(num_messages_after - num_messages_before, NUM_VOTERS)
 
         email_message = mail.outbox[num_messages_before]
         assert "your password" in email_message.subject, "bad subject in email"
@@ -613,7 +632,7 @@ class ElectionBlackboxTests(WebTest):
 
         # now log out as administrator
         self.clear_login()
-        self.assertEquals(self.client.session.has_key('user'), False)
+        self.assertEqual('user' in self.client.session, False)
 
         # return the voter username and password to vote
         return election_id, username, password
@@ -629,11 +648,11 @@ class ElectionBlackboxTests(WebTest):
 
         # parse it as an encrypted vote with randomness, and make sure randomness is there
         the_ballot = utils.from_json(response.testbody)
-        assert the_ballot['answers'][0].has_key('randomness'), "no randomness"
+        assert 'randomness' in the_ballot['answers'][0], "no randomness"
         assert len(the_ballot['answers'][0]['randomness']) == 2, "not enough randomness"
         
         # parse it as an encrypted vote, and re-serialize it
-        ballot = datatypes.LDObject.fromDict(utils.from_json(response.testbody), type_hint='legacy/EncryptedVote')
+        ballot = datatypes.LDObject.fromDict(the_ballot, type_hint='legacy/EncryptedVote')
         encrypted_vote = ballot.serialize()
         
         # cast the ballot
@@ -661,7 +680,7 @@ class ElectionBlackboxTests(WebTest):
             # confirm the vote, now with the actual form
             cast_form = cast_confirm_page.form
         
-            if 'status_update' in cast_form.fields.keys():
+            if 'status_update' in list(cast_form.fields.keys()):
                 cast_form['status_update'] = False
             response = cast_form.submit()
 
@@ -711,7 +730,7 @@ class ElectionBlackboxTests(WebTest):
         self.assertRedirects(response, "/helios/elections/%s/view" % election_id)
 
         # should trigger helios decryption automatically
-        self.assertNotEquals(models.Election.objects.get(uuid=election_id).get_helios_trustee().decryption_proofs, None)
+        self.assertNotEqual(models.Election.objects.get(uuid=election_id).get_helios_trustee().decryption_proofs, None)
 
         # combine decryptions
         response = self.client.post("/helios/elections/%s/combine_decryptions" % election_id, {
@@ -732,7 +751,7 @@ class ElectionBlackboxTests(WebTest):
 
         # check that tally matches
         response = self.client.get("/helios/elections/%s/result" % election_id)
-        self.assertEquals(utils.from_json(response.content), [[0,1]])
+        self.assertEqual(response.json(), [[0,1]])
         
     def test_do_complete_election(self):
         election_id, username, password = self._setup_complete_election()
@@ -761,7 +780,7 @@ class ElectionBlackboxTests(WebTest):
         response = self.app.get("/helios/elections/%s/view" % election_id)
 
         # ensure it redirects
-        self.assertRedirects(response, "/helios/elections/%s/password_voter_login?%s" % (election_id, urllib.urlencode({"return_url": "/helios/elections/%s/view" % election_id})))
+        self.assertRedirects(response, "/helios/elections/%s/password_voter_login?%s" % (election_id, urlencode({"return_url": "/helios/elections/%s/view" % election_id})))
 
         login_form = response.follow().form
 
diff --git a/helios/urls.py b/helios/urls.py
index 6d278a9d195f17b169f4d7f126ef32ed3c012c61..9183eedd1fdc4e2c7fd89535b557d972106fc216 100644
--- a/helios/urls.py
+++ b/helios/urls.py
@@ -1,8 +1,7 @@
 # -*- coding: utf-8 -*-
 from django.conf.urls import url, include
 
-import url_names as names
-import views
+from . import views, url_names as names
 
 urlpatterns = [
   url(r'^autologin$', views.admin_autologin),
diff --git a/helios/utils.py b/helios/utils.py
index 03095a75074b55123571991a8e35cf0924248431..0080c5f0a3d979d0efd6854f9e7304ec62bd43d1 100644
--- a/helios/utils.py
+++ b/helios/utils.py
@@ -5,15 +5,17 @@ Ben Adida - ben@adida.net
 2005-04-11
 """
 
-import urllib, re, datetime, string
+import datetime
+import re
+import string
+import urllib.parse
 
+from django.conf import settings
+
+from helios.crypto.utils import random
 # utils from helios_auth, too
 from helios_auth.utils import *
 
-from django.conf import settings
-  
-import random, logging
-
 
 def split_by_length(str, length, rejoin_with=None):
   """
@@ -38,7 +40,7 @@ def urlencode(str):
     if not str:
         return ""
 
-    return urllib.quote(str)
+    return urllib.parse.quote(str)
 
 def urlencodeall(str):
     """
@@ -53,11 +55,11 @@ def urldecode(str):
     if not str:
         return ""
 
-    return urllib.unquote(str)
+    return urllib.parse.unquote(str)
 
 def dictToURLParams(d):
   if d:
-    return '&'.join([i + '=' + urlencode(v) for i,v in d.items()])
+    return '&'.join([i + '=' + urlencode(v) for i,v in list(d.items())])
   else:
     return None
 ##
@@ -87,31 +89,28 @@ def xss_strip_all_tags(s):
         if text[:2] == "&#":
             try:
                 if text[:3] == "&#x":
-                    return unichr(int(text[3:-1], 16))
+                    return chr(int(text[3:-1], 16))
                 else:
-                    return unichr(int(text[2:-1]))
+                    return chr(int(text[2:-1]))
             except ValueError:
                 pass
         elif text[:1] == "&":
-            import htmlentitydefs
-            entity = htmlentitydefs.entitydefs.get(text[1:-1])
+            import html.entities
+            entity = html.entities.entitydefs.get(text[1:-1])
             if entity:
                 if entity[:2] == "&#":
                     try:
-                        return unichr(int(entity[2:-1]))
+                        return chr(int(entity[2:-1]))
                     except ValueError:
                         pass
                 else:
-                    return unicode(entity, "iso-8859-1")
+                    return str(entity, "iso-8859-1")
         return text # leave as is
         
     return re.sub("(?s)<[^>]*>|&#?\w+;", fixup, s)
     
- 
-random.seed()
 
 def random_string(length=20, alphabet=None):
-    random.seed()
     ALPHABET = alphabet or 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
     r_string = ''
     for i in range(length):
@@ -131,7 +130,7 @@ def get_prefix():
 ##
 
 def string_to_datetime(str, fmt="%Y-%m-%d %H:%M"):
-  if str == None:
+  if str is None:
     return None
 
   return datetime.datetime.strptime(str, fmt)
diff --git a/helios/view_utils.py b/helios/view_utils.py
index 26da7046c50d1f6c8d62e256cd67175f76cf141f..1c8c9a9011e44404633dcfd17e5706ce86bdb2b3 100644
--- a/helios/view_utils.py
+++ b/helios/view_utils.py
@@ -12,7 +12,7 @@ from django.template import loader
 from functools import update_wrapper
 
 import helios
-import utils
+from . import utils
 from helios_auth.security import get_user
 
 ##
@@ -32,7 +32,7 @@ def prepare_vars(request, values):
   vars_with_user['user'] = get_user(request)
 
   # csrf protection
-  if request.session.has_key('csrf_token'):
+  if 'csrf_token' in request.session:
     vars_with_user['csrf_token'] = request.session['csrf_token']
 
   vars_with_user['utils'] = utils
@@ -67,7 +67,7 @@ def render_template_raw(request, template_name, values=None):
 
 
 def render_json(json_txt):
-  return HttpResponse(json_txt, "application/json")
+  return HttpResponse(utils.to_json(json_txt), content_type="application/json")
 
 
 # decorator
@@ -79,8 +79,8 @@ def return_json(func):
     def convert_to_json(self, *args, **kwargs):
       return_val = func(self, *args, **kwargs)
       try:
-        return render_json(utils.to_json(return_val))
-      except Exception, e:
+        return render_json(return_val)
+      except Exception as e:
         import logging
         logging.error("problem with serialization: " + str(return_val) + " / " + str(e))
         raise e
diff --git a/helios/views.py b/helios/views.py
index 6d049a8c732daabb74e438dfad6286f9f6a62b29..26a58519d5d803b4b6cb0e8edc957d4f968e81de 100644
--- a/helios/views.py
+++ b/helios/views.py
@@ -5,51 +5,46 @@ Helios Django Views
 Ben Adida (ben@adida.net)
 """
 
-from django.urls import reverse
-from django.core.paginator import Paginator
+import base64
+import datetime
+import logging
+import os
+import uuid
+from urllib.parse import urlencode
+
 from django.core.exceptions import PermissionDenied
-from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseForbidden
+from django.core.paginator import Paginator
 from django.db import transaction, IntegrityError
-
+from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseForbidden
+from django.urls import reverse
 from validate_email import validate_email
 
-import urllib, os, base64
-
-from crypto import algs, electionalgs, elgamal
-from crypto import utils as cryptoutils
-from workflows import homomorphic
+import helios_auth.url_names as helios_auth_urls
 from helios import utils, VOTERS_EMAIL, VOTERS_UPLOAD, url_names
-from view_utils import SUCCESS, FAILURE, return_json, render_template, render_template_raw
-
-from helios_auth.security import check_csrf, login_required, get_user, save_in_session_across_logouts
+from helios_auth import views as auth_views
 from helios_auth.auth_systems import AUTH_SYSTEMS, can_list_categories
 from helios_auth.models import AuthenticationExpired
-import helios_auth.url_names as helios_auth_urls
-
-from helios_auth import views as auth_views
-
-import tasks
-
-from security import (election_view, election_admin,
-                      trustee_check, set_logged_in_trustee,
-                      can_create_election, user_can_see_election, get_voter,
-                      user_can_admin_election, user_can_feature_election)
-
-import uuid, datetime
-import logging
-
-from models import User, Election, CastVote, Voter, VoterFile, Trustee, AuditedBallot
-import datatypes
-
-import forms
+from helios_auth.security import check_csrf, login_required, get_user, save_in_session_across_logouts
+from . import datatypes
+from . import forms
+from . import tasks
+from .crypto import algs, electionalgs, elgamal
+from .crypto import utils as cryptoutils
+from .models import User, Election, CastVote, Voter, VoterFile, Trustee, AuditedBallot
+from .security import (election_view, election_admin,
+                       trustee_check, set_logged_in_trustee,
+                       can_create_election, user_can_see_election, get_voter,
+                       user_can_admin_election, user_can_feature_election)
+from .view_utils import SUCCESS, FAILURE, return_json, render_template, render_template_raw
+from .workflows import homomorphic
 
 # Parameters for everything
 ELGAMAL_PARAMS = elgamal.Cryptosystem()
 
 # trying new ones from OlivierP
-ELGAMAL_PARAMS.p = 16328632084933010002384055033805457329601614771185955389739167309086214800406465799038583634953752941675645562182498120750264980492381375579367675648771293800310370964745767014243638518442553823973482995267304044326777047662957480269391322789378384619428596446446984694306187644767462460965622580087564339212631775817895958409016676398975671266179637898557687317076177218843233150695157881061257053019133078545928983562221396313169622475509818442661047018436264806901023966236718367204710755935899013750306107738002364137917426595737403871114187750804346564731250609196846638183903982387884578266136503697493474682071L
-ELGAMAL_PARAMS.q = 61329566248342901292543872769978950870633559608669337131139375508370458778917L
-ELGAMAL_PARAMS.g = 14887492224963187634282421537186040801304008017743492304481737382571933937568724473847106029915040150784031882206090286938661464458896494215273989547889201144857352611058572236578734319505128042602372864570426550855201448111746579871811249114781674309062693442442368697449970648232621880001709535143047913661432883287150003429802392229361583608686643243349727791976247247948618930423866180410558458272606627111270040091203073580238905303994472202930783207472394578498507764703191288249547659899997131166130259700604433891232298182348403175947450284433411265966789131024573629546048637848902243503970966798589660808533L
+ELGAMAL_PARAMS.p = 16328632084933010002384055033805457329601614771185955389739167309086214800406465799038583634953752941675645562182498120750264980492381375579367675648771293800310370964745767014243638518442553823973482995267304044326777047662957480269391322789378384619428596446446984694306187644767462460965622580087564339212631775817895958409016676398975671266179637898557687317076177218843233150695157881061257053019133078545928983562221396313169622475509818442661047018436264806901023966236718367204710755935899013750306107738002364137917426595737403871114187750804346564731250609196846638183903982387884578266136503697493474682071
+ELGAMAL_PARAMS.q = 61329566248342901292543872769978950870633559608669337131139375508370458778917
+ELGAMAL_PARAMS.g = 14887492224963187634282421537186040801304008017743492304481737382571933937568724473847106029915040150784031882206090286938661464458896494215273989547889201144857352611058572236578734319505128042602372864570426550855201448111746579871811249114781674309062693442442368697449970648232621880001709535143047913661432883287150003429802392229361583608686643243349727791976247247948618930423866180410558458272606627111270040091203073580238905303994472202930783207472394578498507764703191288249547659899997131166130259700604433891232298182348403175947450284433411265966789131024573629546048637848902243503970966798589660808533
 
 # object ready for serialization
 ELGAMAL_PARAMS_LD_OBJECT = datatypes.LDObject.instantiate(ELGAMAL_PARAMS, datatype='legacy/EGParams')
@@ -78,7 +73,7 @@ def user_reauth(request, user):
   # add a parameter to prevent it? Maybe.
   login_url = "%s%s?%s" % (settings.SECURE_URL_HOST,
                            reverse(helios_auth_urls.AUTH_START, args=[user.user_type]),
-                           urllib.urlencode({'return_url':
+                           urlencode({'return_url':
                                                request.get_full_path()}))
   return HttpResponseRedirect(login_url)
 
@@ -124,9 +119,9 @@ def election_shortcut(request, election_short_name):
 # a hidden view behind the shortcut that performs the actual perm check
 @election_view()
 def _election_vote_shortcut(request, election):
-  vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urllib.urlencode({'election_url' : reverse(url_names.election.ELECTION_HOME, args=[election.uuid])}))
+  vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urlencode({'election_url' : reverse(url_names.election.ELECTION_HOME, args=[election.uuid])}))
   
-  test_cookie_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urllib.urlencode({'continue_url' : vote_url}))
+  test_cookie_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urlencode({'continue_url' : vote_url}))
 
   return HttpResponseRedirect(test_cookie_url)
   
@@ -304,9 +299,9 @@ def one_election_view(request, election):
   election_badge_url = get_election_badge_url(election)
   status_update_message = None
 
-  vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urllib.urlencode({'election_url' : reverse(url_names.election.ELECTION_HOME, args=[election.uuid])}))
+  vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urlencode({'election_url' : reverse(url_names.election.ELECTION_HOME, args=[election.uuid])}))
 
-  test_cookie_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urllib.urlencode({'continue_url' : vote_url}))
+  test_cookie_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urlencode({'continue_url' : vote_url}))
   
   if user:
     voter = Voter.get_by_election_and_user(election, user)
@@ -329,13 +324,13 @@ def one_election_view(request, election):
   # status update message?
   if election.openreg:
     if election.voting_has_started:
-      status_update_message = u"Vote in %s" % election.name
+      status_update_message = "Vote in %s" % election.name
     else:
-      status_update_message = u"Register to vote in %s" % election.name
+      status_update_message = "Register to vote in %s" % election.name
 
   # result!
   if election.result:
-    status_update_message = u"Results are in for %s" % election.name
+    status_update_message = "Results are in for %s" % election.name
   
   trustees = Trustee.get_by_election(election)
 
@@ -353,20 +348,20 @@ def one_election_view(request, election):
 def test_cookie(request):
   continue_url = request.GET['continue_url']
   request.session.set_test_cookie()
-  next_url = "%s?%s" % (reverse(url_names.COOKIE_TEST_2), urllib.urlencode({'continue_url': continue_url}))
+  next_url = "%s?%s" % (reverse(url_names.COOKIE_TEST_2), urlencode({'continue_url': continue_url}))
   return HttpResponseRedirect(settings.SECURE_URL_HOST + next_url)  
 
 def test_cookie_2(request):
   continue_url = request.GET['continue_url']
 
   if not request.session.test_cookie_worked():
-    return HttpResponseRedirect(settings.SECURE_URL_HOST + ("%s?%s" % (reverse(url_names.COOKIE_NO), urllib.urlencode({'continue_url': continue_url}))))
+    return HttpResponseRedirect(settings.SECURE_URL_HOST + ("%s?%s" % (reverse(url_names.COOKIE_NO), urlencode({'continue_url': continue_url}))))
 
   request.session.delete_test_cookie()
   return HttpResponseRedirect(continue_url)  
 
 def nocookies(request):
-  retest_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urllib.urlencode({'continue_url' : request.GET['continue_url']}))
+  retest_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urlencode({'continue_url' : request.GET['continue_url']}))
   return render_template(request, 'nocookies', {'retest_url': retest_url})
 
 ##
@@ -496,10 +491,8 @@ def get_randomness(request, election):
   get some randomness to sprinkle into the sjcl entropy pool
   """
   return {
-    # back to urandom, it's fine
-    "randomness" : base64.b64encode(os.urandom(32))
-    #"randomness" : base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
-    }
+    "randomness" : base64.b64encode(os.urandom(32)).decode('utf-8')
+  }
 
 @election_view(frozen=True)
 @return_json
@@ -593,7 +586,7 @@ def password_voter_login(request, election):
         return one_election_cast_confirm(request, election.uuid)
       
     except Voter.DoesNotExist:
-      redirect_url = login_url + "?" + urllib.urlencode({
+      redirect_url = login_url + "?" + urlencode({
           'bad_voter_login' : '1',
           'return_url' : return_url
           })
@@ -601,7 +594,7 @@ def password_voter_login(request, election):
       return HttpResponseRedirect(settings.SECURE_URL_HOST + redirect_url)
   else:
     # bad form, bad voter login
-    redirect_url = login_url + "?" + urllib.urlencode({
+    redirect_url = login_url + "?" + urlencode({
         'bad_voter_login' : '1',
         'return_url' : return_url
         })
@@ -615,7 +608,7 @@ def one_election_cast_confirm(request, election):
   user = get_user(request)    
 
   # if no encrypted vote, the user is reloading this page or otherwise getting here in a bad way
-  if (not request.session.has_key('encrypted_vote')) or request.session['encrypted_vote'] == None:
+  if ('encrypted_vote' not in request.session) or request.session['encrypted_vote'] is None:
     return HttpResponseRedirect(settings.URL_HOST)
 
   # election not frozen or started
@@ -693,7 +686,7 @@ def one_election_cast_confirm(request, election):
 
     password_only = False
 
-    if auth_systems == None or 'password' in auth_systems:
+    if auth_systems is None or 'password' in auth_systems:
       show_password = True
       password_login_form = forms.VoterPasswordForm()
 
@@ -763,7 +756,7 @@ def one_election_cast_done(request, election):
     # only log out if the setting says so *and* we're dealing
     # with a site-wide voter. Definitely remove current_voter
     # checking that voter.user != None is needed because voter.user may now be None if voter is password only
-    if voter.user == user and voter.user != None:
+    if voter.user == user and voter.user is not None:
       logout = settings.LOGOUT_ON_CONFIRMATION
     else:
       logout = False
@@ -819,7 +812,7 @@ def one_election_bboard(request, election):
     order_by = 'alias'
 
   # if there's a specific voter
-  if request.GET.has_key('q'):
+  if 'q' in request.GET:
     # FIXME: figure out the voter by voter_id
     voters = []
   else:
@@ -843,7 +836,7 @@ def one_election_audited_ballots(request, election):
   UI to show election audited ballots
   """
   
-  if request.GET.has_key('vote_hash'):
+  if 'vote_hash' in request.GET:
     b = AuditedBallot.get(election, request.GET['vote_hash'])
     return HttpResponse(b.raw_vote, content_type="text/plain")
     
@@ -964,7 +957,7 @@ def one_election_copy(request, election):
     name = "Copy of " + election.name,
     election_type = election.election_type,
     private_p = election.private_p,
-    description = election.description,
+    description = election.description_bleached,
     questions = election.questions,
     eligibility = election.eligibility,
     openreg = election.openreg,
@@ -1082,14 +1075,14 @@ def one_election_compute_tally(request, election):
 
 @trustee_check
 def trustee_decrypt_and_prove(request, election, trustee):
-  if not _check_election_tally_type(election) or election.encrypted_tally == None:
+  if not _check_election_tally_type(election) or election.encrypted_tally is None:
     return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW,args=[election.uuid]))
     
   return render_template(request, 'trustee_decrypt_and_prove', {'election': election, 'trustee': trustee})
   
 @election_view(frozen=True)
 def trustee_upload_decryption(request, election, trustee_uuid):
-  if not _check_election_tally_type(election) or election.encrypted_tally == None:
+  if not _check_election_tally_type(election) or election.encrypted_tally is None:
     return FAILURE
 
   trustee = Trustee.get_by_election_and_uuid(election, trustee_uuid)
@@ -1130,7 +1123,7 @@ def release_result(request, election):
     election.save()
 
     if request.POST.get('send_email', ''):
-      return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_email, args=[election.uuid]),urllib.urlencode({'template': 'result'})))
+      return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_email, args=[election.uuid]),urlencode({'template': 'result'})))
     else:
       return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
 
@@ -1293,7 +1286,7 @@ def voters_upload(request, election):
       return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(voters_list_pretty, args=[election.uuid]))
     else:
       # we need to confirm
-      if request.FILES.has_key('voters_file'):
+      if 'voters_file' in request.FILES:
         voters_file = request.FILES['voters_file']
         voter_file_obj = election.add_voters_file(voters_file)
 
@@ -1314,7 +1307,7 @@ def voters_upload(request, election):
 
         return render_template(request, 'voters_upload_confirm', {'election': election, 'voters': voters, 'problems': problems})
       else:
-        return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_upload, args=[election.uuid]), urllib.urlencode({'e':'no voter file specified, try again'})))
+        return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_upload, args=[election.uuid]), urlencode({'e':'no voter file specified, try again'})))
 
 @election_admin()
 def voters_upload_cancel(request, election):
@@ -1471,9 +1464,9 @@ def ballot_list(request, election):
   and optionally take a after parameter.
   """
   limit = after = None
-  if request.GET.has_key('limit'):
+  if 'limit' in request.GET:
     limit = int(request.GET['limit'])
-  if request.GET.has_key('after'):
+  if 'after' in request.GET:
     after = datetime.datetime.strptime(request.GET['after'], '%Y-%m-%d %H:%M:%S')
     
   voters = Voter.get_by_election(election, cast=True, order_by='cast_at', limit=limit, after=after)
diff --git a/helios/widgets.py b/helios/widgets.py
index 9eff2f435bc1bf7e37cb8174a5bc1a6db12272f1..8e3f3e87d26a2798510593c74002e6b869a009dc 100644
--- a/helios/widgets.py
+++ b/helios/widgets.py
@@ -54,18 +54,18 @@ class SelectTimeWidget(Widget):
             self.meridiem_val = 'a.m.' # Default to Morning (A.M.)
         
         if hour_step and twelve_hr:
-            self.hours = range(1,13,hour_step) 
+            self.hours = list(range(1,13,hour_step)) 
         elif hour_step: # 24hr, with stepping.
-            self.hours = range(0,24,hour_step)
+            self.hours = list(range(0,24,hour_step))
         elif twelve_hr: # 12hr, no stepping
-            self.hours = range(1,13)
+            self.hours = list(range(1,13))
         else: # 24hr, no stepping
-            self.hours = range(0,24) 
+            self.hours = list(range(0,24)) 
 
         if minute_step:
-            self.minutes = range(0,60,minute_step)
+            self.minutes = list(range(0,60,minute_step))
         else:
-            self.minutes = range(0,60)
+            self.minutes = list(range(0,60))
 
     def render(self, name, value, attrs=None, renderer=None):
         try: # try to get time values from a datetime.time object (value)
@@ -77,7 +77,7 @@ class SelectTimeWidget(Widget):
                     self.meridiem_val = 'a.m.'
         except AttributeError:
             hour_val = minute_val = 0
-            if isinstance(value, basestring):
+            if isinstance(value, str):
                 match = RE_TIME.match(value)
                 if match:
                     time_groups = match.groups()
@@ -113,8 +113,8 @@ class SelectTimeWidget(Widget):
 
         # For times to get displayed correctly, the values MUST be converted to unicode
         # When Select builds a list of options, it checks against Unicode values
-        hour_val = u"%.2d" % hour_val
-        minute_val = u"%.2d" % minute_val
+        hour_val = "%.2d" % hour_val
+        minute_val = "%.2d" % minute_val
 
         hour_choices = [("%.2d"%i, "%.2d"%i) for i in self.hours]
         local_attrs = self.build_attrs({'id': self.hour_field % id_})
@@ -137,7 +137,7 @@ class SelectTimeWidget(Widget):
             select_html = Select(choices=meridiem_choices).render(self.meridiem_field % name, self.meridiem_val, local_attrs)
             output.append(select_html)
 
-        return mark_safe(u'\n'.join(output))
+        return mark_safe('\n'.join(output))
 
     def id_for_label(self, id_):
         return '%s_hour' % id_
@@ -179,7 +179,7 @@ class SplitSelectDateTimeWidget(MultiWidget):
     # See https://stackoverflow.com/questions/4324676/django-multiwidget-subclass-not-calling-decompress
     def value_from_datadict(self, data, files, name):
         if data.get(name, None) is None:
-            return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
+            return [widget.value_from_datadict(data, files, name ) for widget in self.widgets]
         return self.decompress(data.get(name, None))
 
     def decompress(self, value):
@@ -187,6 +187,23 @@ class SplitSelectDateTimeWidget(MultiWidget):
             return [value.date(), value.time().replace(microsecond=0)]
         return [None, None]
 
+    def compress(self, data_list):
+        """
+        Takes the values from the MultiWidget and passes them as a
+        list to this function. This function needs to compress the
+        list into a single object in order to be correctly rendered by the widget.
+        For instace, django.forms.widgets.SelectDateWidget.format_value(value)
+        expects a date object or a string, not a list.
+        This method was taken from helios/fields.py
+        """
+        if data_list:
+            import datetime
+            if not (data_list[0] and data_list[1]):
+                return None
+            return datetime.datetime.combine(*data_list)
+        return None
+
     def render(self, name, value, attrs=None, renderer=None):
+        value = self.compress(value)
         rendered_widgets = list(widget.render(name, value, attrs=attrs, renderer=renderer) for widget in self.widgets)
-        return u'<br/>'.join(rendered_widgets)
+        return '<br/>'.join(rendered_widgets)
diff --git a/helios/workflows/homomorphic.py b/helios/workflows/homomorphic.py
index c3c98eb715482af7421a90e42f3c23c74c71e12f..9c8039679bff10f9b0178fc16f1b27bafdbc16be 100644
--- a/helios/workflows/homomorphic.py
+++ b/helios/workflows/homomorphic.py
@@ -6,6 +6,7 @@ Ben Adida
 reworked 2011-01-09
 """
 
+import logging
 from helios.crypto import algs
 from . import WorkflowObject
 
@@ -68,10 +69,10 @@ class EncryptedAnswer(WorkflowObject):
         return False
 
       # compute homomorphic sum if needed
-      if max != None:
+      if max is not None:
         homomorphic_sum = choice * homomorphic_sum
     
-    if max != None:
+    if max is not None:
       # determine possible plaintexts for the sum
       sum_possible_plaintexts = self.generate_plaintexts(pk, min=min, max=max)
 
@@ -110,7 +111,7 @@ class EncryptedAnswer(WorkflowObject):
 
     # min and max for number of answers, useful later
     min_answers = 0
-    if question.has_key('min'):
+    if 'min' in question:
       min_answers = question['min']
     max_answers = question['max']
 
@@ -124,7 +125,7 @@ class EncryptedAnswer(WorkflowObject):
         num_selected_answers += 1
 
       # randomness and encryption
-      randomness[answer_num] = algs.Utils.random_mpz_lt(pk.q)
+      randomness[answer_num] = algs.random.mpz_lt(pk.q)
       choices[answer_num] = pk.encrypt_with_r(plaintexts[plaintext_index], randomness[answer_num])
       
       # generate proof
@@ -132,7 +133,7 @@ class EncryptedAnswer(WorkflowObject):
                                                 randomness[answer_num], algs.EG_disjunctive_challenge_generator)
                                                 
       # sum things up homomorphically if needed
-      if max_answers != None:
+      if max_answers is not None:
         homomorphic_sum = choices[answer_num] * homomorphic_sum
         randomness_sum = (randomness_sum + randomness[answer_num]) % pk.q
 
@@ -142,7 +143,7 @@ class EncryptedAnswer(WorkflowObject):
     if num_selected_answers < min_answers:
       raise Exception("Need to select at least %s answer(s)" % min_answers)
     
-    if max_answers != None:
+    if max_answers is not None:
       sum_plaintexts = cls.generate_plaintexts(pk, min=min_answers, max=max_answers)
     
       # need to subtract the min from the offset
@@ -160,7 +161,7 @@ class EncryptedVote(WorkflowObject):
   An encrypted ballot
   """
   def __init__(self):
-    self.encrypted_answers = None
+    self.encrypted_answers = []
 
   @property
   def datatype(self):
@@ -176,26 +177,37 @@ class EncryptedVote(WorkflowObject):
   answers = property(_answers_get, _answers_set)
 
   def verify(self, election):
-    # right number of answers
-    if len(self.encrypted_answers) != len(election.questions):
+    # correct number of answers
+    # noinspection PyUnresolvedReferences
+    n_answers = len(self.encrypted_answers) if self.encrypted_answers is not None else 0
+    n_questions = len(election.questions) if election.questions is not None else 0
+    if n_answers != n_questions:
+      logging.error(f"Incorrect number of answers ({n_answers}) vs questions ({n_questions})")
       return False
-    
+
     # check hash
-    if self.election_hash != election.hash:
-      # print "%s / %s " % (self.election_hash, election.hash)
+    # noinspection PyUnresolvedReferences
+    our_election_hash = self.election_hash if isinstance(self.election_hash, str) else self.election_hash.decode()
+    actual_election_hash = election.hash if isinstance(election.hash, str) else election.hash.decode()
+    if our_election_hash != actual_election_hash:
+      logging.error(f"Incorrect election_hash {our_election_hash} vs {actual_election_hash} ")
       return False
-      
+
     # check ID
-    if self.election_uuid != election.uuid:
+    # noinspection PyUnresolvedReferences
+    our_election_uuid = self.election_uuid if isinstance(self.election_uuid, str) else self.election_uuid.decode()
+    actual_election_uuid = election.uuid if isinstance(election.uuid, str) else election.uuid.decode()
+    if our_election_uuid != actual_election_uuid:
+      logging.error(f"Incorrect election_uuid {our_election_uuid} vs {actual_election_uuid} ")
       return False
-      
+
     # check proofs on all of answers
     for question_num in range(len(election.questions)):
       ea = self.encrypted_answers[question_num]
 
       question = election.questions[question_num]
       min_answers = 0
-      if question.has_key('min'):
+      if 'min' in question:
         min_answers = question['min']
         
       if not ea.verify(election.public_key, min=min_answers, max=question['max']):
diff --git a/helios_auth/__init__.py b/helios_auth/__init__.py
index ca245ff38f820db528afbed3a43d597bc8642b44..d5e23fa983fe0874f005481d1cddae929efde928 100644
--- a/helios_auth/__init__.py
+++ b/helios_auth/__init__.py
@@ -4,7 +4,7 @@ from django.conf import settings
 TEMPLATE_BASE = settings.AUTH_TEMPLATE_BASE or "helios_auth/templates/base.html"
 
 # enabled auth systems
-import auth_systems
-ENABLED_AUTH_SYSTEMS = settings.AUTH_ENABLED_AUTH_SYSTEMS or auth_systems.AUTH_SYSTEMS.keys()
-DEFAULT_AUTH_SYSTEM = settings.AUTH_DEFAULT_AUTH_SYSTEM or None
+from . import auth_systems
+ENABLED_AUTH_SYSTEMS = settings.AUTH_ENABLED_SYSTEMS or list(auth_systems.AUTH_SYSTEMS.keys())
+DEFAULT_AUTH_SYSTEM = settings.AUTH_DEFAULT_SYSTEM or None
 
diff --git a/helios_auth/auth_systems/__init__.py b/helios_auth/auth_systems/__init__.py
index 5a0e9233ba4df89067688283e90649aef4f1ae70..e9dc9763a371790a2574f199d4b341674135b8a2 100644
--- a/helios_auth/auth_systems/__init__.py
+++ b/helios_auth/auth_systems/__init__.py
@@ -1,22 +1,49 @@
+from django.conf import settings
+
+_enabled = settings.AUTH_ENABLED_SYSTEMS or None
+def _is_enabled(system):
+    return _enabled is None or system in _enabled
 
 AUTH_SYSTEMS = {}
 
-import twitter, password, cas, facebook, google, yahoo, linkedin, clever
-AUTH_SYSTEMS['twitter'] = twitter
-AUTH_SYSTEMS['linkedin'] = linkedin
-AUTH_SYSTEMS['password'] = password
-AUTH_SYSTEMS['cas'] = cas
-AUTH_SYSTEMS['facebook'] = facebook
-AUTH_SYSTEMS['google'] = google
-AUTH_SYSTEMS['yahoo'] = yahoo
-AUTH_SYSTEMS['clever'] = clever
+if _is_enabled('twitter'):
+    from . import twitter
+    AUTH_SYSTEMS['twitter'] = twitter
+
+if _is_enabled('linkedin'):
+    from . import linkedin
+    AUTH_SYSTEMS['linkedin'] = linkedin
+
+if _is_enabled('password'):
+    from . import password
+    AUTH_SYSTEMS['password'] = password
+
+if _is_enabled('cas'):
+    from . import cas
+    AUTH_SYSTEMS['cas'] = cas
+
+if _is_enabled('facebook'):
+    from . import facebook
+    AUTH_SYSTEMS['facebook'] = facebook
+
+if _is_enabled('google'):
+    from . import google
+    AUTH_SYSTEMS['google'] = google
+
+if _is_enabled('yahoo'):
+    from . import yahoo
+    AUTH_SYSTEMS['yahoo'] = yahoo
+
+if _is_enabled('clever'):
+    from . import clever
+    AUTH_SYSTEMS['clever'] = clever
 
 # not ready
 #import live
 #AUTH_SYSTEMS['live'] = live
 
 def can_check_constraint(auth_system):
-    return hasattr(AUTH_SYSTEMS[auth_system], 'check_constraint')
+    return auth_system in AUTH_SYSTEMS and hasattr(AUTH_SYSTEMS[auth_system], 'check_constraint')
 
 def can_list_categories(auth_system):
-    return hasattr(AUTH_SYSTEMS[auth_system], 'list_categories')
+    return auth_system in AUTH_SYSTEMS and hasattr(AUTH_SYSTEMS[auth_system], 'list_categories')
diff --git a/helios_auth/auth_systems/cas.py b/helios_auth/auth_systems/cas.py
index dd4216b5ecc2c3953892332fa49ae8a51b13eeff..21c86cfe73577021491bacfd1c20f58f36c94fe2 100644
--- a/helios_auth/auth_systems/cas.py
+++ b/helios_auth/auth_systems/cas.py
@@ -7,13 +7,14 @@ https://sp.princeton.edu/oit/sdp/CAS/Wiki%20Pages/Python.aspx
 
 import datetime
 import re
-import urllib
-import urllib2
+import urllib.parse
+import urllib.request
 import uuid
+from xml.etree import ElementTree
+
 from django.conf import settings
 from django.core.mail import send_mail
 from django.http import HttpResponseRedirect
-from xml.etree import ElementTree
 
 CAS_EMAIL_DOMAIN = "princeton.edu"
 CAS_URL= 'https://fed.princeton.edu/cas/'
@@ -42,17 +43,17 @@ def _get_service_url():
   
 def get_auth_url(request, redirect_url):
   request.session['cas_redirect_url'] = redirect_url
-  return CAS_URL + 'login?service=' + urllib.quote(_get_service_url())
+  return CAS_URL + 'login?service=' + urllib.parse.quote(_get_service_url())
 
 def get_user_category(user_id):
   theurl = CAS_ELIGIBILITY_URL % user_id
 
-  auth_handler = urllib2.HTTPBasicAuthHandler()
+  auth_handler = urllib.request.HTTPBasicAuthHandler()
   auth_handler.add_password(realm=CAS_ELIGIBILITY_REALM, uri= theurl, user= CAS_USERNAME, passwd = CAS_PASSWORD)
-  opener = urllib2.build_opener(auth_handler)
-  urllib2.install_opener(opener)
+  opener = urllib.request.build_opener(auth_handler)
+  urllib.request.install_opener(opener)
   
-  result = urllib2.urlopen(CAS_ELIGIBILITY_URL % user_id).read().strip()
+  result = urllib.request.urlopen(CAS_ELIGIBILITY_URL % user_id).read().strip()
   parsed_result = ElementTree.fromstring(result)
   return parsed_result.text
   
@@ -78,11 +79,11 @@ def get_saml_info(ticket):
   </soap-env:Envelope>
 """ % (uuid.uuid1(), datetime.datetime.utcnow().isoformat(), ticket)
 
-  url = CAS_SAML_VALIDATE_URL % urllib.quote(_get_service_url())
+  url = CAS_SAML_VALIDATE_URL % urllib.parse.quote(_get_service_url())
 
   # by virtue of having a body, this is a POST
-  req = urllib2.Request(url, saml_request)
-  raw_response = urllib2.urlopen(req).read()
+  req = urllib.request.Request(url, saml_request)
+  raw_response = urllib.request.urlopen(req).read()
 
   logging.info("RESP:\n%s\n\n" % raw_response)
 
@@ -130,8 +131,8 @@ def get_user_info(user_id):
   </soap-env:Envelope>
 """ % user_id
 
-  req = urllib2.Request(url, request_body, headers)
-  response = urllib2.urlopen(req).read()
+  req = urllib.request.Request(url, request_body, headers)
+  response = urllib.request.urlopen(req).read()
   
   # parse the result
   from xml.dom.minidom import parseString
@@ -149,12 +150,12 @@ def get_user_info(user_id):
 def get_user_info_special(ticket):
   # fetch the information from the CAS server
   val_url = CAS_URL + "validate" + \
-     '?service=' + urllib.quote(_get_service_url()) + \
-     '&ticket=' + urllib.quote(ticket)
-  r = urllib.urlopen(val_url).readlines() # returns 2 lines
+     '?service=' + urllib.parse.quote(_get_service_url()) + \
+     '&ticket=' + urllib.parse.quote(ticket)
+  r = urllib.request.urlopen(val_url).readlines() # returns 2 lines
 
   # success
-  if len(r) == 2 and re.match("yes", r[0]) != None:
+  if len(r) == 2 and re.match("yes", r[0]) is not None:
     netid = r[1].strip()
     
     category = get_user_category(netid)
@@ -212,7 +213,7 @@ def send_message(user_id, name, user_info, subject, body):
   else:
     email = "%s@%s" % (user_id, CAS_EMAIL_DOMAIN)
     
-  if user_info.has_key('name'):
+  if 'name' in user_info:
     name = user_info["name"]
   else:
     name = email
@@ -224,7 +225,7 @@ def send_message(user_id, name, user_info, subject, body):
 #
 
 def check_constraint(constraint, user):
-  if not user.info.has_key('category'):
+  if 'category' not in user.info:
     return False
   return constraint['year'] == user.info['category']
 
diff --git a/helios_auth/auth_systems/clever.py b/helios_auth/auth_systems/clever.py
index 498951f8dbf0e3d60f2460c83d580be0b703bb64..48648a0b07a55add763512be3beb457ed08bb298 100644
--- a/helios_auth/auth_systems/clever.py
+++ b/helios_auth/auth_systems/clever.py
@@ -4,12 +4,14 @@ Clever Authentication
 """
 
 import base64
+import urllib.parse
+
 import httplib2
-import json
-import urllib
 from django.conf import settings
 from oauth2client.client import OAuth2WebServerFlow, OAuth2Credentials
 
+from helios_auth import utils
+
 # some parameters to indicate that status updating is not possible
 STATUS_UPDATES = False
 
@@ -42,7 +44,7 @@ def get_user_info_after_auth(request):
   # do the POST manually, because OAuth2WebFlow can't do auth header for token exchange
   http = httplib2.Http(".cache")
   auth_header = "Basic %s" % base64.b64encode(settings.CLEVER_CLIENT_ID + ":" + settings.CLEVER_CLIENT_SECRET)
-  resp_headers, content = http.request("https://clever.com/oauth/tokens", "POST", urllib.urlencode({
+  resp_headers, content = http.request("https://clever.com/oauth/tokens", "POST", urllib.parse.urlencode({
         "code" : code,
         "grant_type": "authorization_code",
         "redirect_uri": redirect_uri
@@ -51,7 +53,7 @@ def get_user_info_after_auth(request):
         'Content-Type': "application/x-www-form-urlencoded"
       })
 
-  token_response = json.loads(content)
+  token_response = utils.from_json(content)
   access_token = token_response['access_token']
 
   # package the credentials
@@ -62,7 +64,7 @@ def get_user_info_after_auth(request):
   (resp_headers, content) = http.request("https://api.clever.com/me", "GET")
 
   # {"type":"student","data":{"id":"563395179f7408755c0006b7","district":"5633941748c07c0100000aac","type":"student","created":"2015-10-30T16:04:39.262Z","credentials":{"district_password":"eel7Thohd","district_username":"dianes10"},"dob":"1998-11-01T00:00:00.000Z","ell_status":"Y","email":"diane.s@example.org","gender":"F","grade":"9","hispanic_ethnicity":"Y","last_modified":"2015-10-30T16:04:39.274Z","location":{"zip":"11433"},"name":{"first":"Diane","last":"Schmeler","middle":"J"},"race":"Asian","school":"5633950c62fc41c041000005","sis_id":"738733110","state_id":"114327752","student_number":"738733110"},"links":[{"rel":"self","uri":"/me"},{"rel":"canonical","uri":"/v1.1/students/563395179f7408755c0006b7"},{"rel":"district","uri":"/v1.1/districts/5633941748c07c0100000aac"}]}
-  response = json.loads(content)
+  response = utils.from_json(content)
   
   user_id = response['data']['id']
   user_name = "%s %s" % (response['data']['name']['first'], response['data']['name']['last'])
@@ -70,7 +72,7 @@ def get_user_info_after_auth(request):
   user_district = response['data']['district']
   user_grade = response['data'].get('grade', None)
 
-  print content
+  print(content)
   
   # watch out, response also contains email addresses, but not sure whether thsoe are verified or not
   # so for email address we will only look at the id_token
@@ -100,7 +102,7 @@ def send_message(user_id, name, user_info, subject, body):
 #
 
 def check_constraint(constraint, user):
-  if not user.info.has_key('grade'):
+  if 'grade' not in user.info:
     return False
   return constraint['grade'] == user.info['grade']
 
diff --git a/helios_auth/auth_systems/facebook.py b/helios_auth/auth_systems/facebook.py
index 4c50c6594336f60c2999303c57088e6feeb77b4c..4810ec20494fef34aba4e5017d092c88e185c39c 100644
--- a/helios_auth/auth_systems/facebook.py
+++ b/helios_auth/auth_systems/facebook.py
@@ -2,8 +2,6 @@
 Facebook Authentication
 """
 
-import logging
-
 from django.conf import settings
 from django.core.mail import send_mail
 
@@ -12,7 +10,7 @@ API_KEY = settings.FACEBOOK_API_KEY
 API_SECRET = settings.FACEBOOK_API_SECRET
   
 #from facebookclient import Facebook
-import urllib, urllib2, cgi
+import urllib.request, urllib.error, urllib.parse
 
 # some parameters to indicate that status updating is possible
 STATUS_UPDATES = True
@@ -22,21 +20,21 @@ from helios_auth import utils
 
 def facebook_url(url, params):
   if params:
-    return "https://graph.facebook.com%s?%s" % (url, urllib.urlencode(params))
+    return "https://graph.facebook.com%s?%s" % (url, urllib.parse.urlencode(params))
   else:
     return "https://graph.facebook.com%s" % url
 
 def facebook_get(url, params):
   full_url = facebook_url(url,params)
   try:
-    return urllib2.urlopen(full_url).read()
-  except urllib2.HTTPError:
+    return urllib.request.urlopen(full_url).read()
+  except urllib.error.HTTPError:
     from helios_auth.models import AuthenticationExpired
     raise AuthenticationExpired()
 
 def facebook_post(url, params):
   full_url = facebook_url(url, None)
-  return urllib2.urlopen(full_url, urllib.urlencode(params)).read()
+  return urllib.request.urlopen(full_url, urllib.parse.urlencode(params)).read()
 
 def get_auth_url(request, redirect_url):
   request.session['fb_redirect_uri'] = redirect_url
@@ -69,7 +67,7 @@ def update_status(user_id, user_info, token, message):
       })
 
 def send_message(user_id, user_name, user_info, subject, body):
-  if user_info.has_key('email'):
+  if 'email' in user_info:
     send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (user_name, user_info['email'])], fail_silently=False)    
 
 
diff --git a/helios_auth/auth_systems/facebookclient/__init__.py b/helios_auth/auth_systems/facebookclient/__init__.py
deleted file mode 100644
index 03264730c9ab284b3500793fc232ad15175a945e..0000000000000000000000000000000000000000
--- a/helios_auth/auth_systems/facebookclient/__init__.py
+++ /dev/null
@@ -1,1431 +0,0 @@
-#! /usr/bin/env python
-#
-# pyfacebook - Python bindings for the Facebook API
-#
-# Copyright (c) 2008, Samuel Cormier-Iijima
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#     * Redistributions of source code must retain the above copyright
-#       notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above copyright
-#       notice, this list of conditions and the following disclaimer in the
-#       documentation and/or other materials provided with the distribution.
-#     * Neither the name of the author nor the names of its contributors may
-#       be used to endorse or promote products derived from this software
-#       without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY
-# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY
-# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-"""
-Python bindings for the Facebook API (pyfacebook - http://code.google.com/p/pyfacebook)
-
-PyFacebook is a client library that wraps the Facebook API.
-
-For more information, see
-
-Home Page: http://code.google.com/p/pyfacebook
-Developer Wiki: http://wiki.developers.facebook.com/index.php/Python
-Facebook IRC Channel: #facebook on irc.freenode.net
-
-PyFacebook can use simplejson if it is installed, which
-is much faster than XML and also uses less bandwith. Go to
-http://undefined.org/python/#simplejson to download it, or do
-apt-get install python-simplejson on a Debian-like system.
-"""
-
-import sys
-import time
-import struct
-import urllib
-import urllib2
-import httplib
-import hashlib
-import binascii
-import urlparse
-import mimetypes
-
-# try to use simplejson first, otherwise fallback to XML
-RESPONSE_FORMAT = 'JSON'
-
-import json
-
-# try:
-#     import json as simplejson
-# except ImportError:
-#     try:
-#         import simplejson
-#     except ImportError:
-#         try:
-#             from django.utils import simplejson
-#         except ImportError:
-#             try:
-#                 import jsonlib as simplejson
-#                 simplejson.loads
-#             except (ImportError, AttributeError):
-#                 from xml.dom import minidom
-#                 RESPONSE_FORMAT = 'XML'
-
-# support Google App Engine.  GAE does not have a working urllib.urlopen.
-try:
-    from google.appengine.api import urlfetch
-
-    def urlread(url, data=None, headers=None):
-        if data is not None:
-            if headers is None:
-                headers = {"Content-type": "application/x-www-form-urlencoded"}
-            method = urlfetch.POST
-        else:
-            if headers is None:
-                headers = {}
-            method = urlfetch.GET
-
-        result = urlfetch.fetch(url, method=method,
-                                payload=data, headers=headers)
-
-        if result.status_code == 200:
-            return result.content
-        else:
-            raise urllib2.URLError("fetch error url=%s, code=%d" % (url, result.status_code))
-
-except ImportError:
-    def urlread(url, data=None):
-        res = urllib2.urlopen(url, data=data)
-        return res.read()
-
-__all__ = ['Facebook']
-
-VERSION = '0.1'
-
-FACEBOOK_URL = 'http://api.facebook.com/restserver.php'
-FACEBOOK_SECURE_URL = 'https://api.facebook.com/restserver.php'
-
-class json(object): pass
-
-# simple IDL for the Facebook API
-METHODS = {
-    'application': {
-        'getPublicInfo': [
-            ('application_id', int, ['optional']),
-            ('application_api_key', str, ['optional']),
-            ('application_canvas_name', str,['optional']),
-        ],
-    },
-
-    # admin methods
-    'admin': {
-        'getAllocation': [
-            ('integration_point_name', str, []),
-        ],
-    },
-
-    # auth methods
-    'auth': {
-        'revokeAuthorization': [
-            ('uid', int, ['optional']),
-        ],
-    },
-
-    # feed methods
-    'feed': {
-        'publishStoryToUser': [
-            ('title', str, []),
-            ('body', str, ['optional']),
-            ('image_1', str, ['optional']),
-            ('image_1_link', str, ['optional']),
-            ('image_2', str, ['optional']),
-            ('image_2_link', str, ['optional']),
-            ('image_3', str, ['optional']),
-            ('image_3_link', str, ['optional']),
-            ('image_4', str, ['optional']),
-            ('image_4_link', str, ['optional']),
-            ('priority', int, ['optional']),
-        ],
-
-        'publishActionOfUser': [
-            ('title', str, []),
-            ('body', str, ['optional']),
-            ('image_1', str, ['optional']),
-            ('image_1_link', str, ['optional']),
-            ('image_2', str, ['optional']),
-            ('image_2_link', str, ['optional']),
-            ('image_3', str, ['optional']),
-            ('image_3_link', str, ['optional']),
-            ('image_4', str, ['optional']),
-            ('image_4_link', str, ['optional']),
-            ('priority', int, ['optional']),
-        ],
-
-        'publishTemplatizedAction': [
-            ('title_template', str, []),
-            ('page_actor_id', int, ['optional']),
-            ('title_data', json, ['optional']),
-            ('body_template', str, ['optional']),
-            ('body_data', json, ['optional']),
-            ('body_general', str, ['optional']),
-            ('image_1', str, ['optional']),
-            ('image_1_link', str, ['optional']),
-            ('image_2', str, ['optional']),
-            ('image_2_link', str, ['optional']),
-            ('image_3', str, ['optional']),
-            ('image_3_link', str, ['optional']),
-            ('image_4', str, ['optional']),
-            ('image_4_link', str, ['optional']),
-            ('target_ids', list, ['optional']),
-        ],
-
-        'registerTemplateBundle': [
-            ('one_line_story_templates', json, []),
-            ('short_story_templates', json, ['optional']),
-            ('full_story_template', json, ['optional']),
-            ('action_links', json, ['optional']),
-        ],
-
-        'deactivateTemplateBundleByID': [
-            ('template_bundle_id', int, []),
-        ],
-
-        'getRegisteredTemplateBundles': [],
-
-        'getRegisteredTemplateBundleByID': [
-            ('template_bundle_id', str, []),
-        ],
-
-        'publishUserAction': [
-            ('template_bundle_id', int, []),
-            ('template_data', json, ['optional']),
-            ('target_ids', list, ['optional']),
-            ('body_general', str, ['optional']),
-            ('story_size', int, ['optional']),
-        ],
-    },
-
-    # fql methods
-    'fql': {
-        'query': [
-            ('query', str, []),
-        ],
-    },
-
-    # friends methods
-    'friends': {
-        'areFriends': [
-            ('uids1', list, []),
-            ('uids2', list, []),
-        ],
-
-        'get': [
-            ('flid', int, ['optional']),
-        ],
-
-        'getLists': [],
-
-        'getAppUsers': [],
-    },
-
-    # notifications methods
-    'notifications': {
-        'get': [],
-
-        'send': [
-            ('to_ids', list, []),
-            ('notification', str, []),
-            ('email', str, ['optional']),
-            ('type', str, ['optional']),
-        ],
-
-        'sendRequest': [
-            ('to_ids', list, []),
-            ('type', str, []),
-            ('content', str, []),
-            ('image', str, []),
-            ('invite', bool, []),
-        ],
-
-        'sendEmail': [
-            ('recipients', list, []),
-            ('subject', str, []),
-            ('text', str, ['optional']),
-            ('fbml', str, ['optional']),
-        ]
-    },
-
-    # profile methods
-    'profile': {
-        'setFBML': [
-            ('markup', str, ['optional']),
-            ('uid', int, ['optional']),
-            ('profile', str, ['optional']),
-            ('profile_action', str, ['optional']),
-            ('mobile_fbml', str, ['optional']),
-            ('profile_main', str, ['optional']),
-        ],
-
-        'getFBML': [
-            ('uid', int, ['optional']),
-            ('type', int, ['optional']),
-        ],
-
-        'setInfo': [
-            ('title', str, []),
-            ('type', int, []),
-            ('info_fields', json, []),
-            ('uid', int, []),
-        ],
-
-        'getInfo': [
-            ('uid', int, []),
-        ],
-
-        'setInfoOptions': [
-            ('field', str, []),
-            ('options', json, []),
-        ],
-
-        'getInfoOptions': [
-            ('field', str, []),
-        ],
-    },
-
-    # users methods
-    'users': {
-        'getInfo': [
-            ('uids', list, []),
-            ('fields', list, [('default', ['name'])]),
-        ],
-
-        'getStandardInfo': [
-            ('uids', list, []),
-            ('fields', list, [('default', ['uid'])]),
-        ],
-
-        'getLoggedInUser': [],
-
-        'isAppAdded': [],
-
-        'hasAppPermission': [
-            ('ext_perm', str, []),
-            ('uid', int, ['optional']),
-        ],
-
-        'setStatus': [
-            ('status', str, []),
-            ('clear', bool, []),
-            ('status_includes_verb', bool, ['optional']),
-            ('uid', int, ['optional']),
-        ],
-    },
-
-    # events methods
-    'events': {
-        'get': [
-            ('uid', int, ['optional']),
-            ('eids', list, ['optional']),
-            ('start_time', int, ['optional']),
-            ('end_time', int, ['optional']),
-            ('rsvp_status', str, ['optional']),
-        ],
-
-        'getMembers': [
-            ('eid', int, []),
-        ],
-
-        'create': [
-            ('event_info', json, []),
-        ],
-    },
-
-    # update methods
-    'update': {
-        'decodeIDs': [
-            ('ids', list, []),
-        ],
-    },
-
-    # groups methods
-    'groups': {
-        'get': [
-            ('uid', int, ['optional']),
-            ('gids', list, ['optional']),
-        ],
-
-        'getMembers': [
-            ('gid', int, []),
-        ],
-    },
-
-    # marketplace methods
-    'marketplace': {
-        'createListing': [
-            ('listing_id', int, []),
-            ('show_on_profile', bool, []),
-            ('listing_attrs', str, []),
-        ],
-
-        'getCategories': [],
-
-        'getListings': [
-            ('listing_ids', list, []),
-            ('uids', list, []),
-        ],
-
-        'getSubCategories': [
-            ('category', str, []),
-        ],
-
-        'removeListing': [
-            ('listing_id', int, []),
-            ('status', str, []),
-        ],
-
-        'search': [
-            ('category', str, ['optional']),
-            ('subcategory', str, ['optional']),
-            ('query', str, ['optional']),
-        ],
-    },
-
-    # pages methods
-    'pages': {
-        'getInfo': [
-            ('fields', list, [('default', ['page_id', 'name'])]),
-            ('page_ids', list, ['optional']),
-            ('uid', int, ['optional']),
-        ],
-
-        'isAdmin': [
-            ('page_id', int, []),
-        ],
-
-        'isAppAdded': [
-            ('page_id', int, []),
-        ],
-
-        'isFan': [
-            ('page_id', int, []),
-            ('uid', int, []),
-        ],
-    },
-
-    # photos methods
-    'photos': {
-        'addTag': [
-            ('pid', int, []),
-            ('tag_uid', int, [('default', 0)]),
-            ('tag_text', str, [('default', '')]),
-            ('x', float, [('default', 50)]),
-            ('y', float, [('default', 50)]),
-            ('tags', str, ['optional']),
-        ],
-
-        'createAlbum': [
-            ('name', str, []),
-            ('location', str, ['optional']),
-            ('description', str, ['optional']),
-        ],
-
-        'get': [
-            ('subj_id', int, ['optional']),
-            ('aid', int, ['optional']),
-            ('pids', list, ['optional']),
-        ],
-
-        'getAlbums': [
-            ('uid', int, ['optional']),
-            ('aids', list, ['optional']),
-        ],
-
-        'getTags': [
-            ('pids', list, []),
-        ],
-    },
-
-    # status methods
-    'status': {
-        'get': [
-            ('uid', int, ['optional']),
-            ('limit', int, ['optional']),
-        ],
-        'set': [
-            ('status', str, ['optional']),
-            ('uid', int, ['optional']),
-        ],
-    },
-
-    # fbml methods
-    'fbml': {
-        'refreshImgSrc': [
-            ('url', str, []),
-        ],
-
-        'refreshRefUrl': [
-            ('url', str, []),
-        ],
-
-        'setRefHandle': [
-            ('handle', str, []),
-            ('fbml', str, []),
-        ],
-    },
-
-    # SMS Methods
-    'sms' : {
-        'canSend' : [
-            ('uid', int, []),
-        ],
-
-        'send' : [
-            ('uid', int, []),
-            ('message', str, []),
-            ('session_id', int, []),
-            ('req_session', bool, []),
-        ],
-    },
-
-    'data': {
-        'getCookies': [
-            ('uid', int, []),
-            ('string', str, ['optional']),
-        ],
-
-        'setCookie': [
-            ('uid', int, []),
-            ('name', str, []),
-            ('value', str, []),
-            ('expires', int, ['optional']),
-            ('path', str, ['optional']),
-        ],
-    },
-
-    # connect methods
-    'connect': {
-        'registerUsers': [
-            ('accounts', json, []),
-        ],
-
-        'unregisterUsers': [
-            ('email_hashes', json, []),
-        ],
-
-        'getUnconnectedFriendsCount': [
-        ],
-    },
-
-    #stream methods (beta)
-    'stream' : {
-        'addComment' : [
-            ('post_id', int, []),
-            ('comment', str, []),
-            ('uid', int, ['optional']),
-        ],
-
-        'addLike': [
-            ('uid', int, ['optional']),
-            ('post_id', int, ['optional']),
-        ],
-
-        'get' : [
-            ('viewer_id', int, ['optional']),
-            ('source_ids', list, ['optional']),
-            ('start_time', int, ['optional']),
-            ('end_time', int, ['optional']),
-            ('limit', int, ['optional']),
-            ('filter_key', str, ['optional']),
-        ],
-
-        'getComments' : [
-            ('post_id', int, []),
-        ],
-
-        'getFilters' : [
-            ('uid', int, ['optional']),
-        ],
-
-        'publish' : [
-            ('message', str, ['optional']),
-            ('attachment', json, ['optional']),
-            ('action_links', json, ['optional']),
-            ('target_id', str, ['optional']),
-            ('uid', str, ['optional']),
-        ],
-
-        'remove' : [
-            ('post_id', int, []),
-            ('uid', int, ['optional']),
-        ],
-
-        'removeComment' : [
-            ('comment_id', int, []),
-            ('uid', int, ['optional']),
-        ],
-
-        'removeLike' : [
-            ('uid', int, ['optional']),
-            ('post_id', int, ['optional']),
-        ],
-    }
-}
-
-class Proxy(object):
-    """Represents a "namespace" of Facebook API calls."""
-
-    def __init__(self, client, name):
-        self._client = client
-        self._name = name
-
-    def __call__(self, method=None, args=None, add_session_args=True):
-        # for Django templates
-        if method is None:
-            return self
-
-        if add_session_args:
-            self._client._add_session_args(args)
-
-        return self._client('%s.%s' % (self._name, method), args)
-
-
-# generate the Facebook proxies
-def __generate_proxies():
-    for namespace in METHODS:
-        methods = {}
-
-        for method in METHODS[namespace]:
-            params = ['self']
-            body = ['args = {}']
-
-            for param_name, param_type, param_options in METHODS[namespace][method]:
-                param = param_name
-
-                for option in param_options:
-                    if isinstance(option, tuple) and option[0] == 'default':
-                        if param_type == list:
-                            param = '%s=None' % param_name
-                            body.append('if %s is None: %s = %s' % (param_name, param_name, repr(option[1])))
-                        else:
-                            param = '%s=%s' % (param_name, repr(option[1]))
-
-                if param_type == json:
-                    # we only jsonify the argument if it's a list or a dict, for compatibility
-                    body.append('if isinstance(%s, list) or isinstance(%s, dict): %s = simplejson.dumps(%s)' % ((param_name,) * 4))
-
-                if 'optional' in param_options:
-                    param = '%s=None' % param_name
-                    body.append('if %s is not None: args[\'%s\'] = %s' % (param_name, param_name, param_name))
-                else:
-                    body.append('args[\'%s\'] = %s' % (param_name, param_name))
-
-                params.append(param)
-
-            # simple docstring to refer them to Facebook API docs
-            body.insert(0, '"""Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=%s.%s"""' % (namespace, method))
-
-            body.insert(0, 'def %s(%s):' % (method, ', '.join(params)))
-
-            body.append('return self(\'%s\', args)' % method)
-
-            exec('\n    '.join(body))
-
-            methods[method] = eval(method)
-
-        proxy = type('%sProxy' % namespace.title(), (Proxy, ), methods)
-
-        globals()[proxy.__name__] = proxy
-
-
-__generate_proxies()
-
-
-class FacebookError(Exception):
-    """Exception class for errors received from Facebook."""
-
-    def __init__(self, code, msg, args=None):
-        self.code = code
-        self.msg = msg
-        self.args = args
-
-    def __str__(self):
-        return 'Error %s: %s' % (self.code, self.msg)
-
-
-class AuthProxy(AuthProxy):
-    """Special proxy for facebook.auth."""
-
-    def getSession(self):
-        """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=auth.getSession"""
-        args = {}
-        try:
-            args['auth_token'] = self._client.auth_token
-        except AttributeError:
-            raise RuntimeError('Client does not have auth_token set.')
-        result = self._client('%s.getSession' % self._name, args)
-        self._client.session_key = result['session_key']
-        self._client.uid = result['uid']
-        self._client.secret = result.get('secret')
-        self._client.session_key_expires = result['expires']
-        return result
-
-    def createToken(self):
-        """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=auth.createToken"""
-        token = self._client('%s.createToken' % self._name)
-        self._client.auth_token = token
-        return token
-
-
-class FriendsProxy(FriendsProxy):
-    """Special proxy for facebook.friends."""
-
-    def get(self, **kwargs):
-        """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=friends.get"""
-        if not kwargs.get('flid') and self._client._friends:
-            return self._client._friends
-        return super(FriendsProxy, self).get(**kwargs)
-
-
-class PhotosProxy(PhotosProxy):
-    """Special proxy for facebook.photos."""
-
-    def upload(self, image, aid=None, caption=None, size=(604, 1024), filename=None, callback=None):
-        """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=photos.upload
-
-        size -- an optional size (width, height) to resize the image to before uploading. Resizes by default
-                to Facebook's maximum display width of 604.
-        """
-        args = {}
-
-        if aid is not None:
-            args['aid'] = aid
-
-        if caption is not None:
-            args['caption'] = caption
-
-        args = self._client._build_post_args('facebook.photos.upload', self._client._add_session_args(args))
-
-        try:
-            import cStringIO as StringIO
-        except ImportError:
-            import StringIO
-
-        # check for a filename specified...if the user is passing binary data in
-        # image then a filename will be specified
-        if filename is None:
-            try:
-                import Image
-            except ImportError:
-                data = StringIO.StringIO(open(image, 'rb').read())
-            else:
-                img = Image.open(image)
-                if size:
-                    img.thumbnail(size, Image.ANTIALIAS)
-                data = StringIO.StringIO()
-                img.save(data, img.format)
-        else:
-            # there was a filename specified, which indicates that image was not
-            # the path to an image file but rather the binary data of a file
-            data = StringIO.StringIO(image)
-            image = filename
-
-        content_type, body = self.__encode_multipart_formdata(list(args.iteritems()), [(image, data)])
-        urlinfo = urlparse.urlsplit(self._client.facebook_url)
-        try:
-            content_length = len(body)
-            chunk_size = 4096
-
-            h = httplib.HTTPConnection(urlinfo[1])
-            h.putrequest('POST', urlinfo[2])
-            h.putheader('Content-Type', content_type)
-            h.putheader('Content-Length', str(content_length))
-            h.putheader('MIME-Version', '1.0')
-            h.putheader('User-Agent', 'PyFacebook Client Library')
-            h.endheaders()
-
-            if callback:
-                count = 0
-                while len(body) > 0:
-                    if len(body) < chunk_size:
-                        data = body
-                        body = ''
-                    else:
-                        data = body[0:chunk_size]
-                        body = body[chunk_size:]
-
-                    h.send(data)
-                    count += 1
-                    callback(count, chunk_size, content_length)
-            else:
-                h.send(body)
-
-            response = h.getresponse()
-
-            if response.status != 200:
-                raise Exception('Error uploading photo: Facebook returned HTTP %s (%s)' % (response.status, response.reason))
-            response = response.read()
-        except:
-            # sending the photo failed, perhaps we are using GAE
-            try:
-                from google.appengine.api import urlfetch
-
-                try:
-                    response = urlread(url=self._client.facebook_url,data=body,headers={'POST':urlinfo[2],'Content-Type':content_type,'MIME-Version':'1.0'})
-                except urllib2.URLError:
-                    raise Exception('Error uploading photo: Facebook returned %s' % (response))
-            except ImportError:
-                # could not import from google.appengine.api, so we are not running in GAE
-                raise Exception('Error uploading photo.')
-
-        return self._client._parse_response(response, 'facebook.photos.upload')
-
-
-    def __encode_multipart_formdata(self, fields, files):
-        """Encodes a multipart/form-data message to upload an image."""
-        boundary = '-------tHISiStheMulTIFoRMbOUNDaRY'
-        crlf = '\r\n'
-        l = []
-
-        for (key, value) in fields:
-            l.append('--' + boundary)
-            l.append('Content-Disposition: form-data; name="%s"' % str(key))
-            l.append('')
-            l.append(str(value))
-        for (filename, value) in files:
-            l.append('--' + boundary)
-            l.append('Content-Disposition: form-data; filename="%s"' % (str(filename), ))
-            l.append('Content-Type: %s' % self.__get_content_type(filename))
-            l.append('')
-            l.append(value.getvalue())
-        l.append('--' + boundary + '--')
-        l.append('')
-        body = crlf.join(l)
-        content_type = 'multipart/form-data; boundary=%s' % boundary
-        return content_type, body
-
-
-    def __get_content_type(self, filename):
-        """Returns a guess at the MIME type of the file from the filename."""
-        return str(mimetypes.guess_type(filename)[0]) or 'application/octet-stream'
-
-
-class Facebook(object):
-    """
-    Provides access to the Facebook API.
-
-    Instance Variables:
-
-    added
-        True if the user has added this application.
-
-    api_key
-        Your API key, as set in the constructor.
-
-    app_name
-        Your application's name, i.e. the APP_NAME in http://apps.facebook.com/APP_NAME/ if
-        this is for an internal web application. Optional, but useful for automatic redirects
-        to canvas pages.
-
-    auth_token
-        The auth token that Facebook gives you, either with facebook.auth.createToken,
-        or through a GET parameter.
-
-    callback_path
-        The path of the callback set in the Facebook app settings. If your callback is set
-        to http://www.example.com/facebook/callback/, this should be '/facebook/callback/'.
-        Optional, but useful for automatic redirects back to the same page after login.
-
-    desktop
-        True if this is a desktop app, False otherwise. Used for determining how to
-        authenticate.
-
-    ext_perms
-        Any extended permissions that the user has granted to your application.
-        This parameter is set only if the user has granted any.
-
-    facebook_url
-        The url to use for Facebook requests.
-
-    facebook_secure_url
-        The url to use for secure Facebook requests.
-
-    in_canvas
-        True if the current request is for a canvas page.
-
-    in_iframe
-        True if the current request is for an HTML page to embed in Facebook inside an iframe.
-
-    is_session_from_cookie
-        True if the current request session comes from a session cookie.
-
-    in_profile_tab
-        True if the current request is for a user's tab for your application.
-
-    internal
-        True if this Facebook object is for an internal application (one that can be added on Facebook)
-
-    locale
-        The user's locale. Default: 'en_US'
-
-    page_id
-        Set to the page_id of the current page (if any)
-
-    profile_update_time
-        The time when this user's profile was last updated. This is a UNIX timestamp. Default: None if unknown.
-
-    secret
-        Secret that is used after getSession for desktop apps.
-
-    secret_key
-        Your application's secret key, as set in the constructor.
-
-    session_key
-        The current session key. Set automatically by auth.getSession, but can be set
-        manually for doing infinite sessions.
-
-    session_key_expires
-        The UNIX time of when this session key expires, or 0 if it never expires.
-
-    uid
-        After a session is created, you can get the user's UID with this variable. Set
-        automatically by auth.getSession.
-
-    ----------------------------------------------------------------------
-
-    """
-
-    def __init__(self, api_key, secret_key, auth_token=None, app_name=None, callback_path=None, internal=None, proxy=None, facebook_url=None, facebook_secure_url=None):
-        """
-        Initializes a new Facebook object which provides wrappers for the Facebook API.
-
-        If this is a desktop application, the next couple of steps you might want to take are:
-
-        facebook.auth.createToken() # create an auth token
-        facebook.login()            # show a browser window
-        wait_login()                # somehow wait for the user to log in
-        facebook.auth.getSession()  # get a session key
-
-        For web apps, if you are passed an auth_token from Facebook, pass that in as a named parameter.
-        Then call:
-
-        facebook.auth.getSession()
-
-        """
-        self.api_key = api_key
-        self.secret_key = secret_key
-        self.session_key = None
-        self.session_key_expires = None
-        self.auth_token = auth_token
-        self.secret = None
-        self.uid = None
-        self.page_id = None
-        self.in_canvas = False
-        self.in_iframe = False
-        self.is_session_from_cookie = False
-        self.in_profile_tab = False
-        self.added = False
-        self.app_name = app_name
-        self.callback_path = callback_path
-        self.internal = internal
-        self._friends = None
-        self.locale = 'en_US'
-        self.profile_update_time = None
-        self.ext_perms = None
-        self.proxy = proxy
-        if facebook_url is None:
-            self.facebook_url = FACEBOOK_URL
-        else:
-            self.facebook_url = facebook_url
-        if facebook_secure_url is None:
-            self.facebook_secure_url = FACEBOOK_SECURE_URL
-        else:
-            self.facebook_secure_url = facebook_secure_url
-
-        for namespace in METHODS:
-            self.__dict__[namespace] = eval('%sProxy(self, \'%s\')' % (namespace.title(), 'facebook.%s' % namespace))
-
-
-    def _hash_args(self, args, secret=None):
-        """Hashes arguments by joining key=value pairs, appending a secret, and then taking the MD5 hex digest."""
-        # @author: houyr
-        # fix for UnicodeEncodeError
-        hasher = hashlib.md5(''.join(['%s=%s' % (isinstance(x, unicode) and x.encode("utf-8") or x, isinstance(args[x], unicode) and args[x].encode("utf-8") or args[x]) for x in sorted(args.keys())]))
-        if secret:
-            hasher.update(secret)
-        elif self.secret:
-            hasher.update(self.secret)
-        else:
-            hasher.update(self.secret_key)
-        return hasher.hexdigest()
-
-
-    def _parse_response_item(self, node):
-        """Parses an XML response node from Facebook."""
-        if node.nodeType == node.DOCUMENT_NODE and \
-            node.childNodes[0].hasAttributes() and \
-            node.childNodes[0].hasAttribute('list') and \
-            node.childNodes[0].getAttribute('list') == "true":
-            return {node.childNodes[0].nodeName: self._parse_response_list(node.childNodes[0])}
-        elif node.nodeType == node.ELEMENT_NODE and \
-            node.hasAttributes() and \
-            node.hasAttribute('list') and \
-            node.getAttribute('list')=="true":
-            return self._parse_response_list(node)
-        elif len(filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes)) > 0:
-            return self._parse_response_dict(node)
-        else:
-            return ''.join(node.data for node in node.childNodes if node.nodeType == node.TEXT_NODE)
-
-
-    def _parse_response_dict(self, node):
-        """Parses an XML dictionary response node from Facebook."""
-        result = {}
-        for item in filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes):
-            result[item.nodeName] = self._parse_response_item(item)
-        if node.nodeType == node.ELEMENT_NODE and node.hasAttributes():
-            if node.hasAttribute('id'):
-                result['id'] = node.getAttribute('id')
-        return result
-
-
-    def _parse_response_list(self, node):
-        """Parses an XML list response node from Facebook."""
-        result = []
-        for item in filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes):
-            result.append(self._parse_response_item(item))
-        return result
-
-
-    def _check_error(self, response):
-        """Checks if the given Facebook response is an error, and then raises the appropriate exception."""
-        if type(response) is dict and response.has_key('error_code'):
-            raise FacebookError(response['error_code'], response['error_msg'], response['request_args'])
-
-
-    def _build_post_args(self, method, args=None):
-        """Adds to args parameters that are necessary for every call to the API."""
-        if args is None:
-            args = {}
-
-        for arg in args.items():
-            if type(arg[1]) == list:
-                args[arg[0]] = ','.join(str(a) for a in arg[1])
-            elif type(arg[1]) == unicode:
-                args[arg[0]] = arg[1].encode("UTF-8")
-            elif type(arg[1]) == bool:
-                args[arg[0]] = str(arg[1]).lower()
-
-        args['method'] = method
-        args['api_key'] = self.api_key
-        args['v'] = '1.0'
-        args['format'] = RESPONSE_FORMAT
-        args['sig'] = self._hash_args(args)
-
-        return args
-
-
-    def _add_session_args(self, args=None):
-        """Adds 'session_key' and 'call_id' to args, which are used for API calls that need sessions."""
-        if args is None:
-            args = {}
-
-        if not self.session_key:
-            return args
-            #some calls don't need a session anymore. this might be better done in the markup
-            #raise RuntimeError('Session key not set. Make sure auth.getSession has been called.')
-
-        args['session_key'] = self.session_key
-        args['call_id'] = str(int(time.time() * 1000))
-
-        return args
-
-
-    def _parse_response(self, response, method, format=None):
-        """Parses the response according to the given (optional) format, which should be either 'JSON' or 'XML'."""
-        if not format:
-            format = RESPONSE_FORMAT
-
-        if format == 'JSON':
-            result = simplejson.loads(response)
-
-            self._check_error(result)
-        elif format == 'XML':
-            dom = minidom.parseString(response)
-            result = self._parse_response_item(dom)
-            dom.unlink()
-
-            if 'error_response' in result:
-                self._check_error(result['error_response'])
-
-            result = result[method[9:].replace('.', '_') + '_response']
-        else:
-            raise RuntimeError('Invalid format specified.')
-
-        return result
-
-
-    def hash_email(self, email):
-        """
-        Hash an email address in a format suitable for Facebook Connect.
-
-        """
-        email = email.lower().strip()
-        return "%s_%s" % (
-            struct.unpack("I", struct.pack("i", binascii.crc32(email)))[0],
-            hashlib.md5(email).hexdigest(),
-        )
-
-
-    def unicode_urlencode(self, params):
-        """
-        @author: houyr
-        A unicode aware version of urllib.urlencode.
-        """
-        if isinstance(params, dict):
-            params = params.items()
-        return urllib.urlencode([(k, isinstance(v, unicode) and v.encode('utf-8') or v)
-                          for k, v in params])
-
-
-    def __call__(self, method=None, args=None, secure=False):
-        """Make a call to Facebook's REST server."""
-        # for Django templates, if this object is called without any arguments
-        # return the object itself
-        if method is None:
-            return self
-
-        # __init__ hard-codes into en_US
-        if args is not None and not args.has_key('locale'):
-            args['locale'] = self.locale
-
-        # @author: houyr
-        # fix for bug of UnicodeEncodeError
-        post_data = self.unicode_urlencode(self._build_post_args(method, args))
-
-        if self.proxy:
-            proxy_handler = urllib2.ProxyHandler(self.proxy)
-            opener = urllib2.build_opener(proxy_handler)
-            if secure:
-                response = opener.open(self.facebook_secure_url, post_data).read()
-            else:
-                response = opener.open(self.facebook_url, post_data).read()
-        else:
-            if secure:
-                response = urlread(self.facebook_secure_url, post_data)
-            else:
-                response = urlread(self.facebook_url, post_data)
-
-        return self._parse_response(response, method)
-
-
-    # URL helpers
-    def get_url(self, page, **args):
-        """
-        Returns one of the Facebook URLs (www.facebook.com/SOMEPAGE.php).
-        Named arguments are passed as GET query string parameters.
-
-        """
-        return 'http://www.facebook.com/%s.php?%s' % (page, urllib.urlencode(args))
-
-
-    def get_app_url(self, path=''):
-        """
-        Returns the URL for this app's canvas page, according to app_name.
-
-        """
-        return 'http://apps.facebook.com/%s/%s' % (self.app_name, path)
-
-
-    def get_add_url(self, next=None):
-        """
-        Returns the URL that the user should be redirected to in order to add the application.
-
-        """
-        args = {'api_key': self.api_key, 'v': '1.0'}
-
-        if next is not None:
-            args['next'] = next
-
-        return self.get_url('install', **args)
-
-
-    def get_authorize_url(self, next=None, next_cancel=None):
-        """
-        Returns the URL that the user should be redirected to in order to
-        authorize certain actions for application.
-
-        """
-        args = {'api_key': self.api_key, 'v': '1.0'}
-
-        if next is not None:
-            args['next'] = next
-
-        if next_cancel is not None:
-            args['next_cancel'] = next_cancel
-
-        return self.get_url('authorize', **args)
-
-
-    def get_login_url(self, next=None, popup=False, canvas=True):
-        """
-        Returns the URL that the user should be redirected to in order to login.
-
-        next -- the URL that Facebook should redirect to after login
-
-        """
-        args = {'api_key': self.api_key, 'v': '1.0'}
-
-        if next is not None:
-            args['next'] = next
-
-        if canvas is True:
-            args['canvas'] = 1
-
-        if popup is True:
-            args['popup'] = 1
-
-        if self.auth_token is not None:
-            args['auth_token'] = self.auth_token
-
-        return self.get_url('login', **args)
-
-
-    def login(self, popup=False):
-        """Open a web browser telling the user to login to Facebook."""
-        import webbrowser
-        webbrowser.open(self.get_login_url(popup=popup))
-
-
-    def get_ext_perm_url(self, ext_perm, next=None, popup=False):
-        """
-        Returns the URL that the user should be redirected to in order to grant an extended permission.
-
-        ext_perm -- the name of the extended permission to request
-        next     -- the URL that Facebook should redirect to after login
-
-        """
-        args = {'ext_perm': ext_perm, 'api_key': self.api_key, 'v': '1.0'}
-
-        if next is not None:
-            args['next'] = next
-
-        if popup is True:
-            args['popup'] = 1
-
-        return self.get_url('authorize', **args)
-
-
-    def request_extended_permission(self, ext_perm, popup=False):
-        """Open a web browser telling the user to grant an extended permission."""
-        import webbrowser
-        webbrowser.open(self.get_ext_perm_url(ext_perm, popup=popup))
-
-
-    def check_session(self, request):
-        """
-        Checks the given Django HttpRequest for Facebook parameters such as
-        POST variables or an auth token. If the session is valid, returns True
-        and this object can now be used to access the Facebook API. Otherwise,
-        it returns False, and the application should take the appropriate action
-        (either log the user in or have him add the application).
-
-        """
-        self.in_canvas = (request.POST.get('fb_sig_in_canvas') == '1')
-
-        if self.session_key and (self.uid or self.page_id):
-            return True
-
-
-        if request.method == 'POST':
-            params = self.validate_signature(request.POST)
-        else:
-            if 'installed' in request.GET:
-                self.added = True
-
-            if 'fb_page_id' in request.GET:
-                self.page_id = request.GET['fb_page_id']
-
-            if 'auth_token' in request.GET:
-                self.auth_token = request.GET['auth_token']
-
-                try:
-                    self.auth.getSession()
-                except FacebookError, e:
-                    self.auth_token = None
-                    return False
-
-                return True
-
-            params = self.validate_signature(request.GET)
-
-        if not params:
-            # first check if we are in django - to check cookies
-            if hasattr(request, 'COOKIES'):
-                params = self.validate_cookie_signature(request.COOKIES)
-                self.is_session_from_cookie = True
-            else:
-                # if not, then we might be on GoogleAppEngine, check their request object cookies
-                if hasattr(request,'cookies'):
-                    params = self.validate_cookie_signature(request.cookies)
-                    self.is_session_from_cookie = True
-
-        if not params:
-            return False
-
-        if params.get('in_canvas') == '1':
-            self.in_canvas = True
-
-        if params.get('in_iframe') == '1':
-            self.in_iframe = True
-
-        if params.get('in_profile_tab') == '1':
-            self.in_profile_tab = True
-
-        if params.get('added') == '1':
-            self.added = True
-
-        if params.get('expires'):
-            self.session_key_expires = int(params['expires'])
-
-        if 'locale' in params:
-            self.locale = params['locale']
-
-        if 'profile_update_time' in params:
-            try:
-                self.profile_update_time = int(params['profile_update_time'])
-            except ValueError:
-                pass
-
-        if 'ext_perms' in params:
-            self.ext_perms = params['ext_perms']
-
-        if 'friends' in params:
-            if params['friends']:
-                self._friends = params['friends'].split(',')
-            else:
-                self._friends = []
-
-        if 'session_key' in params:
-            self.session_key = params['session_key']
-            if 'user' in params:
-                self.uid = params['user']
-            elif 'page_id' in params:
-                self.page_id = params['page_id']
-            else:
-                return False
-        elif 'profile_session_key' in params:
-            self.session_key = params['profile_session_key']
-            if 'profile_user' in params:
-                self.uid = params['profile_user']
-            else:
-                return False
-        elif 'canvas_user' in params:
-            self.uid = params['canvas_user']
-        elif 'uninstall' in params:
-            self.uid = params['user']
-        else:
-            return False
-
-        return True
-
-
-    def validate_signature(self, post, prefix='fb_sig', timeout=None):
-        """
-        Validate parameters passed to an internal Facebook app from Facebook.
-
-        """
-        args = post.copy()
-
-        if prefix not in args:
-            return None
-
-        del args[prefix]
-
-        if timeout and '%s_time' % prefix in post and time.time() - float(post['%s_time' % prefix]) > timeout:
-            return None
-
-        args = dict([(key[len(prefix + '_'):], value) for key, value in args.items() if key.startswith(prefix)])
-
-        hash = self._hash_args(args)
-
-        if hash == post[prefix]:
-            return args
-        else:
-            return None
-
-    def validate_cookie_signature(self, cookies):
-        """
-        Validate parameters passed by cookies, namely facebookconnect or js api.
-        """
-
-        api_key = self.api_key
-        if api_key not in cookies:
-            return None
-
-        prefix = api_key + "_"
-       
-        params = {} 
-        vals = ''
-        for k in sorted(cookies):
-            if k.startswith(prefix):
-                key = k.replace(prefix,"")
-                value = cookies[k]
-                params[key] = value
-                vals += '%s=%s' % (key, value)
-                
-        hasher = hashlib.md5(vals)
-
-        hasher.update(self.secret_key)
-        digest = hasher.hexdigest()
-        if digest == cookies[api_key]:
-            params['is_session_from_cookie'] = True
-            return params
-        else:
-            return False
-
-
-
-
-if __name__ == '__main__':
-    # sample desktop application
-
-    api_key = ''
-    secret_key = ''
-
-    facebook = Facebook(api_key, secret_key)
-
-    facebook.auth.createToken()
-
-    # Show login window
-    # Set popup=True if you want login without navigational elements
-    facebook.login()
-
-    # Login to the window, then press enter
-    print 'After logging in, press enter...'
-    raw_input()
-
-    facebook.auth.getSession()
-    print 'Session Key:   ', facebook.session_key
-    print 'Your UID:      ', facebook.uid
-
-    info = facebook.users.getInfo([facebook.uid], ['name', 'birthday', 'affiliations', 'sex'])[0]
-
-    print 'Your Name:     ', info['name']
-    print 'Your Birthday: ', info['birthday']
-    print 'Your Gender:   ', info['sex']
-
-    friends = facebook.friends.get()
-    friends = facebook.users.getInfo(friends[0:5], ['name', 'birthday', 'relationship_status'])
-
-    for friend in friends:
-        print friend['name'], 'has a birthday on', friend['birthday'], 'and is', friend['relationship_status']
-
-    arefriends = facebook.friends.areFriends([friends[0]['uid']], [friends[1]['uid']])
-
-    photos = facebook.photos.getAlbums(facebook.uid)
-
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/__init__.py b/helios_auth/auth_systems/facebookclient/djangofb/__init__.py
deleted file mode 100644
index 68b1b27c37d19e1372369837006947b4f60ef7c5..0000000000000000000000000000000000000000
--- a/helios_auth/auth_systems/facebookclient/djangofb/__init__.py
+++ /dev/null
@@ -1,248 +0,0 @@
-import re
-import datetime
-import facebook
-
-from django.http import HttpResponse, HttpResponseRedirect
-from django.core.exceptions import ImproperlyConfigured
-from django.conf import settings
-from datetime import datetime
-
-try:
-    from threading import local
-except ImportError:
-    from django.utils._threading_local import local
-
-__all__ = ['Facebook', 'FacebookMiddleware', 'get_facebook_client', 'require_login', 'require_add']
-
-_thread_locals = local()
-
-class Facebook(facebook.Facebook):
-    def redirect(self, url):
-        """
-        Helper for Django which redirects to another page. If inside a
-        canvas page, writes a <fb:redirect> instead to achieve the same effect.
-
-        """
-        if self.in_canvas:
-            return HttpResponse('<fb:redirect url="%s" />' % (url, ))
-        elif re.search("^https?:\/\/([^\/]*\.)?facebook\.com(:\d+)?", url.lower()):
-            return HttpResponse('<script type="text/javascript">\ntop.location.href = "%s";\n</script>' % url)
-        else:
-            return HttpResponseRedirect(url)
-
-
-def get_facebook_client():
-    """
-    Get the current Facebook object for the calling thread.
-
-    """
-    try:
-        return _thread_locals.facebook
-    except AttributeError:
-        raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.')
-
-
-def require_login(next=None, internal=None):
-    """
-    Decorator for Django views that requires the user to be logged in.
-    The FacebookMiddleware must be installed.
-
-    Standard usage:
-        @require_login()
-        def some_view(request):
-            ...
-
-    Redirecting after login:
-        To use the 'next' parameter to redirect to a specific page after login, a callable should
-        return a path relative to the Post-add URL. 'next' can also be an integer specifying how many
-        parts of request.path to strip to find the relative URL of the canvas page. If 'next' is None,
-        settings.callback_path and settings.app_name are checked to redirect to the same page after logging
-        in. (This is the default behavior.)
-        @require_login(next=some_callable)
-        def some_view(request):
-            ...
-    """
-    def decorator(view):
-        def newview(request, *args, **kwargs):
-            next = newview.next
-            internal = newview.internal
-
-            try:
-                fb = request.facebook
-            except:
-                raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.')
-
-            if internal is None:
-                internal = request.facebook.internal
-
-            if callable(next):
-                next = next(request.path)
-            elif isinstance(next, int):
-                next = '/'.join(request.path.split('/')[next + 1:])
-            elif next is None and fb.callback_path and request.path.startswith(fb.callback_path):
-                next = request.path[len(fb.callback_path):]
-            elif not isinstance(next, str):
-                next = ''
-
-            if not fb.check_session(request):
-                #If user has never logged in before, the get_login_url will redirect to the TOS page
-                return fb.redirect(fb.get_login_url(next=next))
-
-            if internal and request.method == 'GET' and fb.app_name:
-                return fb.redirect('%s%s' % (fb.get_app_url(), next))
-
-            return view(request, *args, **kwargs)
-        newview.next = next
-        newview.internal = internal
-        return newview
-    return decorator
-
-
-def require_add(next=None, internal=None, on_install=None):
-    """
-    Decorator for Django views that requires application installation.
-    The FacebookMiddleware must be installed.
-    
-    Standard usage:
-        @require_add()
-        def some_view(request):
-            ...
-
-    Redirecting after installation:
-        To use the 'next' parameter to redirect to a specific page after login, a callable should
-        return a path relative to the Post-add URL. 'next' can also be an integer specifying how many
-        parts of request.path to strip to find the relative URL of the canvas page. If 'next' is None,
-        settings.callback_path and settings.app_name are checked to redirect to the same page after logging
-        in. (This is the default behavior.)
-        @require_add(next=some_callable)
-        def some_view(request):
-            ...
-
-    Post-install processing:
-        Set the on_install parameter to a callable in order to handle special post-install processing.
-        The callable should take a request object as the parameter.
-        @require_add(on_install=some_callable)
-        def some_view(request):
-            ...
-    """
-    def decorator(view):
-        def newview(request, *args, **kwargs):
-            next = newview.next
-            internal = newview.internal
-
-            try:
-                fb = request.facebook
-            except:
-                raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.')
-
-            if internal is None:
-                internal = request.facebook.internal
-
-            if callable(next):
-                next = next(request.path)
-            elif isinstance(next, int):
-                next = '/'.join(request.path.split('/')[next + 1:])
-            elif next is None and fb.callback_path and request.path.startswith(fb.callback_path):
-                next = request.path[len(fb.callback_path):]
-            else:
-                next = ''
-
-            if not fb.check_session(request):
-                if fb.added:
-                    if request.method == 'GET' and fb.app_name:
-                        return fb.redirect('%s%s' % (fb.get_app_url(), next))
-                    return fb.redirect(fb.get_login_url(next=next))
-                else:
-                    return fb.redirect(fb.get_add_url(next=next))
-
-            if not fb.added:
-                return fb.redirect(fb.get_add_url(next=next))
-
-            if 'installed' in request.GET and callable(on_install):
-                on_install(request)
-
-            if internal and request.method == 'GET' and fb.app_name:
-                return fb.redirect('%s%s' % (fb.get_app_url(), next))
-
-            return view(request, *args, **kwargs)
-        newview.next = next
-        newview.internal = internal
-        return newview
-    return decorator
-
-# try to preserve the argspecs
-try:
-    import decorator
-except ImportError:
-    pass
-else:
-    def updater(f):
-        def updated(*args, **kwargs):
-            original = f(*args, **kwargs)
-            def newdecorator(view):
-                return decorator.new_wrapper(original(view), view)
-            return decorator.new_wrapper(newdecorator, original)
-        return decorator.new_wrapper(updated, f)
-    require_login = updater(require_login)
-    require_add = updater(require_add)
-
-class FacebookMiddleware(object):
-    """
-    Middleware that attaches a Facebook object to every incoming request.
-    The Facebook object created can also be accessed from models for the
-    current thread by using get_facebook_client().
-
-    """
-
-    def __init__(self, api_key=None, secret_key=None, app_name=None, callback_path=None, internal=None):
-        self.api_key = api_key or settings.FACEBOOK_API_KEY
-        self.secret_key = secret_key or settings.FACEBOOK_SECRET_KEY
-        self.app_name = app_name or getattr(settings, 'FACEBOOK_APP_NAME', None)
-        self.callback_path = callback_path or getattr(settings, 'FACEBOOK_CALLBACK_PATH', None)
-        self.internal = internal or getattr(settings, 'FACEBOOK_INTERNAL', True)
-        self.proxy = None
-        if getattr(settings, 'USE_HTTP_PROXY', False):
-            self.proxy = settings.HTTP_PROXY
-
-    def process_request(self, request):
-        _thread_locals.facebook = request.facebook = Facebook(self.api_key, self.secret_key, app_name=self.app_name, callback_path=self.callback_path, internal=self.internal, proxy=self.proxy)
-        if not self.internal:
-            if 'fb_sig_session_key' in request.GET and 'fb_sig_user' in request.GET:
-                request.facebook.session_key = request.session['facebook_session_key'] = request.GET['fb_sig_session_key']
-                request.facebook.uid = request.session['fb_sig_user'] = request.GET['fb_sig_user']
-            elif request.session.get('facebook_session_key', None) and request.session.get('facebook_user_id', None):
-                request.facebook.session_key = request.session['facebook_session_key']
-                request.facebook.uid = request.session['facebook_user_id']
-
-    def process_response(self, request, response):
-        if not self.internal and request.facebook.session_key and request.facebook.uid:
-            request.session['facebook_session_key'] = request.facebook.session_key
-            request.session['facebook_user_id'] = request.facebook.uid
-
-            if request.facebook.session_key_expires:
-                expiry = datetime.datetime.fromtimestamp(request.facebook.session_key_expires)
-                request.session.set_expiry(expiry)
-
-        try:
-            fb = request.facebook
-        except:
-            return response
-
-        if not fb.is_session_from_cookie:
-            # Make sure the browser accepts our session cookies inside an Iframe
-            response['P3P'] = 'CP="NOI DSP COR NID ADMa OPTa OUR NOR"'
-            fb_cookies = {
-                'expires': fb.session_key_expires,
-                'session_key': fb.session_key,
-                'user': fb.uid,
-            }
-
-            expire_time = None
-            if fb.session_key_expires:
-                expire_time = datetime.utcfromtimestamp(fb.session_key_expires)
-
-            for k in fb_cookies:
-                response.set_cookie(self.api_key + '_' + k, fb_cookies[k], expires=expire_time)
-            response.set_cookie(self.api_key , fb._hash_args(fb_cookies), expires=expire_time)
-
-        return response
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/context_processors.py b/helios_auth/auth_systems/facebookclient/djangofb/context_processors.py
deleted file mode 100644
index 6f954397308f7af525d9fa600fb29e8cf6902c33..0000000000000000000000000000000000000000
--- a/helios_auth/auth_systems/facebookclient/djangofb/context_processors.py
+++ /dev/null
@@ -1,6 +0,0 @@
-def messages(request):
-    """Returns messages similar to ``django.core.context_processors.auth``."""
-    if hasattr(request, 'facebook') and request.facebook.uid is not None:
-        from models import Message
-        messages = Message.objects.get_and_delete_all(uid=request.facebook.uid)
-    return {'messages': messages}
\ No newline at end of file
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/__init__.py b/helios_auth/auth_systems/facebookclient/djangofb/default_app/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/models.py b/helios_auth/auth_systems/facebookclient/djangofb/default_app/models.py
deleted file mode 100644
index 666ccd3f39403df207fac99cee88c6ca00789b8f..0000000000000000000000000000000000000000
--- a/helios_auth/auth_systems/facebookclient/djangofb/default_app/models.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from django.db import models
-
-# get_facebook_client lets us get the current Facebook object
-# from outside of a view, which lets us have cleaner code
-from facebook.djangofb import get_facebook_client
-
-class UserManager(models.Manager):
-    """Custom manager for a Facebook User."""
-    
-    def get_current(self):
-        """Gets a User object for the logged-in Facebook user."""
-        facebook = get_facebook_client()
-        user, created = self.get_or_create(id=int(facebook.uid))
-        if created:
-            # we could do some custom actions for new users here...
-            pass
-        return user
-
-class User(models.Model):
-    """A simple User model for Facebook users."""
-
-    # We use the user's UID as the primary key in our database.
-    id = models.IntegerField(primary_key=True)
-
-    # TODO: The data that you want to store for each user would go here.
-    # For this sample, we let users let people know their favorite progamming
-    # language, in the spirit of Extended Info.
-    language = models.CharField(maxlength=64, default='Python')
-
-    # Add the custom manager
-    objects = UserManager()
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/templates/canvas.fbml b/helios_auth/auth_systems/facebookclient/djangofb/default_app/templates/canvas.fbml
deleted file mode 100644
index 6734dd17caa138540fb10d0fcb750d70c8600d33..0000000000000000000000000000000000000000
--- a/helios_auth/auth_systems/facebookclient/djangofb/default_app/templates/canvas.fbml
+++ /dev/null
@@ -1,22 +0,0 @@
-<fb:header>
-  {% comment %}
-    We can use {{ fbuser }} to get at the current user.
-    {{ fbuser.id }} will be the user's UID, and {{ fbuser.language }}
-    is his/her favorite language (Python :-).
-  {% endcomment %}
-  Welcome, <fb:name uid="{{ fbuser.id }}" firstnameonly="true" useyou="false" />!
-</fb:header>
-
-<div class="clearfix" style="float: left; border: 1px #d8dfea solid; padding: 10px 10px 10px 10px; margin-left: 30px; margin-bottom: 30px; width: 500px;">
-  Your favorite language is {{ fbuser.language|escape }}.
-  <br /><br />
-
-  <div class="grayheader clearfix">
-    <br /><br />
-
-    <form action="." method="POST">
-      <input type="text" name="language" value="{{ fbuser.language|escape }}" />
-      <input type="submit" value="Change" />
-    </form>
-  </div>
-</div>
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/urls.py b/helios_auth/auth_systems/facebookclient/djangofb/default_app/urls.py
deleted file mode 100644
index f75d8d258360fc43e12b2343182d89f14a01ef8c..0000000000000000000000000000000000000000
--- a/helios_auth/auth_systems/facebookclient/djangofb/default_app/urls.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from django.conf.urls import url
-
-from views import canvas
-
-urlpatterns = [
-    url(r'^$', canvas),
-]
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/views.py b/helios_auth/auth_systems/facebookclient/djangofb/default_app/views.py
deleted file mode 100644
index 609314fe01b3bf546984841b9dded39756bfa0cb..0000000000000000000000000000000000000000
--- a/helios_auth/auth_systems/facebookclient/djangofb/default_app/views.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from django.http import HttpResponse
-# from django.views.generic.simple import direct_to_template
-#uncomment the following two lines and the one below
-#if you dont want to use a decorator instead of the middleware
-#from django.utils.decorators import decorator_from_middleware
-#from facebook.djangofb import FacebookMiddleware
-
-# Import the Django helpers
-import facebook.djangofb as facebook
-
-# The User model defined in models.py
-from models import User
-
-# We'll require login for our canvas page. This
-# isn't necessarily a good idea, as we might want
-# to let users see the page without granting our app
-# access to their info. See the wiki for details on how
-# to do this.
-#@decorator_from_middleware(FacebookMiddleware)
-@facebook.require_login()
-def canvas(request):
-    # Get the User object for the currently logged in user
-    user = User.objects.get_current()
-
-    # Check if we were POSTed the user's new language of choice
-    if 'language' in request.POST:
-        user.language = request.POST['language'][:64]
-        user.save()
-
-    # User is guaranteed to be logged in, so pass canvas.fbml
-    # an extra 'fbuser' parameter that is the User object for
-    # the currently logged in user.
-    #return direct_to_template(request, 'canvas.fbml', extra_context={'fbuser': user})
-    return None
-
-@facebook.require_login()
-def ajax(request):
-    return HttpResponse('hello world')
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/models.py b/helios_auth/auth_systems/facebookclient/djangofb/models.py
deleted file mode 100644
index b5d2c62221e9926f7ab4b57cb95fb71ab22be2da..0000000000000000000000000000000000000000
--- a/helios_auth/auth_systems/facebookclient/djangofb/models.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from django.db import models
-from django.utils.html import escape
-from django.utils.safestring import mark_safe
-
-FB_MESSAGE_STATUS = (
-    (0, 'Explanation'),
-    (1, 'Error'),
-    (2, 'Success'),
-)
-
-class MessageManager(models.Manager):
-    def get_and_delete_all(self, uid):
-        messages = []
-        for m in self.filter(uid=uid):
-            messages.append(m)
-            m.delete()
-        return messages
-
-class Message(models.Model):
-    """Represents a message for a Facebook user."""
-    uid = models.CharField(max_length=25)
-    status = models.IntegerField(choices=FB_MESSAGE_STATUS)
-    message = models.CharField(max_length=300)
-    objects = MessageManager()
-
-    def __unicode__(self):
-        return self.message
-
-    def _fb_tag(self):
-        return self.get_status_display().lower()
-
-    def as_fbml(self):
-        return mark_safe(u'<fb:%s message="%s" />' % (
-            self._fb_tag(),
-            escape(self.message),
-        ))
diff --git a/helios_auth/auth_systems/facebookclient/webappfb.py b/helios_auth/auth_systems/facebookclient/webappfb.py
deleted file mode 100644
index 5fdf77af5c05ce29ccd56b6326ca4b8f64a08294..0000000000000000000000000000000000000000
--- a/helios_auth/auth_systems/facebookclient/webappfb.py
+++ /dev/null
@@ -1,170 +0,0 @@
-#
-# webappfb - Facebook tools for Google's AppEngine "webapp" Framework
-#
-# Copyright (c) 2009, Max Battcher
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#     * Redistributions of source code must retain the above copyright
-#       notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above copyright
-#       notice, this list of conditions and the following disclaimer in the
-#       documentation and/or other materials provided with the distribution.
-#     * Neither the name of the author nor the names of its contributors may
-#       be used to endorse or promote products derived from this software
-#       without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY
-# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY
-# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-from google.appengine.api import memcache
-from google.appengine.ext.webapp import RequestHandler
-from facebook import Facebook
-import yaml
-
-"""
-Facebook tools for Google AppEngine's object-oriented "webapp" framework.
-"""
-
-# This global configuration dictionary is for configuration variables
-# for Facebook requests such as the application's API key and secret
-# key. Defaults to loading a 'facebook.yaml' YAML file. This should be
-# useful and familiar for most AppEngine development.
-FACEBOOK_CONFIG = yaml.load(file('facebook.yaml', 'r'))
-
-class FacebookRequestHandler(RequestHandler):
-    """
-    Base class for request handlers for Facebook apps, providing useful
-    Facebook-related tools: a local 
-    """
-
-    def _fbconfig_value(self, name, default=None):
-        """
-        Checks the global config dictionary and then for a class/instance
-        variable, using a provided default if no value is found.
-        """
-        if name in FACEBOOK_CONFIG:
-            default = FACEBOOK_CONFIG[name]
-            
-        return getattr(self, name, default)
-
-    def initialize(self, request, response):
-        """
-        Initialize's this request's Facebook client.
-        """
-        super(FacebookRequestHandler, self).initialize(request, response)
-
-        app_name = self._fbconfig_value('app_name', '')
-        api_key = self._fbconfig_value('api_key', None)
-        secret_key = self._fbconfig_value('secret_key', None)
-
-        self.facebook = Facebook(api_key, secret_key,
-            app_name=app_name)
-
-        require_app = self._fbconfig_value('require_app', False)
-        require_login = self._fbconfig_value('require_login', False)
-        need_session = self._fbconfig_value('need_session', False)
-        check_session = self._fbconfig_value('check_session', True)
-
-        self._messages = None
-        self.redirecting = False
-
-        if require_app or require_login:
-            if not self.facebook.check_session(request):
-                self.redirect(self.facebook.get_login_url(next=request.path))
-                self.redirecting = True
-                return
-        elif check_session:
-            self.facebook.check_session(request) # ignore response
-
-        # NOTE: require_app is deprecated according to modern Facebook login
-        #       policies. Included for completeness, but unnecessary.
-        if require_app and not self.facebook.added:
-            self.redirect(self.facebook.get_add_url(next=request.path))
-            self.redirecting = True
-            return
-
-        if not (require_app or require_login) and need_session:
-            self.facebook.auth.getSession()
-
-    def redirect(self, url, **kwargs):
-        """
-        For Facebook canvas pages we should use <fb:redirect /> instead of
-        a normal redirect.
-        """
-        if self.facebook.in_canvas:
-            self.response.clear()
-            self.response.out.write('<fb:redirect url="%s" />' % (url, ))
-        else:
-            super(FacebookRequestHandler, self).redirect(url, **kwargs)
-
-    def add_user_message(self, kind, msg, detail='', time=15 * 60):
-        """
-        Add a message to the current user to memcache.
-        """
-        if self.facebook.uid:
-            key = 'messages:%s' % self.facebook.uid
-            self._messages = memcache.get(key)
-            message = {
-                'kind': kind,
-                'message': msg,
-                'detail': detail,
-            }
-            if self._messages is not None:
-                self._messages.append(message)
-            else:
-                self._messages = [message]
-            memcache.set(key, self._messages, time=time)
-
-    def get_and_delete_user_messages(self):
-        """
-        Get all of the messages for the current user; removing them.
-        """
-        if self.facebook.uid:
-            key = 'messages:%s' % self.facebook.uid
-            if not hasattr(self, '_messages') or self._messages is None:
-                self._messages = memcache.get(key)
-            memcache.delete(key)
-            return self._messages
-        return None
-
-class FacebookCanvasHandler(FacebookRequestHandler):
-    """
-    Request handler for Facebook canvas (FBML application) requests.
-    """
-
-    def canvas(self, *args, **kwargs):
-        """
-        This will be your handler to deal with Canvas requests.
-        """
-        raise NotImplementedError()
-
-    def get(self, *args):
-        """
-        All valid canvas views are POSTS.
-        """
-        # TODO: Attempt to auto-redirect to Facebook canvas?
-        self.error(404)
-
-    def post(self, *args, **kwargs):
-        """
-        Check a couple of simple safety checks and then call the canvas
-        handler.
-        """
-        if self.redirecting: return
-
-        if not self.facebook.in_canvas:
-            self.error(404)
-            return
-
-        self.canvas(*args, **kwargs)
-
-# vim: ai et ts=4 sts=4 sw=4
diff --git a/helios_auth/auth_systems/facebookclient/wsgi.py b/helios_auth/auth_systems/facebookclient/wsgi.py
deleted file mode 100644
index f6a790db14858d762159478a4aa51e7323144b5d..0000000000000000000000000000000000000000
--- a/helios_auth/auth_systems/facebookclient/wsgi.py
+++ /dev/null
@@ -1,129 +0,0 @@
-"""This is some simple helper code to bridge the Pylons / PyFacebook gap.
-
-There's some generic WSGI middleware, some Paste stuff, and some Pylons
-stuff.  Once you put FacebookWSGIMiddleware into your middleware stack,
-you'll have access to ``environ["pyfacebook.facebook"]``, which is a
-``facebook.Facebook`` object.  If you're using Paste (which includes
-Pylons users), you can also access this directly using the facebook
-global in this module.
-
-"""
-
-# Be careful what you import.  Don't expect everyone to have Pylons,
-# Paste, etc. installed.  Degrade gracefully.
-
-from facebook import Facebook
-
-__docformat__ = "restructuredtext"
-
-
-# Setup Paste, if available.  This needs to stay in the same module as
-# FacebookWSGIMiddleware below.
-
-try:
-    from paste.registry import StackedObjectProxy
-    from webob.exc import _HTTPMove
-    from paste.util.quoting import strip_html, html_quote, no_quote
-except ImportError:
-    pass
-else:
-    facebook = StackedObjectProxy(name="PyFacebook Facebook Connection")
-
-
-    class CanvasRedirect(_HTTPMove):
-
-        """This is for canvas redirects."""
-
-        title = "See Other"
-        code = 200
-        template = '<fb:redirect url="%(location)s" />'
-
-        def html(self, environ):
-            """ text/html representation of the exception """
-            body = self.make_body(environ, self.template, html_quote, no_quote)
-            return body
-
-class FacebookWSGIMiddleware(object):
-
-    """This is WSGI middleware for Facebook."""
-
-    def __init__(self, app, config, facebook_class=Facebook):
-        """Initialize the Facebook middleware.
-
-        ``app``
-            This is the WSGI application being wrapped.
-
-        ``config``
-            This is a dict containing the keys "pyfacebook.apikey" and
-            "pyfacebook.secret".
-
-        ``facebook_class``
-            If you want to subclass the Facebook class, you can pass in
-            your replacement here.  Pylons users will want to use
-            PylonsFacebook.
-
-        """
-        self.app = app
-        self.config = config
-        self.facebook_class = facebook_class
-
-    def __call__(self, environ, start_response):
-        config = self.config
-        real_facebook = self.facebook_class(config["pyfacebook.apikey"],
-                                            config["pyfacebook.secret"])
-        registry = environ.get('paste.registry')
-        if registry:
-            registry.register(facebook, real_facebook)
-        environ['pyfacebook.facebook'] = real_facebook
-        return self.app(environ, start_response)
-
-
-# The remainder is Pylons specific.
-
-try:
-    import pylons
-    from pylons.controllers.util import redirect_to as pylons_redirect_to
-    from routes import url_for
-except ImportError:
-    pass
-else:
-
-
-    class PylonsFacebook(Facebook):
-
-        """Subclass Facebook to add Pylons goodies."""
-
-        def check_session(self, request=None):
-            """The request parameter is now optional."""
-            if request is None:
-                request = pylons.request
-            return Facebook.check_session(self, request)
-
-        # The Django request object is similar enough to the Paste
-        # request object that check_session and validate_signature
-        # should *just work*.
-
-        def redirect_to(self, url):
-            """Wrap Pylons' redirect_to function so that it works in_canvas.
-
-            By the way, this won't work until after you call
-            check_session().
-
-            """
-            if self.in_canvas:
-                raise CanvasRedirect(url)
-            pylons_redirect_to(url)
-
-        def apps_url_for(self, *args, **kargs):
-            """Like url_for, but starts with "http://apps.facebook.com"."""
-            return "http://apps.facebook.com" + url_for(*args, **kargs)
-
-
-    def create_pylons_facebook_middleware(app, config):
-        """This is a simple wrapper for FacebookWSGIMiddleware.
-
-        It passes the correct facebook_class.
-
-        """
-        return FacebookWSGIMiddleware(app, config,
-                                      facebook_class=PylonsFacebook)
diff --git a/helios_auth/auth_systems/google.py b/helios_auth/auth_systems/google.py
index 03419915600fe58636ce9e2098a0219075fa804d..0b08b04c17c01225eaa6c227e74c55e46f168055 100644
--- a/helios_auth/auth_systems/google.py
+++ b/helios_auth/auth_systems/google.py
@@ -4,11 +4,12 @@ Google Authentication
 """
 
 import httplib2
-import json
 from django.conf import settings
 from django.core.mail import send_mail
 from oauth2client.client import OAuth2WebServerFlow
 
+from helios_auth import utils
+
 # some parameters to indicate that status updating is not possible
 STATUS_UPDATES = False
 
@@ -30,7 +31,7 @@ def get_auth_url(request, redirect_url):
 def get_user_info_after_auth(request):
   flow = get_flow(request.session['google-redirect-url'])
 
-  if not request.GET.has_key('code'):
+  if 'code' not in request.GET:
     return None
   
   code = request.GET['code']
@@ -48,7 +49,7 @@ def get_user_info_after_auth(request):
   http = credentials.authorize(http)
   (resp_headers, content) = http.request("https://people.googleapis.com/v1/people/me?personFields=names", "GET")
 
-  response = json.loads(content)
+  response = utils.from_json(content.decode('utf-8'))
 
   name = response['names'][0]['displayName']
   
diff --git a/helios_auth/auth_systems/linkedin.py b/helios_auth/auth_systems/linkedin.py
index 696eda9836f0c141360d24f00739d8ed66fca95e..e75e0786d46a508f9846bd6b9d1491be1bdbcbc5 100644
--- a/helios_auth/auth_systems/linkedin.py
+++ b/helios_auth/auth_systems/linkedin.py
@@ -2,18 +2,12 @@
 LinkedIn Authentication
 """
 
-from oauthclient import client
-
-from django.urls import reverse
-from django.http import HttpResponseRedirect
-
-from helios_auth import utils
-
 from xml.etree import ElementTree
 
-import logging
-
 from django.conf import settings
+
+from .oauthclient import client
+
 API_KEY = settings.LINKEDIN_API_KEY
 API_SECRET = settings.LINKEDIN_API_SECRET
 
diff --git a/helios_auth/auth_systems/live.py b/helios_auth/auth_systems/live.py
index 9f34a2783198003f09a74abb1671d9630cf3895a..348d02ff34a22d685b3d6dbe46aa5885ad162b4d 100644
--- a/helios_auth/auth_systems/live.py
+++ b/helios_auth/auth_systems/live.py
@@ -5,14 +5,14 @@ so much like Facebook
 # NOT WORKING YET because Windows Live documentation and status is unclear. Is it in beta? I think it is.
 """
 
-import logging
+import urllib.parse
+import urllib.request
 
 from django.conf import settings
+
 APP_ID = settings.LIVE_APP_ID
 APP_SECRET = settings.LIVE_APP_SECRET
   
-import urllib, urllib2, cgi
-
 # some parameters to indicate that status updating is possible
 STATUS_UPDATES = False
 # STATUS_UPDATE_WORDING_TEMPLATE = "Send %s to your facebook status"
@@ -21,17 +21,17 @@ from helios_auth import utils
 
 def live_url(url, params):
   if params:
-    return "https://graph.facebook.com%s?%s" % (url, urllib.urlencode(params))
+    return "https://graph.facebook.com%s?%s" % (url, urllib.parse.urlencode(params))
   else:
     return "https://graph.facebook.com%s" % url
 
 def live_get(url, params):
   full_url = live_url(url,params)
-  return urllib2.urlopen(full_url).read()
+  return urllib.request.urlopen(full_url).read()
 
 def live_post(url, params):
   full_url = live_url(url, None)
-  return urllib2.urlopen(full_url, urllib.urlencode(params)).read()
+  return urllib.request.urlopen(full_url, urllib.parse.urlencode(params)).read()
 
 def get_auth_url(request, redirect_url):
   request.session['live_redirect_uri'] = redirect_url
@@ -41,16 +41,16 @@ def get_auth_url(request, redirect_url):
       'scope': 'publish_stream'})
     
 def get_user_info_after_auth(request):
-  args = facebook_get('/oauth/access_token', {
+  args = live_get('/oauth/access_token', {
       'client_id' : APP_ID,
       'redirect_uri' : request.session['fb_redirect_uri'],
-      'client_secret' : API_SECRET,
+      'client_secret' : APP_SECRET,
       'code' : request.GET['code']
       })
 
-  access_token = cgi.parse_qs(args)['access_token'][0]
+  access_token = urllib.parse.parse_qs(args)['access_token'][0]
 
-  info = utils.from_json(facebook_get('/me', {'access_token':access_token}))
+  info = utils.from_json(live_get('/me', {'access_token':access_token}))
 
   return {'type': 'facebook', 'user_id' : info['id'], 'name': info['name'], 'info': info, 'token': {'access_token': access_token}}
     
@@ -58,7 +58,7 @@ def update_status(user_id, user_info, token, message):
   """
   post a message to the auth system's update stream, e.g. twitter stream
   """
-  result = facebook_post('/me/feed', {
+  result = live_post('/me/feed', {
       'access_token': token['access_token'],
       'message': message
       })
diff --git a/helios_auth/auth_systems/oauthclient/client.py b/helios_auth/auth_systems/oauthclient/client.py
index c9e6b829bb946694130cabcc6b336939864bcb3a..4980535e45ab58942cc632c00ecc50c0dd33d578 100644
--- a/helios_auth/auth_systems/oauthclient/client.py
+++ b/helios_auth/auth_systems/oauthclient/client.py
@@ -7,12 +7,11 @@ Used the SampleClient from the OAUTH.org example python client as basis.
 props to leahculver for making a very hard to use but in the end usable oauth lib.
 
 '''
-import httplib
-import urllib, urllib2
-import time
+import urllib.request
 import webbrowser
-import oauth as oauth
-from urlparse import urlparse
+
+from . import oauth as oauth
+
 
 class LoginOAuthClient(oauth.OAuthClient):
 
@@ -36,31 +35,35 @@ class LoginOAuthClient(oauth.OAuthClient):
 
         self.sha1_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
         self.consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
-        if ((oauth_token != None) and (oauth_token_secret!=None)):
+        if (oauth_token is not None) and (oauth_token_secret is not None):
             self.token = oauth.OAuthConsumer(oauth_token, oauth_token_secret)
         else:
             self.token = None
 
-    def oauth_request(self,url, args = {}, method=None):
-        if (method==None):
-            if args=={}:
+    def oauth_request(self, url, args=None, method=None):
+        if args is None:
+            args = {}
+        if method is None:
+            if args == {}:
                 method = "GET"
             else:
                 method = "POST"
         req = oauth.OAuthRequest.from_consumer_and_token(self.consumer, self.token, method, url, args)
         req.sign_request(self.sha1_method, self.consumer,self.token)
-        if (method=="GET"):
+        if method== "GET":
             return self.http_wrapper(req.to_url())
-        elif (method == "POST"):
+        elif method == "POST":
             return self.http_wrapper(req.get_normalized_http_url(),req.to_postdata())
 
     #this is barely working. (i think. mostly it is that everyone else is using httplib) 
-    def http_wrapper(self, url, postdata={}): 
+    def http_wrapper(self, url, postdata=None):
+        if postdata is None:
+            postdata = {}
         try:
-            if (postdata != {}): 
-                f = urllib.urlopen(url, postdata) 
+            if postdata != {}:
+                f = urllib.request.urlopen(url, postdata) 
             else: 
-                f = urllib.urlopen(url) 
+                f = urllib.request.urlopen(url) 
             response = f.read()
         except:
             import traceback
@@ -133,26 +136,26 @@ if __name__ == '__main__':
     consumer_key = ''
     consumer_secret = ''
     while not consumer_key:
-        consumer_key = raw_input('Please enter consumer key: ')
+        consumer_key = input('Please enter consumer key: ')
     while not consumer_secret:
-        consumer_secret = raw_input('Please enter consumer secret: ')
+        consumer_secret = input('Please enter consumer secret: ')
     auth_client = LoginOAuthClient(consumer_key,consumer_secret)
     tok = auth_client.get_request_token()
     token = tok['oauth_token']
     token_secret = tok['oauth_token_secret']
     url = auth_client.get_authorize_url(token) 
     webbrowser.open(url)
-    print "Visit this URL to authorize your app: " + url
-    response_token = raw_input('What is the oauth_token from twitter: ')
+    print("Visit this URL to authorize your app: " + url)
+    response_token = input('What is the oauth_token from twitter: ')
     response_client = LoginOAuthClient(consumer_key, consumer_secret,token, token_secret, server_params={})
     tok = response_client.get_access_token()
-    print "Making signed request"
+    print("Making signed request")
     #verify user access
     content = response_client.oauth_request('https://twitter.com/account/verify_credentials.json', method='POST')
     #make an update
     #content = response_client.oauth_request('https://twitter.com/statuses/update.xml', {'status':'Updated from a python oauth client. awesome.'}, method='POST')
-    print content
+    print(content)
    
-    print 'Done.'
+    print('Done.')
 
 
diff --git a/helios_auth/auth_systems/oauthclient/oauth/__init__.py b/helios_auth/auth_systems/oauthclient/oauth/__init__.py
index baf543ed4a8db09d92ee91b925d18bc6f5d09f93..f318a8af4421f92c9a5276d619fd8ef52dbe9644 100755
--- a/helios_auth/auth_systems/oauthclient/oauth/__init__.py
+++ b/helios_auth/auth_systems/oauthclient/oauth/__init__.py
@@ -1,10 +1,8 @@
-import cgi
-import urllib
-import time
-import random
-import urlparse
-import hmac
 import binascii
+import hmac
+import random
+import time
+import urllib.parse
 
 VERSION = '1.0' # Hi Blaine!
 HTTP_METHOD = 'GET'
@@ -22,7 +20,7 @@ def build_authenticate_header(realm=''):
 # url escape
 def escape(s):
     # escape '/' too
-    return urllib.quote(s, safe='~')
+    return urllib.parse.quote(s, safe='~')
 
 # util function: current timestamp
 # seconds since epoch (UTC)
@@ -60,12 +58,12 @@ class OAuthToken(object):
         self.secret = secret
 
     def to_string(self):
-        return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
+        return urllib.parse.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
 
     # return a token from something like:
     # oauth_token_secret=digg&oauth_token=digg
     def from_string(s):
-        params = cgi.parse_qs(s, keep_blank_values=False)
+        params = urllib.parse.parse_qs(s, keep_blank_values=False)
         key = params['oauth_token'][0]
         secret = params['oauth_token_secret'][0]
         return OAuthToken(key, secret)
@@ -112,7 +110,7 @@ class OAuthRequest(object):
     # get any non-oauth parameters
     def get_nonoauth_parameters(self):
         parameters = {}
-        for k, v in self.parameters.iteritems():
+        for k, v in self.parameters.items():
             # ignore oauth parameters
             if k.find('oauth_') < 0:
                 parameters[k] = v
@@ -123,14 +121,14 @@ class OAuthRequest(object):
         auth_header = 'OAuth realm="%s"' % realm
         # add the oauth parameters
         if self.parameters:
-            for k, v in self.parameters.iteritems():
+            for k, v in self.parameters.items():
                 if k[:6] == 'oauth_':
                     auth_header += ', %s="%s"' % (k, escape(str(v)))
         return {'Authorization': auth_header}
 
     # serialize as post data for a POST request
     def to_postdata(self):
-        return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()])
+        return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.items()])
 
     # serialize as a url for a GET request
     def to_url(self):
@@ -144,7 +142,7 @@ class OAuthRequest(object):
             del params['oauth_signature']
         except:
             pass
-        key_values = params.items()
+        key_values = list(params.items())
         # sort lexicographically, first after key, then after value
         key_values.sort()
         # combine key value pairs in string and escape
@@ -156,7 +154,7 @@ class OAuthRequest(object):
 
     # parses the url and rebuilds it to be scheme://host/path
     def get_normalized_http_url(self):
-        parts = urlparse.urlparse(self.http_url)
+        parts = urllib.parse.urlparse(self.http_url)
         url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path
         return url_string
         
@@ -194,7 +192,7 @@ class OAuthRequest(object):
             parameters.update(query_params)
 
         # URL parameters
-        param_str = urlparse.urlparse(http_url)[4] # query
+        param_str = urllib.parse.urlparse(http_url)[4] # query
         url_params = OAuthRequest._split_url_string(param_str)
         parameters.update(url_params)
 
@@ -249,15 +247,15 @@ class OAuthRequest(object):
             # split key-value
             param_parts = param.split('=', 1)
             # remove quotes and unescape the value
-            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+            params[param_parts[0]] = urllib.parse.unquote(param_parts[1].strip('\"'))
         return params
     _split_header = staticmethod(_split_header)
     
     # util function: turn url string into parameters, has to do some unescaping
     def _split_url_string(param_str):
-        parameters = cgi.parse_qs(param_str, keep_blank_values=False)
-        for k, v in parameters.iteritems():
-            parameters[k] = urllib.unquote(v[0])
+        parameters = urllib.parse.parse_qs(param_str, keep_blank_values=False)
+        for k, v in parameters.items():
+            parameters[k] = urllib.parse.unquote(v[0])
         return parameters
     _split_url_string = staticmethod(_split_url_string)
 
@@ -273,7 +271,7 @@ class OAuthServer(object):
         self.signature_methods = signature_methods or {}
 
     def set_data_store(self, oauth_data_store):
-        self.data_store = data_store
+        self.data_store = oauth_data_store
 
     def get_data_store(self):
         return self.data_store
@@ -351,7 +349,7 @@ class OAuthServer(object):
             # get the signature method object
             signature_method = self.signature_methods[signature_method]
         except:
-            signature_method_names = ', '.join(self.signature_methods.keys())
+            signature_method_names = ', '.join(list(self.signature_methods.keys()))
             raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
 
         return signature_method
@@ -499,11 +497,11 @@ class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
 
         # hmac object
         try:
-            import hashlib # 2.5
-            hashed = hmac.new(key, raw, hashlib.sha1)
+            from Crypto.Hash import SHA1
+            hashed = hmac.new(key, raw, SHA1)
         except:
-            import sha # deprecated
-            hashed = hmac.new(key, raw, sha)
+            import hashlib
+            hashed = hmac.new(key, raw, hashlib.sha1)
 
         # calculate the digest base 64
         return binascii.b2a_base64(hashed.digest())[:-1]
diff --git a/helios_auth/auth_systems/openid/util.py b/helios_auth/auth_systems/openid/util.py
index 1ed33f3be0f82de0f2f9d545a79ccf8b972bd878..2a2e12a429b0206f6700acbb929b5e01ffdfe11f 100644
--- a/helios_auth/auth_systems/openid/util.py
+++ b/helios_auth/auth_systems/openid/util.py
@@ -3,20 +3,15 @@
 Utility code for the Django example consumer and server.
 """
 
-from urlparse import urljoin
+from urllib.parse import urljoin
 
-from django.db import connection
-from django.template.context import RequestContext
-from django.template import loader
-from django import http
+from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
+from django.db import connection
 from django.urls import reverse as reverseURL
-
-from django.conf import settings
-
-from openid.store.filestore import FileOpenIDStore
 from openid.store import sqlstore
-from openid.yadis.constants import YADIS_CONTENT_TYPE
+from openid.store.filestore import FileOpenIDStore
+
 
 def getOpenIDStore(filestore_path, table_prefix):
     """
@@ -69,14 +64,13 @@ def getOpenIDStore(filestore_path, table_prefix):
         s = types[db_engine](connection.connection,
                                             **tablenames)
     except KeyError:
-        raise ImproperlyConfigured, \
-              "Database engine %s not supported by OpenID library" % \
-              (db_engine,)
+        raise ImproperlyConfigured("Database engine %s not supported by OpenID library" % \
+              (db_engine,))
 
     try:
         s.createTables()
-    except (SystemExit, KeyboardInterrupt, MemoryError), e:
-        raise
+    except (SystemExit, KeyboardInterrupt, MemoryError) as e:
+        raise e
     except:
         # XXX This is not the Right Way to do this, but because the
         # underlying database implementation might differ in behavior
@@ -138,5 +132,5 @@ def normalDict(request_data):
     values are lists, because in OpenID, each key in the query arg set
     can have at most one value.
     """
-    return dict((k, v[0]) for k, v in request_data.iteritems())
+    return dict((k, v[0]) for k, v in request_data.items())
 
diff --git a/helios_auth/auth_systems/openid/view_helpers.py b/helios_auth/auth_systems/openid/view_helpers.py
index 06eef8a287bbbf0d5a05d32d8ed0e13939761eac..de79b451a037800f974e33535e52569af754c1c6 100644
--- a/helios_auth/auth_systems/openid/view_helpers.py
+++ b/helios_auth/auth_systems/openid/view_helpers.py
@@ -1,14 +1,8 @@
-
-from django import http
-from django.http import HttpResponseRedirect
-
 from openid.consumer import consumer
 from openid.consumer.discover import DiscoveryFailure
 from openid.extensions import ax, pape, sreg
-from openid.yadis.constants import YADIS_HEADER_NAME, YADIS_CONTENT_TYPE
-from openid.server.trustroot import RP_RETURN_TO_URL_TYPE
 
-import util
+from . import util
 
 PAPE_POLICIES = [
     'AUTH_PHISHING_RESISTANT',
@@ -56,16 +50,12 @@ def start_openid(session, openid_url, trust_root, return_to):
 
     # Start OpenID authentication.
     c = get_consumer(session)
-    error = None
 
     try:
         auth_request = c.begin(openid_url)
-    except DiscoveryFailure, e:
+    except DiscoveryFailure as e:
         # Some other protocol-level failure occurred.
-        error = "OpenID discovery error: %s" % (str(e),)
-
-    if error:
-        raise Exception("error in openid")
+        raise Exception("error in openid: OpenID discovery error") from e
 
     # Add Simple Registration request information.  Some fields
     # are optional, some are required.  It's possible that the
@@ -80,7 +70,7 @@ def start_openid(session, openid_url, trust_root, return_to):
     # XXX - uses myOpenID-compatible schema values, which are
     # not those listed at axschema.org.
 
-    for k, v in AX_REQUIRED_FIELDS.iteritems():
+    for k, v in AX_REQUIRED_FIELDS.items():
         ax_request.add(ax.AttrInfo(v, required=True))
 
     auth_request.addExtension(ax_request)
@@ -123,12 +113,12 @@ def finish_openid(session, request_args, return_to):
 
             ax_response = ax.FetchResponse.fromSuccessResponse(response)
             if ax_response:
-                for k, v in AX_REQUIRED_FIELDS.iteritems():
+                for k, v in AX_REQUIRED_FIELDS.items():
                     """
                     the values are the URIs, they are the key into the data
                     the key is the shortname
                     """
-                    if ax_response.data.has_key(v):
+                    if v in ax_response.data:
                         ax_items[k] = ax_response.get(v)
 
         # Map different consumer status codes to template contexts.
@@ -141,7 +131,7 @@ def finish_openid(session, request_args, return_to):
 
             consumer.SUCCESS:
             {'url': response.getDisplayIdentifier(),
-             'sreg': sreg_response and sreg_response.items(),
+             'sreg': sreg_response and list(sreg_response.items()),
              'ax': ax_items}
             }
 
diff --git a/helios_auth/auth_systems/password.py b/helios_auth/auth_systems/password.py
index aadb03fb05e52d9bbe4e9b2d5e8e02933b89590f..36f42ecffcc109f283d157e1ffe2a37fadb8b706 100644
--- a/helios_auth/auth_systems/password.py
+++ b/helios_auth/auth_systems/password.py
@@ -52,7 +52,7 @@ def password_login_view(request):
     # set this in case we came here straight from the multi-login chooser
     # and thus did not have a chance to hit the "start/password" URL
     request.session['auth_system_name'] = 'password'
-    if request.POST.has_key('return_url'):
+    if 'return_url' in request.POST:
       request.session['auth_return_url'] = request.POST.get('return_url')
 
     if form.is_valid():
diff --git a/helios_auth/auth_systems/twitter.py b/helios_auth/auth_systems/twitter.py
index 8739d607ebcb1d85b0b539c8f85eb34cceaa4ce8..541ac4df7cd874259a71be3566246fa3b2a48b57 100644
--- a/helios_auth/auth_systems/twitter.py
+++ b/helios_auth/auth_systems/twitter.py
@@ -2,7 +2,7 @@
 Twitter Authentication
 """
 
-from oauthclient import client
+from .oauthclient import client
 
 from django.conf.urls import url
 from django.urls import reverse
diff --git a/helios_auth/auth_systems/yahoo.py b/helios_auth/auth_systems/yahoo.py
index 5131a19be520301ab4ea6643e88d97015d186d0f..ccfdc12ffd8850dc17feb54bee08b4fa784a5c1e 100644
--- a/helios_auth/auth_systems/yahoo.py
+++ b/helios_auth/auth_systems/yahoo.py
@@ -6,7 +6,7 @@ Yahoo Authentication
 from django.conf import settings
 from django.core.mail import send_mail
 
-from openid import view_helpers
+from .openid import view_helpers
 
 # some parameters to indicate that status updating is not possible
 STATUS_UPDATES = False
diff --git a/helios_auth/jsonfield.py b/helios_auth/jsonfield.py
index 34cecf7894487c1141388abe13d5a0934a4ab9dc..ab66d5d5d95f5e48860e36f88a4f70124cfee558 100644
--- a/helios_auth/jsonfield.py
+++ b/helios_auth/jsonfield.py
@@ -5,10 +5,12 @@ http://www.djangosnippets.org/snippets/377/
 """
 
 import json
-from django.core.exceptions import ValidationError
+
 from django.core.serializers.json import DjangoJSONEncoder
 from django.db import models
 
+from . import utils
+
 
 class JSONField(models.TextField):
     """
@@ -37,14 +39,10 @@ class JSONField(models.TextField):
 
     # noinspection PyUnusedLocal
     def from_db_value(self, value, *args, **kwargs):
-        if value == "" or value is None:
+        parsed_value = utils.from_json(value)
+        if parsed_value is None:
             return None
 
-        try:
-            parsed_value = json.loads(value)
-        except Exception as e:
-            raise ValidationError("Received value is not JSON", e)
-
         if self.json_type and parsed_value:
             parsed_value = self.json_type.fromJSONDict(parsed_value, **self.deserialization_params)
 
@@ -55,7 +53,7 @@ class JSONField(models.TextField):
 
     def get_prep_value(self, value):
         """Convert our JSON object to a string before we save"""
-        if isinstance(value, basestring):
+        if isinstance(value, str):
             return value
 
         if value is None:
diff --git a/helios_auth/migrations/0001_initial.py b/helios_auth/migrations/0001_initial.py
index fc54ff2aa58d29872fdd0453e87ffb9123974af8..002068a63be3355f5eb285660f1b0eb5b7514816 100644
--- a/helios_auth/migrations/0001_initial.py
+++ b/helios_auth/migrations/0001_initial.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals
 
 from django.db import models, migrations
+
 import helios_auth.jsonfield
 
 
diff --git a/helios_auth/models.py b/helios_auth/models.py
index fb050d225808599e95459e057f7ff0489e5c1ecf..15fa0253c7e16acc1200f61d0d527f2d01aa76fb 100644
--- a/helios_auth/models.py
+++ b/helios_auth/models.py
@@ -8,8 +8,8 @@ Ben Adida
 """
 from django.db import models
 
-from auth_systems import AUTH_SYSTEMS
-from jsonfield import JSONField
+from .auth_systems import can_check_constraint, AUTH_SYSTEMS
+from .jsonfield import JSONField
 
 
 # an exception to catch when a user is no longer authenticated
@@ -53,7 +53,7 @@ class User(models.Model):
     
     if not created_p:
       # special case the password: don't replace it if it exists
-      if obj.info.has_key('password'):
+      if 'password' in obj.info:
         info['password'] = obj.info['password']
 
       obj.info = info
@@ -64,7 +64,7 @@ class User(models.Model):
     return obj
     
   def can_update_status(self):
-    if not AUTH_SYSTEMS.has_key(self.user_type):
+    if self.user_type not in AUTH_SYSTEMS:
       return False
 
     return AUTH_SYSTEMS[self.user_type].STATUS_UPDATES
@@ -74,7 +74,7 @@ class User(models.Model):
     Certain auth systems can choose to limit election creation
     to certain users. 
     """
-    if not AUTH_SYSTEMS.has_key(self.user_type):
+    if self.user_type not in AUTH_SYSTEMS:
       return False
     
     return AUTH_SYSTEMS[self.user_type].can_create_election(self.user_id, self.info)
@@ -86,16 +86,16 @@ class User(models.Model):
     return AUTH_SYSTEMS[self.user_type].STATUS_UPDATE_WORDING_TEMPLATE
 
   def update_status(self, status):
-    if AUTH_SYSTEMS.has_key(self.user_type):
+    if self.user_type in AUTH_SYSTEMS:
       AUTH_SYSTEMS[self.user_type].update_status(self.user_id, self.info, self.token, status)
       
   def send_message(self, subject, body):
-    if AUTH_SYSTEMS.has_key(self.user_type):
+    if self.user_type in AUTH_SYSTEMS:
       subject = subject.split("\n")[0]
       AUTH_SYSTEMS[self.user_type].send_message(self.user_id, self.name, self.info, subject, body)
 
   def send_notification(self, message):
-    if AUTH_SYSTEMS.has_key(self.user_type):
+    if self.user_type in AUTH_SYSTEMS:
       if hasattr(AUTH_SYSTEMS[self.user_type], 'send_notification'):
         AUTH_SYSTEMS[self.user_type].send_notification(self.user_id, self.info, message)
   
@@ -110,17 +110,17 @@ class User(models.Model):
       return False
       
     # no constraint? Then eligible!
-    if not eligibility_case.has_key('constraint'):
+    if 'constraint' not in eligibility_case:
       return True
     
     # from here on we know we match the auth system, but do we match one of the constraints?  
 
-    auth_system = AUTH_SYSTEMS[self.user_type]
-
     # does the auth system allow for checking a constraint?
-    if not hasattr(auth_system, 'check_constraint'):
+    if not can_check_constraint(self.user_type):
       return False
-      
+
+    auth_system = AUTH_SYSTEMS[self.user_type]
+
     for constraint in eligibility_case['constraint']:
       # do we match on this constraint?
       if auth_system.check_constraint(constraint=constraint, user = self):
@@ -141,14 +141,14 @@ class User(models.Model):
     if self.name:
       return self.name
 
-    if self.info.has_key('name'):
+    if 'name' in self.info:
       return self.info['name']
 
     return self.user_id
   
   @property
   def public_url(self):
-    if AUTH_SYSTEMS.has_key(self.user_type):
+    if self.user_type in AUTH_SYSTEMS:
       if hasattr(AUTH_SYSTEMS[self.user_type], 'public_url'):
         return AUTH_SYSTEMS[self.user_type].public_url(self.user_id)
 
diff --git a/helios_auth/security/__init__.py b/helios_auth/security/__init__.py
index d3c5ac184a160d6790b1f04e8584341919dd3ef5..1fa30e1cf3d5edef11b7a445f4527235f0f567d8 100644
--- a/helios_auth/security/__init__.py
+++ b/helios_auth/security/__init__.py
@@ -12,7 +12,7 @@ from django.http import HttpResponseRedirect
 # nicely update the wrapper function
 from functools import update_wrapper
 
-import oauth
+from . import oauth
 from helios_auth.models import User
 
 FIELDS_TO_SAVE = 'FIELDS_TO_SAVE'
@@ -93,10 +93,10 @@ def get_user(request):
   # request.session.set_expiry(settings.SESSION_COOKIE_AGE)
   
   # set up CSRF protection if needed
-  if not request.session.has_key('csrf_token') or (type(request.session['csrf_token']) != str and type(request.session['csrf_token']) != unicode):
+  if 'csrf_token' not in request.session or not isinstance(request.session['csrf_token'], str):
     request.session['csrf_token'] = str(uuid.uuid4())
 
-  if request.session.has_key('user'):
+  if 'user' in request.session:
     user = request.session['user']
 
     # find the user
@@ -109,7 +109,7 @@ def check_csrf(request):
   if request.method != "POST":
     return HttpResponseNotAllowed("only a POST for this URL")
     
-  if (not request.POST.has_key('csrf_token')) or (request.POST['csrf_token'] != request.session['csrf_token']):
+  if ('csrf_token' not in request.POST) or (request.POST['csrf_token'] != request.session['csrf_token']):
     raise Exception("A CSRF problem was detected")
 
 def save_in_session_across_logouts(request, field_name, field_value):
diff --git a/helios_auth/security/oauth.py b/helios_auth/security/oauth.py
index 71676c89f039eebadbb1129081e8e96d2f638ee7..568272bd835eaae3a3f9238b821fab8b870222e8 100644
--- a/helios_auth/security/oauth.py
+++ b/helios_auth/security/oauth.py
@@ -6,14 +6,12 @@ Hacked a bit by Ben Adida (ben@adida.net) so that:
 - access tokens are looked up with an extra param of consumer
 """
 
-import urllib
-import time
-import random
-import urlparse
-import hmac
 import base64
+import hmac
 import logging
-import hashlib
+import random
+import time
+import urllib.parse
 
 VERSION = '1.0' # Hi Blaine!
 HTTP_METHOD = 'GET'
@@ -31,7 +29,7 @@ def build_authenticate_header(realm=''):
 # url escape
 def escape(s):
     # escape '/' too
-    return urllib.quote(s, safe='~')
+    return urllib.parse.quote(s, safe='~')
 
 # util function: current timestamp
 # seconds since epoch (UTC)
@@ -69,13 +67,13 @@ class OAuthToken(object):
         self.secret = secret
 
     def to_string(self):
-        return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
+        return urllib.parse.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
 
     # return a token from something like:
     # oauth_token_secret=digg&oauth_token=digg
     @staticmethod
     def from_string(s):
-        params = urlparse.parse_qs(s, keep_blank_values=False)
+        params = urllib.parse.parse_qs(s, keep_blank_values=False)
         key = params['oauth_token'][0]
         secret = params['oauth_token_secret'][0]
         return OAuthToken(key, secret)
@@ -124,7 +122,7 @@ class OAuthRequest(object):
     # get any non-oauth parameters
     def get_nonoauth_parameters(self):
         parameters = {}
-        for k, v in self.parameters.iteritems():
+        for k, v in self.parameters.items():
             # ignore oauth parameters
             if k.find('oauth_') < 0:
                 parameters[k] = v
@@ -135,7 +133,7 @@ class OAuthRequest(object):
         auth_header = 'OAuth realm="%s"' % realm
         # add the oauth parameters
         if self.parameters:
-            for k, v in self.parameters.iteritems():
+            for k, v in self.parameters.items():
               # only if it's a standard OAUTH param (Ben)
               if k in self.OAUTH_PARAMS:
                 auth_header += ', %s="%s"' % (k, escape(str(v)))
@@ -143,7 +141,7 @@ class OAuthRequest(object):
 
     # serialize as post data for a POST request
     def to_postdata(self):
-        return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems())
+        return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.items())
 
     # serialize as a url for a GET request
     def to_url(self):
@@ -157,7 +155,7 @@ class OAuthRequest(object):
             del params['oauth_signature']
         except:
             pass
-        key_values = params.items()
+        key_values = list(params.items())
         # sort lexicographically, first after key, then after value
         key_values.sort()
         # combine key value pairs in string and escape
@@ -169,7 +167,7 @@ class OAuthRequest(object):
 
     # parses the url and rebuilds it to be scheme://host/path
     def get_normalized_http_url(self):
-        parts = urlparse.urlparse(self.http_url)
+        parts = urllib.parse.urlparse(self.http_url)
         url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path
         return url_string
 
@@ -208,7 +206,7 @@ class OAuthRequest(object):
             parameters.update(query_params)
 
         # URL parameters
-        param_str = urlparse.urlparse(http_url)[4] # query
+        param_str = urllib.parse.urlparse(http_url)[4] # query
         url_params = OAuthRequest._split_url_string(param_str)
         parameters.update(url_params)
 
@@ -263,15 +261,15 @@ class OAuthRequest(object):
             # split key-value
             param_parts = param.split('=', 1)
             # remove quotes and unescape the value
-            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+            params[param_parts[0]] = urllib.parse.unquote(param_parts[1].strip('\"'))
         return params
 
     # util function: turn url string into parameters, has to do some unescaping
     @staticmethod
     def _split_url_string(param_str):
-        parameters = urlparse.parse_qs(param_str, keep_blank_values=False)
-        for k, v in parameters.iteritems():
-            parameters[k] = urllib.unquote(v[0])
+        parameters = urllib.parse.parse_qs(param_str, keep_blank_values=False)
+        for k, v in parameters.items():
+            parameters[k] = urllib.parse.unquote(v[0])
         return parameters
 
 # OAuthServer is a worker to check a requests validity against a data store
@@ -364,7 +362,7 @@ class OAuthServer(object):
             # get the signature method object
             signature_method = self.signature_methods[signature_method]
         except:
-            signature_method_names = ', '.join(self.signature_methods.keys())
+            signature_method_names = ', '.join(list(self.signature_methods.keys()))
             raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
 
         return signature_method
@@ -513,7 +511,12 @@ class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
         key, raw = self.build_signature_base_string(oauth_request, consumer, token)
 
         # hmac object
-        hashed = hmac.new(key, raw, hashlib.sha1)
+        try:
+            from Crypto.Hash import SHA1
+            hashed = hmac.new(key, raw, SHA1)
+        except:
+            import hashlib
+            hashed = hmac.new(key, raw, hashlib.sha1)
 
         # calculate the digest base 64
         return base64.b64encode(hashed.digest())
diff --git a/helios_auth/tests.py b/helios_auth/tests.py
index de21e3b09c600084194a47bd5cd0f73522a74daa..934933258f49fb8a64752fc241340a1ad4489789 100644
--- a/helios_auth/tests.py
+++ b/helios_auth/tests.py
@@ -3,16 +3,15 @@ Unit Tests for Auth Systems
 """
 
 import unittest
-import models
 
+from django.core import mail
 from django.db import IntegrityError, transaction
-
-from django.test.client import Client
 from django.test import TestCase
+from django.urls import reverse
 
-from django.core import mail
+from . import models, views
+from .auth_systems import AUTH_SYSTEMS, password as password_views
 
-from auth_systems import AUTH_SYSTEMS
 
 class UserModelTests(unittest.TestCase):
 
@@ -23,7 +22,7 @@ class UserModelTests(unittest.TestCase):
         """
         there should not be two users with the same user_type and user_id
         """
-        for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+        for auth_system, auth_system_module in AUTH_SYSTEMS.items():
             models.User.objects.create(user_type = auth_system, user_id = 'foobar', info={'name':'Foo Bar'})
             
             def double_insert():
@@ -36,22 +35,22 @@ class UserModelTests(unittest.TestCase):
         """
         shouldn't create two users, and should reset the password
         """
-        for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+        for auth_system, auth_system_module in AUTH_SYSTEMS.items():
             u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_cou', info={'name':'Foo Bar'})
 
             def double_update_or_create():
                 new_name = 'Foo2 Bar'
                 u2 = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_cou', info={'name': new_name})
 
-                self.assertEquals(u.id, u2.id)
-                self.assertEquals(u2.info['name'], new_name)
+                self.assertEqual(u.id, u2.id)
+                self.assertEqual(u2.info['name'], new_name)
 
 
     def test_can_create_election(self):
         """
         check that auth systems have the can_create_election call and that it's true for the common ones
         """
-        for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+        for auth_system, auth_system_module in AUTH_SYSTEMS.items():
             assert(hasattr(auth_system_module, 'can_create_election'))
             if auth_system != 'clever':
                 assert(auth_system_module.can_create_election('foobar', {}))
@@ -62,13 +61,13 @@ class UserModelTests(unittest.TestCase):
         check that a user set up with status update ability reports it as such,
         and otherwise does not report it
         """
-        for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+        for auth_system, auth_system_module in AUTH_SYSTEMS.items():
             u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_status_update', info={'name':'Foo Bar Status Update'})
 
             if hasattr(auth_system_module, 'send_message'):
-                self.assertNotEquals(u.update_status_template, None)
+                self.assertNotEqual(u.update_status_template, None)
             else:
-                self.assertEquals(u.update_status_template, None)
+                self.assertEqual(u.update_status_template, None)
 
     def test_eligibility(self):
         """
@@ -76,22 +75,18 @@ class UserModelTests(unittest.TestCase):
 
         FIXME: also test constraints on eligibility
         """
-        for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+        for auth_system, auth_system_module in AUTH_SYSTEMS.items():
             u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_status_update', info={'name':'Foo Bar Status Update'})
 
             self.assertTrue(u.is_eligible_for({'auth_system': auth_system}))
 
     def test_eq(self):
-        for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+        for auth_system, auth_system_module in AUTH_SYSTEMS.items():
             u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_eq', info={'name':'Foo Bar Status Update'})
             u2 = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_eq', info={'name':'Foo Bar Status Update'})
 
-            self.assertEquals(u, u2)
-
+            self.assertEqual(u, u2)
 
-import views
-import auth_systems.password as password_views
-from django.urls import reverse
 
 # FIXME: login CSRF should make these tests more complicated
 # and should be tested for
@@ -125,6 +120,6 @@ class UserBlackboxTests(TestCase):
         """using the test email backend"""
         self.test_user.send_message("testing subject", "testing body")
 
-        self.assertEquals(len(mail.outbox), 1)
-        self.assertEquals(mail.outbox[0].subject, "testing subject")
-        self.assertEquals(mail.outbox[0].to[0], "\"Foobar User\" <foobar-test@adida.net>")
+        self.assertEqual(len(mail.outbox), 1)
+        self.assertEqual(mail.outbox[0].subject, "testing subject")
+        self.assertEqual(mail.outbox[0].to[0], "\"Foobar User\" <foobar-test@adida.net>")
diff --git a/helios_auth/urls.py b/helios_auth/urls.py
index 5244e10b8a1e2f07f8ab712c3b8f3997e58bedfa..9fbc158f499f0df32cd3f31830990b537ed00f73 100644
--- a/helios_auth/urls.py
+++ b/helios_auth/urls.py
@@ -7,9 +7,8 @@ Ben Adida (ben@adida.net)
 
 from django.conf.urls import url
 
-import url_names
-import views
-from settings import AUTH_ENABLED_AUTH_SYSTEMS
+from settings import AUTH_ENABLED_SYSTEMS
+from . import views, url_names
 
 urlpatterns = [
     # basic static stuff
@@ -23,11 +22,11 @@ urlpatterns = [
 ]
 
 # password auth
-if 'password' in AUTH_ENABLED_AUTH_SYSTEMS:
-    from auth_systems.password import urlpatterns as password_patterns
+if 'password' in AUTH_ENABLED_SYSTEMS:
+    from .auth_systems.password import urlpatterns as password_patterns
     urlpatterns.extend(password_patterns)
 
 # twitter
-if 'twitter' in AUTH_ENABLED_AUTH_SYSTEMS:
-    from auth_systems.twitter import urlpatterns as twitter_patterns
+if 'twitter' in AUTH_ENABLED_SYSTEMS:
+    from .auth_systems.twitter import urlpatterns as twitter_patterns
     urlpatterns.extend(twitter_patterns)
diff --git a/helios_auth/utils.py b/helios_auth/utils.py
index f57dedf6e961e8598963049dca55f6cdaf211f00..dc723adce77a3f08c19e1a0438a8ae20f611e882 100644
--- a/helios_auth/utils.py
+++ b/helios_auth/utils.py
@@ -7,23 +7,30 @@ Some basic utils
 
 import json
 
+
 ## JSON
 def to_json(d):
-  return json.dumps(d, sort_keys=True)
-  
-def from_json(json_str):
-  if not json_str: return None
-  return json.loads(json_str)
-  
-def JSONtoDict(json):
-    x=json.loads(json)
-    return x
-    
-def JSONFiletoDict(filename):
-  f = open(filename, 'r')
-  content = f.read()
-  f.close()
-  return JSONtoDict(content)
-    
+    return json.dumps(d, sort_keys=True)
+
+
+def from_json(value):
+    if value == "" or value is None:
+        return None
 
+    if isinstance(value, str):
+        try:
+            return json.loads(value)
+        except Exception as e:
+            # import ast
+            # try:
+            #     parsed_value = ast.literal_eval(parsed_value)
+            # except Exception as e1:
+            raise Exception("value is not JSON parseable, that's bad news") from e
 
+    return value
+
+
+def JSONFiletoDict(filename):
+    with open(filename, 'r') as f:
+        content = f.read()
+    return from_json(content)
diff --git a/helios_auth/view_utils.py b/helios_auth/view_utils.py
index 6699ab28ab5030b4b8703ea4868f5ef1edd22a14..48b90c7806167ee9031a3c5e55a82f03adf9af37 100644
--- a/helios_auth/view_utils.py
+++ b/helios_auth/view_utils.py
@@ -52,7 +52,3 @@ def render_template_raw(request, template_name, values=None):
   vars_with_user = prepare_vars(request, values)
 
   return t.render(context=vars_with_user, request=request)
-
-
-def render_json(json_txt):
-  return HttpResponse(json_txt)
diff --git a/helios_auth/views.py b/helios_auth/views.py
index f246dafd582ef696d6ba2351fad93d507f7924eb..73f23050ba2f9767a94da17a2e5b180578c4c6be 100644
--- a/helios_auth/views.py
+++ b/helios_auth/views.py
@@ -5,19 +5,19 @@ Ben Adida
 2009-07-05
 """
 
-import urllib
-from django.urls import reverse
+from urllib.parse import urlencode
+
 from django.http import HttpResponseRedirect, HttpResponse
+from django.urls import reverse
 
-import helios_auth
 import settings
-from auth_systems import AUTH_SYSTEMS
-from auth_systems import password
+from helios_auth import DEFAULT_AUTH_SYSTEM, ENABLED_AUTH_SYSTEMS
 from helios_auth.security import get_user
 from helios_auth.url_names import AUTH_INDEX, AUTH_START, AUTH_AFTER, AUTH_WHY, AUTH_AFTER_INTERVENTION
-from models import User
-from security import FIELDS_TO_SAVE
-from view_utils import render_template, render_template_raw
+from .auth_systems import AUTH_SYSTEMS, password
+from .models import User
+from .security import FIELDS_TO_SAVE
+from .view_utils import render_template, render_template_raw
 
 
 def index(request):
@@ -28,21 +28,21 @@ def index(request):
   user = get_user(request)
 
   # single auth system?
-  if len(helios_auth.ENABLED_AUTH_SYSTEMS) == 1 and not user:
-    return HttpResponseRedirect(reverse(AUTH_START, args=[helios_auth.ENABLED_AUTH_SYSTEMS[0]])+ '?return_url=' + request.GET.get('return_url', ''))
+  if len(ENABLED_AUTH_SYSTEMS) == 1 and not user:
+    return HttpResponseRedirect(reverse(AUTH_START, args=[ENABLED_AUTH_SYSTEMS[0]])+ '?return_url=' + request.GET.get('return_url', ''))
 
-  #if helios_auth.DEFAULT_AUTH_SYSTEM and not user:
-  #  return HttpResponseRedirect(reverse(start, args=[helios_auth.DEFAULT_AUTH_SYSTEM])+ '?return_url=' + request.GET.get('return_url', ''))
+  #if DEFAULT_AUTH_SYSTEM and not user:
+  #  return HttpResponseRedirect(reverse(start, args=[DEFAULT_AUTH_SYSTEM])+ '?return_url=' + request.GET.get('return_url', ''))
   
   default_auth_system_obj = None
-  if helios_auth.DEFAULT_AUTH_SYSTEM:
-    default_auth_system_obj = AUTH_SYSTEMS[helios_auth.DEFAULT_AUTH_SYSTEM]
+  if DEFAULT_AUTH_SYSTEM:
+    default_auth_system_obj = AUTH_SYSTEMS[DEFAULT_AUTH_SYSTEM]
 
   #form = password.LoginForm()
 
   return render_template(request, 'index', {'return_url' : request.GET.get('return_url', '/'),
-                                            'enabled_auth_systems' : helios_auth.ENABLED_AUTH_SYSTEMS,
-                                            'default_auth_system': helios_auth.DEFAULT_AUTH_SYSTEM,
+                                            'enabled_auth_systems' : ENABLED_AUTH_SYSTEMS,
+                                            'default_auth_system': DEFAULT_AUTH_SYSTEM,
                                             'default_auth_system_obj': default_auth_system_obj})
 
 def login_box_raw(request, return_url='/', auth_systems = None):
@@ -50,20 +50,20 @@ def login_box_raw(request, return_url='/', auth_systems = None):
   a chunk of HTML that shows the various login options
   """
   default_auth_system_obj = None
-  if helios_auth.DEFAULT_AUTH_SYSTEM:
-    default_auth_system_obj = AUTH_SYSTEMS[helios_auth.DEFAULT_AUTH_SYSTEM]
+  if DEFAULT_AUTH_SYSTEM:
+    default_auth_system_obj = AUTH_SYSTEMS[DEFAULT_AUTH_SYSTEM]
 
   # make sure that auth_systems includes only available and enabled auth systems
   if auth_systems is not None:
-    enabled_auth_systems = set(auth_systems).intersection(set(helios_auth.ENABLED_AUTH_SYSTEMS)).intersection(set(AUTH_SYSTEMS.keys()))
+    enabled_auth_systems = set(auth_systems).intersection(set(ENABLED_AUTH_SYSTEMS)).intersection(set(AUTH_SYSTEMS.keys()))
   else:
-    enabled_auth_systems = set(helios_auth.ENABLED_AUTH_SYSTEMS).intersection(set(AUTH_SYSTEMS.keys()))
+    enabled_auth_systems = set(ENABLED_AUTH_SYSTEMS).intersection(set(AUTH_SYSTEMS.keys()))
 
   form = password.LoginForm()
 
   return render_template_raw(request, 'login_box', {
       'enabled_auth_systems': enabled_auth_systems, 'return_url': return_url,
-      'default_auth_system': helios_auth.DEFAULT_AUTH_SYSTEM, 'default_auth_system_obj': default_auth_system_obj,
+      'default_auth_system': DEFAULT_AUTH_SYSTEM, 'default_auth_system_obj': default_auth_system_obj,
       'form' : form})
   
 def do_local_logout(request):
@@ -74,7 +74,7 @@ def do_local_logout(request):
 
   user = None
 
-  if request.session.has_key('user'):
+  if 'user' in request.session:
     user = request.session['user']
     
   # 2010-08-14 be much more aggressive here
@@ -151,7 +151,7 @@ def _do_auth(request):
     return HttpResponse("an error occurred trying to contact " + system_name +", try again later")
   
 def start(request, system_name):
-  if not (system_name in helios_auth.ENABLED_AUTH_SYSTEMS):
+  if not (system_name in ENABLED_AUTH_SYSTEMS):
     return HttpResponseRedirect(reverse(AUTH_INDEX))
   
   # why is this here? Let's try without it
@@ -173,7 +173,7 @@ def perms_why(request):
 
 def after(request):
   # which auth system were we using?
-  if not request.session.has_key('auth_system_name'):
+  if 'auth_system_name' not in request.session:
     do_local_logout(request)
     return HttpResponseRedirect("/")
     
@@ -188,7 +188,7 @@ def after(request):
     
     request.session['user'] = user
   else:
-    return HttpResponseRedirect("%s?%s" % (reverse(AUTH_WHY), urllib.urlencode({'system_name' : request.session['auth_system_name']})))
+    return HttpResponseRedirect("%s?%s" % (reverse(AUTH_WHY), urlencode({'system_name' : request.session['auth_system_name']})))
 
   # does the auth system want to present an additional view?
   # this is, for example, to prompt the user to follow @heliosvoting
@@ -203,7 +203,7 @@ def after(request):
 
 def after_intervention(request):
   return_url = "/"
-  if request.session.has_key('auth_return_url'):
+  if 'auth_return_url' in request.session:
     return_url = request.session['auth_return_url']
     del request.session['auth_return_url']
   return HttpResponseRedirect(settings.URL_HOST + return_url)
diff --git a/heliosverifier/js/jscrypto/helios.js b/heliosverifier/js/jscrypto/helios.js
index 93c0ed48c317d21fee609d30a4108157b1fddbfd..f46a31bbb2db866ddf6acd59b120213d770b1fb8 100644
--- a/heliosverifier/js/jscrypto/helios.js
+++ b/heliosverifier/js/jscrypto/helios.js
@@ -609,13 +609,14 @@ HELIOS.dejsonify_list_of_lists = function(lol, item_dejsonifier) {
 }
 
 HELIOS.Trustee = Class.extend({
-  init: function(uuid, public_key, public_key_hash, pok, decryption_factors, decryption_proofs) {
+  init: function(uuid, public_key, public_key_hash, pok, decryption_factors, decryption_proofs, email) {
     this.uuid = uuid;
     this.public_key = public_key;
     this.public_key_hash = public_key_hash;
     this.pok = pok;
     this.decryption_factors = decryption_factors;
     this.decryption_proofs = decryption_proofs;
+    this.email = email;
   },
   
   toJSONObject: function() {
@@ -631,5 +632,7 @@ HELIOS.Trustee.fromJSONObject = function(d) {
   return new HELIOS.Trustee(d.uuid,
     ElGamal.PublicKey.fromJSONObject(d.public_key), d.public_key_hash, ElGamal.DLogProof.fromJSONObject(d.pok),
     HELIOS.dejsonify_list_of_lists(d.decryption_factors, BigInt.fromJSONObject),
-    HELIOS.dejsonify_list_of_lists(d.decryption_proofs, ElGamal.Proof.fromJSONObject));
-};
\ No newline at end of file
+    HELIOS.dejsonify_list_of_lists(d.decryption_proofs, ElGamal.Proof.fromJSONObject),
+    d.email
+   );
+};
diff --git a/requirements.txt b/requirements.txt
index 8e69fb261c4417d9c3c41cdd8a10c8911ecb4713..0b88e889d1dfaa36250db009fc41e40e1bea00c8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,23 +1,22 @@
 Django==1.11.28
-anyjson==0.3.3
+django_webtest>=1.9.7
+
+gunicorn==20.0.4
+
+dj_database_url==0.5.0
+psycopg2==2.8.4
+
 celery==4.2.1
-django-picklefield==0.3.0
-kombu==4.2.0
-html5lib==0.999
-psycopg2==2.7.3.2
-pyparsing==1.5.7
-python-dateutil>=1.5
-python-openid==2.2.5
-wsgiref==0.1.2
-gunicorn==19.9
-requests==2.21.0
-unicodecsv==0.9.0
-dj_database_url==0.3.0
-django_webtest>=1.9
-webtest==2.0.18
-bleach==1.4.1
-boto==2.27.0
-django-ses==0.6.0
-validate_email==1.2
-oauth2client==1.2
-rollbar==0.12.1
+django-picklefield==1.1.0
+
+python-dateutil>=2.8
+unicodecsv==0.14.1
+bleach==3.1.1
+validate_email==1.3
+pycryptodome==3.8.2
+
+python3-openid==3.0.10
+boto==2.49.0
+django-ses==0.8.14
+oauth2client==4.1.3
+rollbar==0.14.7
diff --git a/runtime.txt b/runtime.txt
index f27f1cc5ca4d2e750411e5ee682f6eb6baa674c2..4252f10667b7c2fce89b70b74097960c86c7cf6c 100644
--- a/runtime.txt
+++ b/runtime.txt
@@ -1 +1 @@
-python-2.7.15
+python-3.6.12
diff --git a/server_ui/urls.py b/server_ui/urls.py
index e691c07afe7b29c0b347e5bde5ea75e9addd65c7..aad77654242dcfa268cc23b7103de800b68b9dfa 100644
--- a/server_ui/urls.py
+++ b/server_ui/urls.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 from django.conf.urls import url
 
-from views import home, about, docs, faq, privacy
+from .views import home, about, docs, faq, privacy
 
 urlpatterns = [
   url(r'^$', home),
diff --git a/server_ui/view_utils.py b/server_ui/view_utils.py
index df947214620fc0169b9569767a84e633cd0e7615..f499603e108fdbb3db9a352df2e0fb6efb4ab2e9 100644
--- a/server_ui/view_utils.py
+++ b/server_ui/view_utils.py
@@ -20,7 +20,7 @@ def render_template(request, template_name, values = None):
   vars_with_user['CURRENT_URL'] = request.path
   
   # csrf protection
-  if request.session.has_key('csrf_token'):
+  if 'csrf_token' in request.session:
     vars_with_user['csrf_token'] = request.session['csrf_token']
   
   return render_to_response('server_ui/templates/%s.html' % template_name, vars_with_user)
diff --git a/server_ui/views.py b/server_ui/views.py
index 190d902aa71f4c0722ed68a787055a4f978ba7c8..5010dbc34f6b270af8ceb1aad8faab14686ebaaa 100644
--- a/server_ui/views.py
+++ b/server_ui/views.py
@@ -3,15 +3,15 @@ server_ui specific views
 """
 
 import copy
+
 from django.conf import settings
 
 import helios_auth.views as auth_views
 from helios.models import Election
 from helios.security import can_create_election
 from helios_auth.security import get_user
-from view_utils import render_template
-import glue
-
+from . import glue
+from .view_utils import render_template
 
 glue.glue()  # actually apply glue helios.view <-> helios.signals
 
@@ -36,7 +36,7 @@ def home(request):
   else:
     elections_voted = None
  
-  auth_systems = copy.copy(settings.AUTH_ENABLED_AUTH_SYSTEMS)
+  auth_systems = copy.copy(settings.AUTH_ENABLED_SYSTEMS)
   try:
     auth_systems.remove('password')
   except: pass
diff --git a/settings.py b/settings.py
index b7266d2cf66f9d3c0cf34d4fd05285f88eb67459..f4dddc3bd18e1466f77c5ccb6b1eddaace743d6b 100644
--- a/settings.py
+++ b/settings.py
@@ -9,7 +9,7 @@ TESTING = 'test' in sys.argv
 
 # go through environment variables and override them
 def get_from_env(var, default):
-    if not TESTING and os.environ.has_key(var):
+    if not TESTING and var in os.environ:
         return os.environ[var]
     else:
         return default
@@ -38,19 +38,16 @@ SHOW_USER_INFO = (get_from_env('SHOW_USER_INFO', '1') == '1')
 DATABASES = {
     'default': {
         'ENGINE': 'django.db.backends.postgresql_psycopg2',
-        'NAME': 'helios'
-    }
+        'NAME': 'helios',
+        'CONN_MAX_AGE': 600,
+    },
 }
 
 # override if we have an env variable
 if get_from_env('DATABASE_URL', None):
     import dj_database_url
-    DATABASES['default'] =  dj_database_url.config()
+    DATABASES['default'] = dj_database_url.config(conn_max_age=600, ssl_require=True)
     DATABASES['default']['ENGINE'] = 'django.db.backends.postgresql_psycopg2'
-    DATABASES['default']['CONN_MAX_AGE'] = 600
-
-    # require SSL
-    DATABASES['default']['OPTIONS'] = {'sslmode': 'require'}
 
 # Local time zone for this installation. Choices can be found here:
 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
@@ -211,9 +208,11 @@ HELIOS_VOTERS_EMAIL = True
 HELIOS_PRIVATE_DEFAULT = False
 
 # authentication systems enabled
-#AUTH_ENABLED_AUTH_SYSTEMS = ['password','facebook','twitter', 'google', 'yahoo']
-AUTH_ENABLED_AUTH_SYSTEMS = get_from_env('AUTH_ENABLED_AUTH_SYSTEMS', 'google').split(",")
-AUTH_DEFAULT_AUTH_SYSTEM = get_from_env('AUTH_DEFAULT_AUTH_SYSTEM', None)
+# AUTH_ENABLED_SYSTEMS = ['password','facebook','twitter', 'google', 'yahoo']
+AUTH_ENABLED_SYSTEMS = get_from_env('AUTH_ENABLED_SYSTEMS',
+                                    get_from_env('AUTH_ENABLED_AUTH_SYSTEMS', 'password,google,facebook')
+                                    ).split(",")
+AUTH_DEFAULT_SYSTEM = get_from_env('AUTH_DEFAULT_SYSTEM', get_from_env('AUTH_DEFAULT_AUTH_SYSTEM', None))
 
 # google
 GOOGLE_CLIENT_ID = get_from_env('GOOGLE_CLIENT_ID', '')
@@ -262,12 +261,12 @@ if get_from_env('EMAIL_USE_AWS', '0') == '1':
 
 # set up logging
 import logging
+
 logging.basicConfig(
-    level = logging.DEBUG,
-    format = '%(asctime)s %(levelname)s %(message)s'
+    level=logging.DEBUG if TESTING else logging.INFO,
+    format='%(asctime)s %(levelname)s %(message)s'
 )
 
-
 # set up celery
 CELERY_BROKER_URL = get_from_env('CELERY_BROKER_URL', 'amqp://localhost')
 if TESTING:
@@ -277,7 +276,7 @@ if TESTING:
 # Rollbar Error Logging
 ROLLBAR_ACCESS_TOKEN = get_from_env('ROLLBAR_ACCESS_TOKEN', None)
 if ROLLBAR_ACCESS_TOKEN:
-  print "setting up rollbar"
+  print("setting up rollbar")
   MIDDLEWARE += ['rollbar.contrib.django.middleware.RollbarNotifierMiddleware',]
   ROLLBAR = {
     'access_token': ROLLBAR_ACCESS_TOKEN,