blob: f3a827afc67d7af012271ff6d9bdd797c0610bf6 [file] [log] [blame]
# STV Explorer using Historical data from ASF Board Votes
#####
# 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.
#####
#
# Prereqs:
#
# * svn checkout of foundation:voter and foundation:Meetings
# * Web server with the ability to run cgi (Apache httpd recommended)
# * Python 2.6.x
# * Ruby 1.9.x
# * wunderbar gem ([sudo] gem install wunderbar)
# * (optional) jQuery http://code.jquery.com/jquery-min.js
#
# Installation instructions:
#
# ruby whatif.rb --install=/var/www
#
# 1) Specify a path that supports cgi, like public-html or Sites.
# 2) (optional, but highly recommended) download jquery-min.js into
# your installation directory.
#
# Execution instructions:
#
# Point your web browser at your cgi script. For best results, use
# Firefox 4 or a WebKit based browser, like Google Chrome.
MEETINGS = File.expand_path('../Meetings').untaint unless defined? MEETINGS
WHATIF = './whatif.py' unless defined? WHATIF
require 'wunderbar'
require 'tempfile'
def raw_votes(date)
all_votes = Dir["#{MEETINGS}/*/raw_board_votes.txt"]
if date
result = "#{MEETINGS}/#{date}/raw_board_votes.txt"
else
result = all_votes.sort.last
end
result.untaint if all_votes.include? result
result
end
def ini(vote)
vote.sub('/raw_','/').sub('votes.','nominations.').sub('.txt','.ini')
end
def filtered_election(votes, seats, candidates)
list = candidates.join(' ')
list.untaint if list =~ /^\w+( \w+)*$/
seats.untaint if seats =~ /^\d+$/
output = `#{WHATIF} #{votes} #{seats} #{list}`
output.scan(/.*elected$/).inject(Hash.new('none')) do |results, line|
name, status = line.scan(/^(.*?)\s+(n?o?t?\s?elected)$/).flatten
results.merge({name.gsub(/[^[[:alnum:]]]/,'') => status.gsub(/\s/, '-')})
end
end
# XMLHttpRequest (AJAX)
_json do
nominees = File.read(ini(raw_votes(@date))).scan(/^\w:\s*(.*)/).flatten
candidates = params.keys & nominees.map {|name| name.gsub(/[^[[:alnum:]]]/,'')}
_! filtered_election(raw_votes(@date), @seats, candidates)
end
# main output
_html do
_head_ do
_title 'STV Explorer'
_style! %{
h1 {font-family: sans-serif; font-weight: normal}
select {display: block; margin: 0 0 1em 1em; font-size: 140%}
label div {display: inline-block; min-width: 12em; font-size: x-large}
label div {-webkit-transition: background-color 1s}
label div {-moz-transition: background-color 1s}
label {float: left; clear: both}
label[for=seats] {display: inline; line-height: 500%}
p, input[type=checkbox] {margin-left: 1em}
p, input[type=submit] {display: block; clear: both}
.elected {background: #0F0}
.not-elected {background: #F00}
.none {background: yellow}
}
_script src: 'assets/jquery-min.js'
end
_body? do
_h1_ 'STV Explorer'
nominees = Hash[File.read(ini(raw_votes(@date))).scan(/^\w:\s*(.*)/).
flatten.map {|name| [name.gsub(/[^[[:alnum:]]]/,''), name]}]
candidates = params.keys & nominees.keys
candidates = nominees.keys if candidates.empty? or @reset
@seats ||= '9'
results = filtered_election(raw_votes(@date), @seats, candidates)
# form of nominees and seats
_form method: 'post', id: 'vote' do
_select name: 'date' do
Dir["#{MEETINGS}/*/raw_board_votes.txt"].sort.reverse.each do |votes|
next unless File.exist? ini(votes.untaint)
date = votes[/(\d+)\/raw_board_votes.txt$/,1]
display = date.sub(/(\d{4})(\d\d)(\d\d)/,'\1-\2-\3')
_option display, value: date, selected: (votes == raw_votes(@date))
end
end
nominees.sort.each do |id, name|
_label_ id: id do
_input type: 'checkbox', name: id, checked: candidates.include?(id)
_div name, class: results[id]
end
end
_label_ for: 'seats' do
_span 'seats:'
_input name: 'seats', id: 'seats', value: @seats, size: 2,
type: 'number', min: 1, max: nominees.length-1
end
_input type: 'submit', value: 'submit', name: 'submit'
end
_p_ do
_a "Member's Meeting Information",
href: 'https://whimsy.apache.org/members/meeting'
end
_script %{
// submit form using XHR; update class for labels based on results
function refresh() {
$.post('', $('#vote').serialize(), function(results) {
for (var name in results) {
$('#'+name+' div').attr('class', results[name]);
}
}, 'json');
return false;
}
// On checkbox click, remove class from associated label & refresh
$(':checkbox').click(function() {
$('div', $(this).parent()).attr('class', 'none');
refresh();
});
// reset whenever the date changes
$('select').change(function() {
$('input[value=submit]').attr('name', 'reset');
$('input[value=submit]').click();
});
// If JS is enabled, we don't need a submit button
$('input[type=submit]').hide();
// Refresh on change in number of seats
$('#seats').on('input', function() {return refresh()});
}
end
end
__END__
MEETINGS = '../Meetings'