You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@steve.apache.org by hu...@apache.org on 2015/03/23 13:45:11 UTC

svn commit: r1668620 - in /steve/trunk/pysteve/lib: ./ __init__.py constants.py election.py form.py response.py voter.py

Author: humbedooh
Date: Mon Mar 23 12:45:11 2015
New Revision: 1668620

URL: http://svn.apache.org/r1668620
Log:
move libs (symlink to come when I figure out how here..)

Added:
    steve/trunk/pysteve/lib/
    steve/trunk/pysteve/lib/__init__.py
    steve/trunk/pysteve/lib/constants.py
    steve/trunk/pysteve/lib/election.py
    steve/trunk/pysteve/lib/form.py
    steve/trunk/pysteve/lib/response.py
    steve/trunk/pysteve/lib/voter.py

Added: steve/trunk/pysteve/lib/__init__.py
URL: http://svn.apache.org/viewvc/steve/trunk/pysteve/lib/__init__.py?rev=1668620&view=auto
==============================================================================
--- steve/trunk/pysteve/lib/__init__.py (added)
+++ steve/trunk/pysteve/lib/__init__.py Mon Mar 23 12:45:11 2015
@@ -0,0 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""
+Stuff
+"""
\ No newline at end of file

Added: steve/trunk/pysteve/lib/constants.py
URL: http://svn.apache.org/viewvc/steve/trunk/pysteve/lib/constants.py?rev=1668620&view=auto
==============================================================================
--- steve/trunk/pysteve/lib/constants.py (added)
+++ steve/trunk/pysteve/lib/constants.py Mon Mar 23 12:45:11 2015
@@ -0,0 +1,17 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+VALID_VOTE_TYPES = ['yna','stv1','stv2','stv3','stv4','stv5','stv6','stv7','stv8','stv9']
\ No newline at end of file

Added: steve/trunk/pysteve/lib/election.py
URL: http://svn.apache.org/viewvc/steve/trunk/pysteve/lib/election.py?rev=1668620&view=auto
==============================================================================
--- steve/trunk/pysteve/lib/election.py (added)
+++ steve/trunk/pysteve/lib/election.py Mon Mar 23 12:45:11 2015
@@ -0,0 +1,319 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import hashlib
+import json
+import os
+import random
+import time
+
+from __main__ import homedir, config
+
+
+def exists(election, *issue):
+    "Returns True if an election/issue exists, False otherwise"
+    elpath = os.path.join(homedir, "issues", election)
+    if issue:
+        elpath += "/" + issue[0] + ".json"
+        return os.path.isfile(elpath)
+    else:
+        return os.path.isdir(elpath)
+
+
+def getBasedata(election, hideHash=False):
+    "Get base data from an election"
+    elpath = os.path.join(homedir, "issues", election)
+    if os.path.isdir(elpath):
+        with open(elpath + "/basedata.json", "r") as f:
+            data = f.read()
+            f.close()
+            basedata = json.loads(data)
+            if hideHash and 'hash' in basedata:
+                del basedata['hash']
+            basedata['id'] = election
+            return basedata
+    return None
+
+def close(election, reopen = False):
+    "Mark an election as closed"
+    elpath = os.path.join(homedir, "issues", election)
+    if os.path.isdir(elpath):
+        basedata = {}
+        with open(elpath + "/basedata.json", "r") as f:
+            data = f.read()
+            f.close()
+            basedata = json.loads(data)
+        if reopen:
+            basedata['closed'] = False
+        else:
+            basedata['closed'] = True
+        with open(elpath + "/basedata.json", "w") as f:
+            f.write(json.dumps(basedata))
+            f.close()
+
+def getIssue(electionID, issueID):
+    "Get JSON data from an issue"
+    issuepath = os.path.join(homedir, "issues", electionID, issueID) + ".json"
+    issuedata = None
+    if os.path.isfile(issuepath):
+        with open(issuepath, "r") as f:
+            data = f.read()
+            f.close()
+            issuedata = json.loads(data)
+        issuedata['id'] = issueID
+        issuedata['APIURL'] = "https://%s/steve/voter/view/%s/%s" % (config.get("general", "rooturl"), electionID, issueID)
+        issuedata['prettyURL'] = "https://%s/steve/ballot?%s/%s" % (config.get("general", "rooturl"), electionID, issueID)
+    return issuedata
+
+
+def getVotes(electionID, issueID):
+    "Read votes from the vote file"
+    issuepath = os.path.join(homedir, "issues", electionID, issueID) + ".json.votes"
+    issuedata = {}
+    if os.path.isfile(issuepath):
+        with open(issuepath, "r") as f:
+            data = f.read()
+            f.close()
+            issuedata = json.loads(data)
+    return issuedata
+
+def createElection(eid, title, owner, monitors, starts, ends, isopen):
+    elpath = os.path.join(homedir, "issues", eid)
+    os.mkdir(elpath)
+    with open(elpath  + "/basedata.json", "w") as f:
+        f.write(json.dumps({
+            'title': title,
+            'owner': owner,
+            'monitors': monitors,
+            'starts': starts,
+            'ends': ends,
+            'hash': hashlib.sha512("%f-stv-%s" % (time.time(), os.environ['REMOTE_ADDR'])).hexdigest(),
+            'open': isopen
+        }))
+        f.close()
+    with open(elpath  + "/voters.json", "w") as f:
+        f.write("{}")
+        f.close()
+
+
+def listIssues(election):
+    "List all issues in an election"
+    issues = []
+    elpath = os.path.join(homedir, "issues", election)
+    if os.path.isdir(elpath):
+        issues = [f.strip(".json") for f in os.listdir(elpath) if os.path.isfile(os.path.join(elpath, f)) and f != "basedata.json" and f != "voters.json" and f.endswith(".json")]
+    return issues
+
+def listElections():
+    "List all elections"
+    elections = []
+    path = os.path.join(homedir, "issues")
+    if os.path.isdir(path):
+        elections = [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))]
+    return elections
+
+def vote(electionID, issueID, voterID, vote):
+    "Casts a vote on an issue"
+    votes = {}
+    basedata = getBasedata(electionID)
+    if basedata:
+        issuepath = os.path.join(homedir, "issues", electionID, issueID) + ".json"
+        if os.path.isfile(issuepath + ".votes"):
+            with open(issuepath + ".votes", "r") as f:
+                votes = json.loads(f.read())
+                f.close()
+        votes[voterID] = vote
+        with open(issuepath + ".votes", "w") as f:
+            f.write(json.dumps(votes))
+            f.close()
+        votehash = hashlib.sha224(basedata['hash'] + issueID + voterID + vote).hexdigest()
+        return votehash
+    else:
+        raise Exception("No such election")
+
+
+def invalidate(issueData, vote):
+    "Tries to invalidate a vote, returns why if succeeded, None otherwise"
+    letters = ['y', 'n', 'a']
+    if issueData['type'].find("stv") == 0:
+        letters = [chr(i) for i in range(ord('a'), ord('a') + len(issueData['candidates']))]
+    for char in letters:
+        if vote.count(char) > 1:
+            return "Duplicate letters found"
+    for char in vote:
+        if char not in letters:
+            return "Invalid characters in vote. Accepted are: %s" % ", ".join(letters)
+    return None
+
+
+def deleteIssue(electionID, issueID):
+    "Deletes an issue if it exists"
+    if exists(electionID):
+        issuepath = os.path.join(homedir, "issues", electionID, issueID) + ".json"
+        if os.path.isfile(issuepath):
+            os.unlink(issuepath)
+        if os.path.isfile(issuepath + ".votes"):
+            os.unlink(issuepath + ".votes")
+        return True
+    else:
+        raise Exception("No such election")
+
+
+
+def yna(votes):
+    """ Simple YNA tallying
+    :param votes: The JSON object from $issueid.json.votes
+    :return: y,n,a as numbers
+    """
+    y = n = a = 0
+    for vote in votes.values():
+        if vote == 'y':
+            y += 1
+        if vote == 'n':
+            n += 1
+        if vote == 'a':
+            a += 1
+
+    return y, n, a
+
+
+def getproportion(votes, winners, step, surplus):
+    """ Proportionally move votes
+    :param votes:
+    :param winners:
+    :param step:
+    :param surplus:
+    :return:
+    """
+    prop = {}
+    tvotes = 0
+    for key in votes:
+        vote = votes[key]
+        xstep = step
+        char = vote[xstep] if len(vote) > xstep else None
+        # Step through votes till we find a non-winner vote
+        while xstep < len(vote) and vote[xstep] in winners:
+            xstep += 1
+        if xstep >= step:
+            tvotes += 1
+        # We found it? Good, let's add that to the tally
+        if xstep < len(vote) and not vote[xstep] in winners:
+            char = vote[xstep]
+            prop[char] = (prop[char] if char in prop else 0) + 1
+
+    # If this isn't the initial 1st place tally, do the proportional math:
+    # surplus votes / votes with an Nth preference * number of votes in that preference for the candidate
+    if step > 0:
+        for c in prop:
+            prop[c] *= (surplus / tvotes) if surplus > 0 else 0
+    return prop
+
+
+def stv(candidates, votes, numseats, shuffle = False):
+    """ Calculate N winners using STV
+    :param candidates:
+    :param votes:
+    :param int numseats: the number of seats available
+    :param shuffle: Whether or not to shuffle winners
+    :return:
+    """
+
+    debug = []
+    
+    # Set up letters for mangling
+    letters = [chr(i) for i in range(ord('a'), ord('a') + len(candidates))]
+    cc = "".join(letters)
+
+    # Keep score of votes
+    points = {}
+
+    # Set all scores to 0 at first
+    for c in cc:
+        points[c] = 0
+
+    # keep score of winners
+    winners = []
+    turn = 0
+
+    # Find quota to win a seat
+    quota = ( len(votes) / (numseats + 1) ) + 1
+    debug.append("Seats available: %u. Votes cast: %u" % (numseats, len(votes)))
+    debug.append("Votes required to win a seat: %u ( (%u/(%u+1))+1 )" % (quota, len(votes), numseats))
+
+    
+    surplus = 0
+    # While we still have seats to fill
+    if not len(candidates) < numseats:
+        y = 0
+        while len(winners) < numseats and len(cc) > 0 and turn < 1000:  #Don't run for > 1000 iterations, that's a bug
+            turn += 1
+
+            s = 0
+            
+            # Get votes
+            xpoints = getproportion(votes, winners, 0, surplus)
+            surplus = 0
+            if turn == 1:
+                debug.append("Initial tally: %s" % json.dumps(xpoints))
+            else:
+                debug.append("Proportional move: %s" % json.dumps(xpoints))
+                
+            for x in xpoints:
+                points[x] += xpoints[x]
+            mq = 0
+
+            # For each candidate letter, find if someone won a seat
+            for c in cc:
+                if len(winners) >= numseats:
+                    break
+                if points[c] >= quota and not c in winners:
+                    winners.append(c)
+                    debug.append("WINNER: %s got elected in with %u votes! %u seats remain" % (c, points[c], numseats - len(winners)))
+                    cc.replace(c, "")
+                    mq += 1
+                    surplus += points[c] - quota
+
+            # If we found no winners in this round, eliminate the lowest scorer and retally
+            if mq < 1:
+                lowest = 99999999
+                lowestC = None
+                for c in cc:
+                    if points[c] < lowest:
+                        lowest = points[c]
+                        lowestC = c
+
+                debug.append("DRAW: %s is eliminated" % lowestC)
+                if lowestC:
+                    cc.replace(lowestC, "")
+                else:
+                    debug.append("No more canididates?? buggo?")
+                    break
+            y += 1
+
+    # Everyone's a winner!!
+    else:
+        winners = letters
+
+    # Compile list of winner names
+    winnernames = []
+    if shuffle:
+        random.shuffle(winners)
+    for c in winners:
+        i = ord(c) - ord('a')
+        winnernames.append(candidates[i]['name'])
+
+    # Return the data
+    return winners, winnernames, debug
\ No newline at end of file

Added: steve/trunk/pysteve/lib/form.py
URL: http://svn.apache.org/viewvc/steve/trunk/pysteve/lib/form.py?rev=1668620&view=auto
==============================================================================
--- steve/trunk/pysteve/lib/form.py (added)
+++ steve/trunk/pysteve/lib/form.py Mon Mar 23 12:45:11 2015
@@ -0,0 +1,29 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import hashlib, json, random, os, sys, time
+from __main__ import homedir, config
+import cgi
+
+
+xform = cgi.FieldStorage();
+
+def getvalue(key):
+    val = xform.getvalue(key)
+    if val:
+        return val.replace("<", "&lt;")
+    else:
+        return None
\ No newline at end of file

Added: steve/trunk/pysteve/lib/response.py
URL: http://svn.apache.org/viewvc/steve/trunk/pysteve/lib/response.py?rev=1668620&view=auto
==============================================================================
--- steve/trunk/pysteve/lib/response.py (added)
+++ steve/trunk/pysteve/lib/response.py Mon Mar 23 12:45:11 2015
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import json
+
+responseCodes = {
+    200: 'Okay',
+    201: 'Created',
+    206: 'Partial content',
+    304: 'Not Modified',
+    400: 'Bad Request',
+    403: 'Access denied',
+    404: 'Not Found',
+    410: 'Gone',
+    500: 'Server Error'
+}
+
+def respond(code, js):
+    c = responseCodes[code] if code in responseCodes else "Unknown Response Code(?)"
+    out = json.dumps(js, indent=4)
+    print("Status: %u %s\r\nContent-Type: application/json\r\nCache-Control: no-cache\r\nContent-Length: %u\r\n" % (code, c, len(out)))
+    print(out)
+    
+    
+    
\ No newline at end of file

Added: steve/trunk/pysteve/lib/voter.py
URL: http://svn.apache.org/viewvc/steve/trunk/pysteve/lib/voter.py?rev=1668620&view=auto
==============================================================================
--- steve/trunk/pysteve/lib/voter.py (added)
+++ steve/trunk/pysteve/lib/voter.py Mon Mar 23 12:45:11 2015
@@ -0,0 +1,94 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import hashlib, json, random, os, sys, time
+from __main__ import homedir, config
+
+# SMTP Lib
+import smtplib
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+from smtplib import SMTPException
+
+
+
+def get(election, basedata, uid):
+    elpath = os.path.join(homedir, "issues", election)
+    with open(elpath + "/voters.json", "r") as f:
+        voters = json.loads(f.read())
+        f.close()
+        xhash = hashlib.sha512(basedata['hash'] + uid).hexdigest()
+        for voter in voters:
+            if voters[voter] == xhash:
+                return voter
+    return None
+        
+def add(election, basedata, email):
+    uid = hashlib.sha224("%s%s%s%s" % (email, basedata['hash'], time.time(), random.randint(1,99999999))).hexdigest()
+    xhash = hashlib.sha512(basedata['hash'] + uid).hexdigest()
+    elpath = os.path.join(homedir, "issues", election)
+    with open(elpath + "/voters.json", "r") as f:
+        voters = json.loads(f.read())
+        f.close()
+    voters[email] = xhash
+    with open(elpath + "/voters.json", "w") as f:
+        f.write(json.dumps(voters))
+        f.close()
+    return uid, xhash
+
+def remove(election, basedata, email):
+    elpath = os.path.join(homedir, "issues", election)
+    with open(elpath + "/voters.json", "r") as f:
+        voters = json.loads(f.read())
+        f.close()
+    if email in voters:
+        del voters[email]
+    with open(elpath + "/voters.json", "w") as f:
+        f.write(json.dumps(voters))
+        f.close()
+
+def hasVoted(election, issue, uid):
+    issue = issue.strip(".json")
+    path = os.path.join(homedir, "issues", election, issue)
+    votes = {}
+    if os.path.isfile(path + ".json.votes"):
+        with open(path + ".json.votes", "r") as f:
+            votes = json.loads(f.read())
+            f.close()
+    return True if uid in votes else False
+
+def email(rcpt, subject, message):
+    sender = config.get("email", "sender")
+    signature = config.get("email", "signature")
+    receivers = [rcpt]
+    msg = """From: %s
+To: %s
+Subject: %s
+
+%s
+
+With regards,
+%s
+--
+Powered by Apache STeVe - https://steve.apache.org
+""" % (sender, rcpt, subject, message, signature)
+    
+    try:
+       smtpObj = smtplib.SMTP(config.get("email", "mta"))
+       smtpObj.sendmail(sender, receivers, msg)         
+    except SMTPException:
+       raise Exception("Could not send email - SMTP server down?")
+       
\ No newline at end of file