Author: humbedooh
Date: Wed Mar 25 12:35:18 2015
New Revision: 1669107

URL: http://svn.apache.org/r1669107
Log:
Add COP (Candidate or Party) voting

Added:
    steve/trunk/pysteve/lib/plugins/cop.py
    steve/trunk/pysteve/www/htdocs/ballot_cop.html
    steve/trunk/pysteve/www/htdocs/js/steve_cop.js
Modified:
    steve/trunk/pysteve/lib/plugins/__init__.py
    steve/trunk/pysteve/www/htdocs/css/steve_interactive.css
    steve/trunk/pysteve/www/htdocs/js/steve_rest.js

Modified: steve/trunk/pysteve/lib/plugins/__init__.py
URL: 
http://svn.apache.org/viewvc/steve/trunk/pysteve/lib/plugins/__init__.py?rev=1669107&r1=1669106&r2=1669107&view=diff
==============================================================================
--- steve/trunk/pysteve/lib/plugins/__init__.py (original)
+++ steve/trunk/pysteve/lib/plugins/__init__.py Wed Mar 25 12:35:18 2015
@@ -15,11 +15,14 @@
 # limitations under the License.
 #
 """
-yna
-stv
-dh
-fpp
-mntv
+CORE VOTE PLUGINS:
+
+    yna:    Yes/No/Abstain
+    stv:    Single Transferable Vote
+    dh:     D'Hondt (Jefferson) Voting
+    fpp:    First Past the Post (Presidential elections)
+    mntv:   Multiple Non-Transferable Votes
+    cop:    Candidate or Party Voting
 """
 
-__all__ = ['yna','stv','dh','fpp','mntv']
\ No newline at end of file
+__all__ = ['yna','stv','dh','fpp','mntv','cop']
\ No newline at end of file

