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("<", "<")
+ 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