Author: humbedooh
Date: Tue Mar 24 20:15:45 2015
New Revision: 1668981
URL: http://svn.apache.org/r1668981
Log:
Start work on modularizing the different vote types.
For now, STV voting is handled/checked by stv.py, YNA by yna.py in the plugins
dir.
This will also mean upcoming changes to the admin rest api
Added:
steve/trunk/pysteve/lib/plugins/
steve/trunk/pysteve/lib/plugins/__init__.py
steve/trunk/pysteve/lib/plugins/stv.py
steve/trunk/pysteve/lib/plugins/yna.py
Modified:
steve/trunk/pysteve/lib/constants.py
steve/trunk/pysteve/lib/election.py
steve/trunk/pysteve/www/cgi-bin/rest_admin.py
steve/trunk/pysteve/www/htdocs/js/steve_rest.js
Modified: steve/trunk/pysteve/lib/constants.py
URL:
http://svn.apache.org/viewvc/steve/trunk/pysteve/lib/constants.py?rev=1668981&r1=1668980&r2=1668981&view=diff
==============================================================================
--- steve/trunk/pysteve/lib/constants.py (original)
+++ steve/trunk/pysteve/lib/constants.py Tue Mar 24 20:15:45 2015
@@ -14,4 +14,8 @@
# 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
+
+VOTE_TYPES = (
+
+)
+
Modified: steve/trunk/pysteve/lib/election.py
URL:
http://svn.apache.org/viewvc/steve/trunk/pysteve/lib/election.py?rev=1668981&r1=1668980&r2=1668981&view=diff
==============================================================================
--- steve/trunk/pysteve/lib/election.py (original)
+++ steve/trunk/pysteve/lib/election.py Tue Mar 24 20:15:45 2015
@@ -22,6 +22,8 @@ import time
from __main__ import homedir, config
+import constants
+from plugins import *
def exists(election, *issue):
"Returns True if an election/issue exists, False otherwise"
@@ -79,6 +81,13 @@ def getIssue(electionID, issueID):
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)
+
+ # Add vote category for JS magic
+ for vtype in constants.VOTE_TYPES:
+ if vtype['key'] == issuedata['type']:
+ issuedata['category'] = vtype['category']
+ break
+
return issuedata
@@ -156,20 +165,24 @@ def getVotes(electionID, issueID):
return votes
else:
return {}
+
+def validType(issueType):
+ for voteType in constants.VOTE_TYPES:
+ if voteType['key'] == issueType:
+ return True
+ return False
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
-
+ for voteType in constants.VOTE_TYPES:
+ if voteType['key'] == issueData['type']:
+ return voteType['validate_func'](vote, issueData)
+ return "Invalid vote type!"
+
+def tally(votes, issue):
+ for voteType in constants.VOTE_TYPES:
+ if voteType['key'] == issue['type']:
+ return voteType['tally_func'](votes, issue)
+ raise Exception("Invalid vote type!")
def deleteIssue(electionID, issueID):
"Deletes an issue if it exists"
@@ -184,152 +197,6 @@ def deleteIssue(electionID, issueID):
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
-
def getHash(electionID):
basedata = getBasedata(electionID)
issues = listIssues(electionID)
Added: steve/trunk/pysteve/lib/plugins/__init__.py
URL:
http://svn.apache.org/viewvc/steve/trunk/pysteve/lib/plugins/__init__.py?rev=1668981&view=auto
==============================================================================
--- steve/trunk/pysteve/lib/plugins/__init__.py (added)
+++ steve/trunk/pysteve/lib/plugins/__init__.py Tue Mar 24 20:15:45 2015
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+"""
+yna
+stv
+"""
+
+__all__ = ['yna','stv']
\ No newline at end of file
Added: steve/trunk/pysteve/lib/plugins/stv.py
URL:
http://svn.apache.org/viewvc/steve/trunk/pysteve/lib/plugins/stv.py?rev=1668981&view=auto
==============================================================================
--- steve/trunk/pysteve/lib/plugins/stv.py (added)
+++ steve/trunk/pysteve/lib/plugins/stv.py Tue Mar 24 20:15:45 2015
@@ -0,0 +1,227 @@
+""" STV Voting Plugin """
+import re, json, random
+
+from lib import constants
+
+def validateSTV(vote, issue):
+ "Tries to invalidate a vote, returns why if succeeded, None otherwise"
+ letters = [chr(i) for i in range(ord('a'), ord('a') +
len(issue['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 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 tallySTV(votes, issue):
+ m = re.match(r"stv(\d+)", issue['type'])
+ if not m:
+ raise Exception("Not an STV vote!")
+
+ numseats = int(m.group(1))
+ candidates = []
+ for c in issue['candidates']:
+ candidates.append(c['name'])
+
+
+ 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 = []
+ random.shuffle(winners)
+ for c in winners:
+ i = ord(c) - ord('a')
+ winnernames.append(candidates[i])
+
+ # Return the data
+ return {
+ 'votes': len(votes),
+ 'winners': winners,
+ 'winnernames': winnernames,
+ 'debug': debug
+ }
+
+
+constants.VOTE_TYPES += (
+ {
+ 'key': "stv1",
+ 'description': "Single Transferrable Vote with 1 seat",
+ 'category': 'stv',
+ 'validate_func': validateSTV,
+ 'vote_func': None,
+ 'tally_func': tallySTV
+ },
+ {
+ 'key': "stv2",
+ 'description': "Single Transferrable Vote with 2 seats",
+ 'category': 'stv',
+ 'validate_func': validateSTV,
+ 'vote_func': None,
+ 'tally_func': tallySTV
+ },
+ {
+ 'key': "stv3",
+ 'description': "Single Transferrable Vote with 3 seats",
+ 'category': 'stv',
+ 'validate_func': validateSTV,
+ 'vote_func': None,
+ 'tally_func': tallySTV
+ },
+ {
+ 'key': "stv4",
+ 'description': "Single Transferrable Vote with 4 seats",
+ 'category': 'stv',
+ 'validate_func': validateSTV,
+ 'vote_func': None,
+ 'tally_func': tallySTV
+ },
+ {
+ 'key': "stv5",
+ 'description': "Single Transferrable Vote with 5 seats",
+ 'category': 'stv',
+ 'validate_func': validateSTV,
+ 'vote_func': None,
+ 'tally_func': tallySTV
+ },
+ {
+ 'key': "stv6",
+ 'description': "Single Transferrable Vote with 6 seats",
+ 'category': 'stv',
+ 'validate_func': validateSTV,
+ 'vote_func': None,
+ 'tally_func': tallySTV
+ },
+ {
+ 'key': "stv7",
+ 'description': "Single Transferrable Vote with 7 seats",
+ 'category': 'stv',
+ 'validate_func': validateSTV,
+ 'vote_func': None,
+ 'tally_func': tallySTV
+ },
+ {
+ 'key': "stv8",
+ 'description': "Single Transferrable Vote with 8 seats",
+ 'category': 'stv',
+ 'validate_func': validateSTV,
+ 'vote_func': None,
+ 'tally_func': tallySTV
+ },
+ {
+ 'key': "stv9",
+ 'description': "Single Transferrable Vote with 9 seats",
+ 'category': 'stv',
+ 'validate_func': validateSTV,
+ 'vote_func': None,
+ 'tally_func': tallySTV
+ }
+)
\ No newline at end of file
Added: steve/trunk/pysteve/lib/plugins/yna.py
URL:
http://svn.apache.org/viewvc/steve/trunk/pysteve/lib/plugins/yna.py?rev=1668981&view=auto
==============================================================================
--- steve/trunk/pysteve/lib/plugins/yna.py (added)
+++ steve/trunk/pysteve/lib/plugins/yna.py Tue Mar 24 20:15:45 2015
@@ -0,0 +1,44 @@
+from lib import constants
+
+def tallyYNA(votes, issue):
+ """ 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 {
+ 'votes': len(votes),
+ 'yes': y,
+ 'no': n,
+ 'abstain': a
+ }
+
+def validateYNA(vote, issue):
+ "Tries to invalidate a vote, returns why if succeeded, None otherwise"
+ letters = ['y','n','a']
+ 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
+
+constants.VOTE_TYPES += (
+ {
+ 'key': "yna",
+ 'description': "YNA (Yes/No/Abstain) vote",
+ 'category': 'yna',
+ 'validate_func': validateYNA,
+ 'vote_func': None,
+ 'tally_func': tallyYNA
+ },
+)
\ No newline at end of file
Modified: steve/trunk/pysteve/www/cgi-bin/rest_admin.py
URL:
http://svn.apache.org/viewvc/steve/trunk/pysteve/www/cgi-bin/rest_admin.py?rev=1668981&r1=1668980&r2=1668981&view=diff
==============================================================================
--- steve/trunk/pysteve/www/cgi-bin/rest_admin.py (original)
+++ steve/trunk/pysteve/www/cgi-bin/rest_admin.py Tue Mar 24 20:15:45 2015
@@ -136,7 +136,7 @@ else:
raise Exception("Required fields
missing: %s" % ", ".join(xr))
else:
xr.pop(0)
- if not form.getvalue('type') in
constants.VALID_VOTE_TYPES:
+ if not
election.validType(form.getvalue('type')):
raise Exception('Invalid vote type: %s' %
form.getvalue('type'))
with open(issuepath + ".json", "w") as f:
candidates = []
@@ -485,13 +485,9 @@ else:
issuedata = election.getIssue(electionID, issue)
votes = election.getVotes(electionID, issue)
if issuedata and votes:
- if issuedata['type'].startswith("stv"):
- numseats = int(issuedata['type'][3])
- winners, winnernames, debug =
election.stv(issuedata['candidates'], votes, numseats, shuffle = True)
- response.respond(200, {'votes': len(votes),
'winners': winners, 'winnernames': winnernames, 'debug': debug})
- elif issuedata['type'] == "yna":
- yes, no, abstain = election.yna(votes)
- response.respond(200, {'votes': len(votes), 'yes':
yes, 'no': no, 'abstain': abstain})
+ if election.validType(issuedata['type']):
+ result = election.tally(votes, issuedata)
+ response.respond(200, result)
else:
response.respond(500, {'message': "Unknown vote
type"})
elif not votes:
@@ -522,7 +518,14 @@ else:
else:
response.respond(403, {'message': "You do not have karma
to tally the votes here"})
else:
- response.respond(404, {'message': 'No such election or
issue'})
+ response.respond(404, {'message': 'No such election or
issue'})
+ # Get registered vote stpye
+ elif action == "types":
+ types = []
+ for vtype in constants.VOTE_TYPES:
+ types.append(vtype['key'])
+ response.respond(200, {'types': types})
+
else:
response.respond(400, {'message': "No (or invalid) action
supplied"})
else:
Modified: steve/trunk/pysteve/www/htdocs/js/steve_rest.js
URL:
http://svn.apache.org/viewvc/steve/trunk/pysteve/www/htdocs/js/steve_rest.js?rev=1668981&r1=1668980&r2=1668981&view=diff
==============================================================================
--- steve/trunk/pysteve/www/htdocs/js/steve_rest.js (original)
+++ steve/trunk/pysteve/www/htdocs/js/steve_rest.js Tue Mar 24 20:15:45 2015
@@ -646,7 +646,7 @@ function renderElectionFrontpage(respons
inner.innerHTML = issue.id + ": " + issue.title;
outer.appendChild(no)
outer.appendChild(inner)
- outer.setAttribute("onclick", "location.href='ballot_" +
(issue.type == "yna" ? "yna" : "stv") + ".html?" + el[0] + "/" + issue.id + "/"
+ (el[1] ? el[1] : "") + "';")
+ outer.setAttribute("onclick", "location.href='ballot_" +
issue.category + ".html?" + el[0] + "/" + issue.id + "/" + (el[1] ? el[1] : "")
+ "';")
outer.style.animation = "fadein " + (0.5 + (s/6)) + "s"
issueList.appendChild(outer)
}