Added: steve/trunk/pysteve/lib/plugins/cop.py
URL: 
http://svn.apache.org/viewvc/steve/trunk/pysteve/lib/plugins/cop.py?rev=1669107&view=auto
==============================================================================
--- steve/trunk/pysteve/lib/plugins/cop.py (added)
+++ steve/trunk/pysteve/lib/plugins/cop.py Wed Mar 25 12:35:18 2015
@@ -0,0 +1,186 @@
+#
+# 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.
+#
+"""
+Candidate or Party Voting Plugin
+Currrently supports an arbitrary number of candidates from up to 26 parties
+"""
+import re, json, random
+
+from lib import constants, form
+
+def validateCOP(vote, issue):
+    "Tries to invalidate a vote, returns why if succeeded, None otherwise"
+    parties = {}
+    for c in issue['candidates']:
+        parties[c['pletter']] = True
+    letters = [chr(i) for i in range(ord('a'), ord('a') + len(parties))]
+    ivote = -1
+    try:
+        ivote = int(vote)
+    except:
+        pass # This is a fast way to determine vote type, passing here is FINE!
+    if not vote in letters and (ivote < 0 or ivote > len(issue['candidates'])):
+            return "Invalid characters in vote. Accepted are: %s" % ", 
".join(letters,range(1,len(issue['candidates'])+1))
+    return None
+
+def parseCandidatesCOP(data):
+    data = data if data else ""
+    candidates = []
+    pletter = ''
+    cletter = ''
+    pname = ''
+    s = 0
+    for line in data.split("\n"):
+        line = line.strip()
+        if len(line) > 0:
+            arr = line.split(":", 1)
+            letter = arr[0]
+            letter = letter.lower()
+            
+            # Party delimiter?
+            if letter in [chr(i) for i in range(ord('a'), ord('a') + 26)] and 
len(arr) > 1 and len(arr[1]) > 0:
+                pname = arr[1]
+                pletter = letter
+            else:
+                candidates.append({
+                    'name': line,
+                    'letter': str(s),
+                    'pletter': pletter,
+                    'pname': pname
+                })
+                s += 1
+    return candidates
+
+
+def tallyCOP(votes, issue):
+    m = re.match(r"cop(\d+)", issue['type'])
+    if not m:
+        raise Exception("Not a COP vote!")
+    
+    numseats = int(m.group(1))
+    parties = {}
+
+    for c in issue['candidates']:
+        if not c['pletter'] in parties:
+            parties[c['pletter']] = {
+                'name': c['pname'],
+                'letter': c['pletter'],
+                'surplus': 0,
+                'candidates': []
+            }
+        parties[c['pletter']]['candidates'].append({
+            'letter': c['letter'],
+            'name': c['name'],
+            'votes': 0,
+            'elected': False
+            })
+    
+
+    debug = []
+    winners = []
+    
+        
+    # Tally up all scores and surplus
+    for key in votes:
+        vote = votes[key]
+        
+        for party in parties:
+            if parties[party]['letter'] == vote:
+                parties[party]['surplus'] += 1
+            else:
+                for candidate in parties[party]['candidates']:
+                    if candidate['letter'] == vote:
+                        candidate['votes'] += 1
+                
+        
+    numvotes = len(votes)
+    
+    if numseats < len(issue['candidates']):
+        
+        # Start by assigning all surplus (party votes) to the first listed 
candidate
+        iterations = 0
+        
+        while numseats > len(winners) and iterations < 9999: # Catch 
forever-looping counts (in case of bug)
+            quota = (numvotes / numseats * 1.0) # Make it a float to prevent 
from rounding down for now
+            for party in parties:
+                surplus = 0
+                movedOn = False
+                for candidate in parties[party]['candidates']:
+                    if not candidate['elected'] and numseats > len(winners):
+                        if candidate['votes'] >= quota:
+                            candidate['elected'] = True
+                            winners.append("%s (%s) %u" % ( candidate['name'], 
parties[party]['name'], candidate['votes']))
+                            surplus += candidate['votes'] - quota
+                    
+                # If surplus of votes, add it to the next candidate in the 
same party
+                if surplus > 0:
+                    for candidate in parties[party]['candidates']:
+                        if not candidate['elected']:
+                            candidate['votes'] += surplus
+                            movedOn = True
+                            break
+                        
+                # If surplus but no candidates left, decrease the number of 
votes required by the surplus
+                if not movedOn:
+                    numvotes -= surplus
+            
+    # Everyone's a winner!!
+    else:
+        for party in parties:
+            for candidate in parties[party]['candidates']:
+                winners.append("%s (%s) %u" % ( candidate['name'], 
parties[party]['name'], candidate['votes']))
+        
+
+   
+    # Return the data
+    return {
+        'votes': len(votes),
+        'winners': winners,
+        'winnernames': winners,
+        'debug': debug
+    }
+
+
+constants.VOTE_TYPES += (
+    {
+        'key': "cop1",
+        'description': "Candidate or Party Vote with 1 seat",
+        'category': 'cop',
+        'validate_func': validateCOP,
+        'vote_func': None,
+        'parsers': {
+            'candidates': parseCandidatesCOP
+        },
+        'tally_func': tallyCOP
+    },
+)
+
+# Add ad nauseam
+for i in range(2,constants.MAX_NUM+1):
+    constants.VOTE_TYPES += (
+        {
+            'key': "cop%02u" % i,
+            'description': "Candidate or Party Vote with %u seats" % i,
+            'category': 'cop',
+            'validate_func': validateCOP,
+            'vote_func': None,
+            'parsers': {
+                'candidates': parseCandidatesCOP
+            },
+            'tally_func': tallyCOP
+        },
+    )

Added: steve/trunk/pysteve/www/htdocs/ballot_cop.html
URL: 
http://svn.apache.org/viewvc/steve/trunk/pysteve/www/htdocs/ballot_cop.html?rev=1669107&view=auto
==============================================================================
--- steve/trunk/pysteve/www/htdocs/ballot_cop.html (added)
+++ steve/trunk/pysteve/www/htdocs/ballot_cop.html Wed Mar 25 12:35:18 2015
@@ -0,0 +1,39 @@
+ <!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<link rel="stylesheet" href="css/steve_interactive.css">
+<link rel="stylesheet" href="css/jquery-ui.css">
+<script src="js/steve_rest.js" type="text/javascript"></script>
+<script src="js/steve_cop.js" type="text/javascript"></script>
+<script src="js/jquery.js" type="text/javascript"></script>
+<script src="js/jquery-ui.js" type="text/javascript"></script>
+<title>Apache STeVe: Candidate or Party vote</title>
+
+</head>
+<body onload="window.setTimeout(loadIssue, 500, null, null, null, 
displayIssueCOP);">
+    <div id="popups"></div>
+    <p style="text-align: center;">
+        <img src="/images/steve_logo.png"/>
+    </p>
+    <a href="javascript:void(location.href='election.html' + 
document.location.search);">Back to election front page</a>
+<div class="formbox">
+    <p>
+        This is a standard Candidate or Party vote.
+        To vote, click on the candidate or party of your choice and then click 
on <kbd>Cast vote</kbd>.
+        Should you reconsider later, you can always recast your vote for as 
long as the election remains open.
+    </p>
+<div id="preloaderWrapper">
+    <img src="/images/steve_spinner.gif"/>
+    <div id="preloader">
+        Loading issue, please wait...
+    </div>
+</div>
+</div>
+<p style="font-size: 12px; font-style: italic; text-align: center;">
+    Powered by <a href="https://steve.apache.org/";>Apache STeVe</a>.
+    Copyright 2015, the Apache Software Foundation.
+    Licensed under the <a 
href="http://www.apache.org/licenses/LICENSE-2.0";>Apache License 2.0</a>
+</p>
+</body>
+</html>
\ No newline at end of file

