view vote-counter.py @ 5:58e4b36d5e74

Clean up the code
author "German Poo-Caaman~o <gpoo@gnome.org>"
date Thu, 14 Dec 2006 10:22:46 -0300
parents ffa9fad76d38
children 80df109584f7
line wrap: on
line source

#! /usr/bin/python
#
# Usage: python vote-counter.py mbox secret-cookie addresses

import re
import sys
import string
import md5

MAX_CANDIDATES = 1

candidate_tuples = [ \
    ("UNIVERSIDAD TECNICA FEDERICO SANTA MARIA", 1), \
    ("UNIVERSIDAD DE CONCEPCION", 2) ]

class Ballot:
    def __init__ (self):
        self.email = 0
        self.member_name = 0
        self.member = 0
        self.token = 0
        self.votes = []

    def add_vote(self, name, id):
        self.votes.append((name, id))

class Candidate:
    def __init__(self, name, id):
        self.name = name
        self.id = id
        self.count = 0
        self.voters = []

# hash from valid addresses to whether they have sent in a ballot yet
class Voters(dict):
    def load_from_file(self, filename):
        def unmunge_email(addr):
            unmunged = nospam_re.sub("", addr)
            return unmunged

        nospam_re = re.compile("no_spam\.?")
        comment_re = re.compile("^#.*")
        entry_re = re.compile(" *(.*?)(<.*?>) *\((.*?)\) *")

        fp = open(filename)

        for line in fp.readlines():
            line = comment_re.sub("", line)
            string.strip(line)
            if line == "" or line == "\n":
                continue

            match = entry_re.search(line)
            if match:
                name = string.strip(match.group(1))
                email = unmunge_email(string.strip(match.group(2)))
                contribution = string.strip(match.group(3))
                self[string.strip(email)] = 0
            else:
                print "No match: " + line

        fp.close()

    def valid_voter(self, addr):
        return self.has_key(addr)

def get_ballots_from_mbox(filename, candidates):
    ballots = []
    current_ballot = 0

    from_line_re = re.compile ("^From: *(.*)")
    member_name_re = re.compile (">? *Member: *(.*)")
    member_address_re = re.compile (">? *Member Address: *([^ ]*)")
    auth_token_re = re.compile (">? *Validation Token: *(.*)")
    vote_re = re.compile (">? *([A-Z ]+) *\(ID# *([0-9]+)\)")

    fp = open(filename)
    for line in fp.readlines():
        match = from_line_re.match(line)
        if match:
            email = string.strip(match.group(1))
            if current_ballot:
                ballots.append(current_ballot)
            current_ballot = Ballot()
            current_ballot.email = email

            continue

        match = member_name_re.match(line)
        if match:
            member = string.strip(match.group(1))
            if (current_ballot.member_name):
                print "Duplicate member name in ballot from '%s' - " \
                      "duplicates ''%s', '%s'" \
                      % (current_ballot.email, current_ballot.member_name, 
                         member)
            else:        
                current_ballot.member_name = member

            continue
                
        match = member_address_re.match(line)
        if match:
            member = string.strip(match.group(1))
            if (current_ballot.member):
                print "Duplicate member address in ballot from '%s' - " \
                      "duplicates ''%s', '%s'" \
                      % (current_ballot.email, current_ballot.member, member)
            else:        
                current_ballot.member = member

            continue
                
        match = auth_token_re.match(line)
        if match:
            token = string.strip(match.group(1))
            if (current_ballot.token):
                print "Duplicate auth token in ballot from '%s' - " \
                      "duplicates '%s', '%s'" \
                      % (current_ballot.email, current_ballot.token, token)
            else:
                current_ballot.token = token
            continue

        match = vote_re.match(line)
        if match:
            name = string.strip(match.group(1))
            id = string.strip(match.group(2))

            id = int(id)

            if not candidates.has_key(id):
                print "Unknown candidate '%s' ID %d in ballot from '%s'" \
                      % (name, id, current_ballot.email)
            elif not candidates[id].name == name:
                print "Candidate name '%s' for ID '%s' doesn't match, " \
                      "expected '%s'" % (name, id, candidates[id].name)    
            else:
                current_ballot.add_vote(name, id)
            continue
        
    if current_ballot:
        ballots.append(current_ballot)    
        
    fp.close ()

    return ballots

