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)
        }


Reply via email to