Modified: steve/trunk/pysteve/www/htdocs/css/steve_interactive.css
URL: 
http://svn.apache.org/viewvc/steve/trunk/pysteve/www/htdocs/css/steve_interactive.css?rev=1669107&r1=1669106&r2=1669107&view=diff
==============================================================================
--- steve/trunk/pysteve/www/htdocs/css/steve_interactive.css (original)
+++ steve/trunk/pysteve/www/htdocs/css/steve_interactive.css Wed Mar 25 
12:35:18 2015
@@ -302,6 +302,10 @@ pre {
     float: left;
     min-height: 400px;
     background-repeat: no-repeat;
+    height:auto;
+    margin-left:auto;
+    margin-right:auto;
+    padding: 10px 20px 30px 20px;
 }
 
 #ballot {
@@ -435,6 +439,11 @@ body, html {
   list-style: none;
   text-align: left;
 }
+
+ol .showList {
+  list-style: outside !important;
+}
+
 #ballot .ballotNumber {
   /*background: linear-gradient(to bottom, #fccd41 0%,#cea202 100%);*/
   background: linear-gradient(to bottom, #4f7bff 0%,#1444bc 100%);
@@ -465,7 +474,9 @@ body, html {
   border-radius: 5px;
   border: 2px solid #333;
   box-shadow: 3px 4px 4px 0px rgba(153,153,153,1);
-  min-width: 800px;
+  height:auto;  
+  font-family: Helvetica, Arial, sans-serif;
+  overflow: auto;
 }
 
 .formbox h2 {
@@ -494,6 +505,8 @@ body, html {
 .formbox .keyvaluepair {
   min-height: 32px;
   min-width: 700px;
+  position: relative;
+  clear: both;
 }
 
 .formbox .keyfield {
@@ -609,4 +622,19 @@ fieldset legend {
   height: 500px;
   margin: 0px auto;
   text-align: center;
-}
\ No newline at end of file
+}
+
+
+
+#contents {
+    width:80%;
+    height:auto;
+    margin-left:auto;
+    margin-right:auto;
+    padding: 10px 20px 30px 20px;
+    
+    color: #333333;
+    font-family: Helvetica, Arial, sans-serif;
+    overflow: auto.
+}
+

Added: steve/trunk/pysteve/www/htdocs/js/steve_cop.js
URL: 
http://svn.apache.org/viewvc/steve/trunk/pysteve/www/htdocs/js/steve_cop.js?rev=1669107&view=auto
==============================================================================
--- steve/trunk/pysteve/www/htdocs/js/steve_cop.js (added)
+++ steve/trunk/pysteve/www/htdocs/js/steve_cop.js Wed Mar 25 12:35:18 2015
@@ -0,0 +1,169 @@
+var step = -1
+var election_data;
+var vote_COP = null;
+
+var candidates;
+var chars;
+
+
+function loadIssue(election, issue, uid, callback) {
+       
+       var messages = ["Herding cats...", "Shaving yaks...", "Shooing some 
cows away...", "Fetching election data...", "Loading issues..."]
+       if (!election || !uid) {
+               var l = document.location.search.substr(1).split("/");
+               election = l[0];
+               issue = l.length > 1 ? l[l.length-2] : "";
+               uid = l.length > 2 ? l[l.length-1] : "";
+       }
+       if (step == -1) {
+               getJSON("/steve/voter/view/" + election + "/" + issue + "?uid=" 
+ uid, [election, issue, uid], callback)
+       }
+       
+       var obj = document.getElementById('preloader');
+       step++;
+       if (!election_data && obj) {
+               if (step % 2 == 1) obj.innerHTML = 
messages[parseInt(Math.random()*messages.length-0.01)]
+       } else if (obj && (step % 2 == 1)) {
+               obj.innerHTML = "Ready..!"
+       }
+       if (step % 2 == 1) {
+               obj.style.transform = "translate(0,0)"
+       } else if (obj) {
+               obj.style.transform = "translate(0,-500%)"
+       }
+       if (!election_data|| (step % 2 == 0) ) {
+               window.setTimeout(loadElection, 750, election, uid, callback);
+       }
+}
+
+
+function drawCandidatesCOP() {
+    var box = document.getElementById('candidates')
+    box.innerHTML = "<h3>Candidates:</h3>"
+       var pname = null
+       var pli = null;
+    for (i in candidates) {
+        var name = candidates[i]
+               if (pname != name['pname']) {
+                       pname = name['pname']
+                       var char = name['pletter']
+                       pli = document.createElement('ol')
+                       pli.setAttribute("class", "showList")
+                       pli.style.marginTop = "20px"
+                       var outer = document.createElement('div')
+                       var inner = document.createElement('span')
+                       inner.style.fontFamily = "monospace"
+                       inner.style.fontWeigth = "bold"
+                       inner.innerHTML = name['pname'];
+                       outer.setAttribute("class", "ballotbox_clist_DH")
+                       if (char == vote_COP) {
+                               outer.setAttribute("class", 
"ballotbox_clist_selectedDH")
+                       }
+                       outer.setAttribute("id", name)
+                       outer.setAttribute("onclick", "vote_COP = '"+char+"'; 
drawCandidatesCOP();")
+                       outer.appendChild(inner)
+                       outer.setAttribute("title", "Click to select "  + name 
+ " as your preference")
+                       pli.appendChild(outer)
+                       box.appendChild(pli)
+                       
+               }
+        var char = name['letter']
+        var li = document.createElement('li')
+        var outer = document.createElement('div')
+        var inner = document.createElement('span')
+        inner.style.fontFamily = "monospace"
+               li.style.marginLeft = "30px"
+               li.style.marginBottom = "10px"
+        inner.innerHTML = name['name'];
+        outer.setAttribute("class", "ballotbox_clist_DH")
+        if (char == vote_COP) {
+            outer.setAttribute("class", "ballotbox_clist_selectedDH")
+        }
+        outer.setAttribute("id", name)
+        outer.setAttribute("onclick", "vote_COP = '"+char+"'; 
drawCandidatesCOP();")
+        outer.appendChild(inner)
+        outer.setAttribute("title", "Click to select "  + name + " as your 
preference")
+        li.appendChild(outer)
+       
+        pli.appendChild(li)
+        
+    }
+}
+
+function displayIssueCOP(code, response, state) {
+    chars = 
['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z']
  // Corresponding STV letters, in same order as nominees
+    election_data = response
+    if (code != 200) {
+        document.getElementById('preloaderWrapper').innerHTML = "<h1>Could not 
load issue:</h1><h2>" + response.message + "</h2>";
+    } else {
+        candidates = []
+        statements = {}
+        var m = response.issue.type.match(/(\d+)/);
+        if (m) {
+            seats = parseInt(m[1])
+        }
+        for (c in response.issue.candidates) {
+            var candidate = response.issue.candidates[c];
+            candidates.push(candidate);
+        }
+        if (document.getElementById('cnum')) 
document.getElementById('cnum').innerHTML = candidates.length
+        if (document.getElementById('snum')) 
document.getElementById('snum').innerHTML = seats        
+        while (chars.length > candidates.length) chars.splice(-1,1)
+        
+        
+        var obj = document.getElementById('preloaderWrapper')
+        obj.innerHTML = ""
+        obj.setAttribute("id", "contents")
+        
+        
+        var l = document.createElement('ol')
+        l.setAttribute("id", "candidates")
+               l.setAttribute("type", "a")
+               l.setAttribute("class", "showList")
+        obj.appendChild(l)
+        
+        drawCandidatesCOP();
+        
+        
+        var vote = document.createElement('input')
+        vote.setAttribute("type", "button")
+        vote.setAttribute("class", "btn-green")
+        vote.setAttribute("value", "Cast vote")
+        vote.setAttribute("onclick", "castVoteCOP();")
+        
+        
+        obj.appendChild(vote)
+        
+        document.getElementById('title').innerHTML = response.issue.title
+        document.title = response.issue.title + " - Apache STeVe"
+        
+    }
+    
+}
+
+function castVoteCOP() {
+    var l = document.location.search.substr(1).split("/");
+    election = l[0];
+    issue = l.length > 1 ? l[l.length-2] : "";
+    uid = l.length > 2 ? l[l.length-1] : "";
+    if (vote_COP) {
+        postREST("/steve/voter/vote/" + election + "/" + issue, {
+            uid: uid,
+            vote: vote_COP
+        },
+        undefined,
+        COPVoteCallback,
+        null)
+    } else {
+        alert("Please select a preference first!")
+    }
+    
+}
+
+function COPVoteCallback(code, response, state) {
+    if (code != 200) {
+        alert(response.message)
+    } else {
+        document.getElementById('contents').innerHTML = "<h2>Your vote has 
been registered!</h2><p style='text-align:center;'><big>Should you reconsider, 
you can always reload this page and vote again.<br/><br/><a 
href=\"javascript:void(location.href='election.html'+document.location.search);\">Back
 to election front page</a></big></p>"
+    }
+}
\ No newline at end of file

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=1669107&r1=1669106&r2=1669107&view=diff
==============================================================================
--- steve/trunk/pysteve/www/htdocs/js/steve_rest.js (original)
+++ steve/trunk/pysteve/www/htdocs/js/steve_rest.js Wed Mar 25 12:35:18 2015
@@ -276,15 +276,16 @@ function saveYNA() {
        var issue = l[1]
        
        var title = document.getElementById('ititle').value
-       var nominatedby = document.getElementById('nominatedby').value
-       var seconds = 
document.getElementById('seconds').value.split(/,\s*/).join("\n")
+       var nominatedby = document.getElementById('nominatedby') ? 
document.getElementById('nominatedby').value : null
+       var seconds = document.getElementById('seconds') ? 
document.getElementById('seconds').value.split(/,\s*/).join("\n") : null
        var description = document.getElementById('description').value
        
        postREST("/steve/admin/edit/" + election + "/" + issue, {
                title: title,
                nominatedby: nominatedby,
                seconds: seconds,
-               description: description
+               description: description,
+               candidates: document.getElementById('candidates') ? 
document.getElementById('candidates').value : null
        },
        undefined,
        saveCallback,
@@ -493,6 +494,39 @@ function renderEditIssue(code, response,
                        obj.appendChild(div)
                        renderEditCandidates()
                }
+               else if (edit_i.type.match(/^cop/)) {
+                       
+                       // base data
+                       obj.innerHTML = "<h3>Editing a Candidate or Party Vote 
issue</h3>"
+                       obj.appendChild(keyvaluepair("id", "Issue ID:", "text", 
edit_i.id, true))
+                       obj.appendChild(keyvaluepair("ititle", "Issue title:", 
"text", edit_i.title))
+                       obj.appendChild(keyvaluepair("description", 
"Description (optinal):", "textarea", edit_i.description))
+                       obj.appendChild(document.createElement('hr'))
+                       
+                       // candidates/parties
+                       var p = null
+                       var pletter = null
+                       var biglist = ""
+                       for (i in edit_i.candidates) {
+                               var c = edit_i.candidates[i]
+                               if (c['pletter'] != pletter) {
+                                       biglist += "\n" + 
c['pletter'].toUpperCase() + ":" + c['pname'] + "\n"
+                                       pletter = c['pletter']
+                               }
+                               biglist += c['name'] + "\n"
+                       }
+                       obj.appendChild(keyvaluepair("candidates", 
"Candidate/Party List:", "textarea", biglist))
+                       
+                       var div = document.createElement('div')
+                       div.setAttribute("class", "keyvaluepair")
+                       var btn = document.createElement('input')
+                       btn.setAttribute("type", "button")
+                       btn.setAttribute("class", "btn-green")
+                       btn.setAttribute("value", "Save changes")
+                       btn.setAttribute("onclick", "saveYNA();")
+                       div.appendChild(btn)
+                       obj.appendChild(div)
+               }
        } else {
                alert(response.message)
        }


Reply via email to