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