def check_ballots(ballots, valid_addressess):
    valid_ballots = {}

    def contains_dups (b):
        dups = {}
        for (name, id) in b.votes:
            if dups.has_key (id):
                return True
            dups[id] = 1
        return False

    dup_tokens = {}
    def md5_is_bad(b):
        #key = b.member + secret_cookie
        key = "%s%s" % (b.member, secret_cookie)
        m = md5.new(key)
        digest = m.digest()
        token = m.hexdigest()
        if token == b.token:
            if dup_tokens.has_key(token):
                print "Auth token occurs twice, someone voted more than once"
                return True
            else:
                dup_tokens[token] = 1
            return False
        else:
            print "Bad auth token is %s hashed from '%s'" % (token, key)
            return True

    i = 0
    for b in ballots:
        error = 0
        if not b.member:
            error = "missing member address"
        elif not b.token:
            error = "missing auth token"
        elif len(b.votes) > MAX_CANDIDATES:
            error = "too many votes (%d votes)" % len(b.votes)
        elif len(b.votes) == 0:
            error = "didn't list any candidates"
        elif contains_dups(b):
            error = "contains duplicate votes for the same candidate"
        elif md5_is_bad(b):
            error = "bad authentication token"
        elif not valid_addresses.valid_voter(b.member):
            error = "ballot from someone not in the list of valid voters"
        else:
            if valid_ballots.has_key(b.token):
                old = valid_ballots[b.token]
                print "Overriding previous valid ballot %d from %s with " \
                      "new ballot %d" %  (old[1], old[0].email, i)
            valid_ballots[b.token] = (b, i)

        if error:
            print "Ignoring ballot %d from '%s' due to: %s" \
                  % (i, b.email, error)
            
        i = i + 1

    return valid_ballots

def print_valid_ballots(valid_ballots, valid_addresses, candidates):
    valids = valid_ballots.values()
    valids.sort(lambda a, b: cmp(a[1], b[1]))

    for (b, i) in valids:
        print "Ballot %d:" % i

        print "  From:   " + b.email
        print "  Member: " + b.member_name
        print "  Member Address: " + b.member
        print "  Token:  " + b.token
        print "  Voted for %d candidates:" % len(b.votes)

        voted_for = []

        valid_addresses[b.member] = 1

        for (name, id) in b.votes:
            candidates[id].count += 1
            candidates[id].voters.append(b.member)
            voted_for.append(candidates[id].name)

        for v in voted_for:
            print "   " + v

def print_voters_without_ballot(valid_addresses):
    print "\nThe following members did not vote:"
    for addr in valid_addresses.keys ():
        if not valid_addresses[addr]:
            print addr

def print_summary(valid_ballots, valid_addresses, candidates):
    candidate_list = candidates.values()
    candidate_list.sort(lambda a, b: cmp(b.count, a.count))

    print "\n\nELECTION RESULTS:"

    print " %d of %d members cast a valid ballot" \
          % (len(valid_ballots), len(valid_addresses.keys()))

    for candidate in candidate_list:
        print "  %s (%d votes)" % (candidate.name, candidate.count)


if __name__ == '__main__':
    candidates = {}
    for (name, id) in candidate_tuples:
        candidates[id] = Candidate(name, id)

    try:
        mbox_filename = sys.argv[1]     # mail archive file 
        secret_cookie = sys.argv[2]     # secret cookie
        voter_filename = sys.argv[3]    # valid voter addresses file
    except:
        print "Usage: %s mbox secret-cookie list-of-voters" % sys.argv[0]
        sys.exit(1)

    valid_addresses = Voters()
    valid_addresses.load_from_file(voter_filename)

    ballots = get_ballots_from_mbox(mbox_filename, candidates)

    valid_ballots = check_ballots(ballots, valid_addresses)

    ## Print results only after all errors have been printed, so
    ## we don't lose any errors.
    print_valid_ballots(valid_ballots, valid_addresses, candidates)
    print_voters_without_ballot(valid_addresses)

    print_summary(valid_ballots, valid_addresses, candidates)

# vi:set ts=4 sw=4 expandtab: