From c5fda1e5174238779afd496014379d6446d1e3c1 Mon Sep 17 00:00:00 2001 From: Date: Tue, 25 Jul 2006 16:45:53 -0400 Subject: [PATCH] Major changes in this commit over include work over several days but that was never working well enough to end in a commit. These included: * Added a voting interface so that folks can use their magic token to log into the system and vote. * Expanded a variety of the existing models to accomidate the process of voting. * Adjusted the DB to allow for storing different types of votes. --- .bzrignore | 1 + app/controllers/elections_controller.rb | 24 ++++--- app/controllers/voter_controller.rb | 40 ++++++++--- app/models/candidate.rb | 4 ++ app/models/ranking.rb | 5 ++ app/models/token.rb | 18 +++++ app/models/vote.rb | 80 ++++++++++++++++------ app/models/voter.rb | 13 ++++ app/views/elections/detailed_results.rhtml | 31 +++++++++ app/views/voter/index.rhtml | 35 ++-------- app/views/voter/review.rhtml | 31 +++++++++ app/views/voter/thanks.rhtml | 9 +++ app/views/voter/vote.rhtml | 35 ++++++++++ config/environment.rb | 5 +- db/create.sql | 5 +- lib/randarray.rb | 70 +++++++++++++++++++ randarray.rb | 70 +++++++++++++++++++ test/fixtures/tokens.yml | 5 ++ test/unit/token_test.rb | 10 +++ 19 files changed, 417 insertions(+), 74 deletions(-) create mode 100644 app/models/token.rb create mode 100644 app/views/elections/detailed_results.rhtml create mode 100644 app/views/voter/review.rhtml create mode 100644 app/views/voter/thanks.rhtml create mode 100644 app/views/voter/vote.rhtml create mode 100644 lib/randarray.rb create mode 100644 randarray.rb create mode 100644 test/fixtures/tokens.yml create mode 100644 test/unit/token_test.rb diff --git a/.bzrignore b/.bzrignore index 2371a0a..d2da808 100644 --- a/.bzrignore +++ b/.bzrignore @@ -3,3 +3,4 @@ development.log production.log server.log test.log +tmp diff --git a/app/controllers/elections_controller.rb b/app/controllers/elections_controller.rb index b3406a8..339c77b 100644 --- a/app/controllers/elections_controller.rb +++ b/app/controllers/elections_controller.rb @@ -1,5 +1,3 @@ -require 'uniq_token' - class ElectionsController < ApplicationController model :raw_voter_list, :voter, :vote, :candidate @@ -102,21 +100,29 @@ class ElectionsController < ApplicationController voter.destroy end + def summary_results + end + + def detailed_results + @election = Election.find( params[:id] ) + @voting_rolls = [] + @election.voters.each do |voter| + if voter.vote and voter.vote.confirmed? + @voting_rolls << voter + end + end + end + private + def randomize_order + end def process_incoming_voters(raw_voter_list) incoming_voters = RawVoterList.new( raw_voter_list ) - token_generator = UniqueTokenGenerator.new( 16 ) unless incoming_voters.entries.empty? incoming_voters.each do |new_voter| - until new_voter.password and \ - Voter.find_all( [ "password = ?", new_voter.password ]).empty? - new_voter.password = token_generator.token - end - - breakpoint if incoming_voters.email == 0 new_voter.contacted = 1 elsif incoming_voters.email == 1 diff --git a/app/controllers/voter_controller.rb b/app/controllers/voter_controller.rb index 2ceb42b..2527118 100644 --- a/app/controllers/voter_controller.rb +++ b/app/controllers/voter_controller.rb @@ -5,21 +5,41 @@ class VoterController < ApplicationController def index password = params[:id] - @voter = Voter.find_all( [ "password = ?", password ] )[0] + password = params[:vote][:password] if params[:vote] + if @voter = Voter.find_all( [ "password = ?", password ] )[0] + render :action => 'vote' + end end def review - password = params[:id] - @voter = Voter.find_all( [ "password = ?", password ] )[0] + if authenticate + # remove any existing votes and reload + if @voter.vote + @voter.vote.destroy + @voter.reload + end - # destroy the old vote if that's what we need to do - @voter.vote.destroy if @voter.vote - @voter.reload + @vote = Vote.new + @voter.vote = @vote + @vote.votestring = params[:vote][:votestring] + @vote.save + else + redirect_to :action => 'index' + end + end - @voter.vote = Vote.new - @voter.vote.votestring = params[:vote][:votestring] - @voter.vote.save - render_text "success" + def confirm + if authenticate + @voter.vote.confirm! + render :action => 'thanks' + else + redirect_to :action => 'index' + end end + private + def authenticate + password = params[:id] + @voter = Voter.find_all( [ "password = ?", password ] )[0] + end end diff --git a/app/models/candidate.rb b/app/models/candidate.rb index a0e492e..dfc8b1f 100644 --- a/app/models/candidate.rb +++ b/app/models/candidate.rb @@ -4,5 +4,9 @@ class Candidate < ActiveRecord::Base def <=>(other) self.name <=> other.name end + + def to_s + name + end end diff --git a/app/models/ranking.rb b/app/models/ranking.rb index f24f0f9..4b96b54 100644 --- a/app/models/ranking.rb +++ b/app/models/ranking.rb @@ -1,4 +1,9 @@ class Ranking < ActiveRecord::Base belongs_to :candidate belongs_to :vote + + def <=>(other) + self.rank <=> other.rank + end + end diff --git a/app/models/token.rb b/app/models/token.rb new file mode 100644 index 0000000..2bab513 --- /dev/null +++ b/app/models/token.rb @@ -0,0 +1,18 @@ +class Token < ActiveRecord::Base + belongs_to :vote + + def initialize + super + + token_generator = UniqueTokenGenerator.new( 16 ) + until not token.empty? and Token.find_all( [ "token = ?", token ]).empty? + self.token = token_generator.token + end + + self + end + + def to_s + self.token + end +end diff --git a/app/models/vote.rb b/app/models/vote.rb index 2aa1c4b..82f34a9 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,38 +1,76 @@ class Vote < ActiveRecord::Base + # relationships to other classes belongs_to :voter has_many :rankings + has_one :token - def initialize - super - @votes = [] + # callbacks + after_update :save_rankings + before_destroy :destroy_rankings + + def to_s + votes.join("") end - - def votestring=(string="") - rel_votes = string.split("").collect { |vote| vote.to_i } - - # covert relative orders to absolute candidate ids - candidate_ids = voter.election.candidates.sort - candidate_ids.collect! { |candidate| candidate.id.to_i } - - rel_votes.collect! { |vote| candidate_ids[ vote - 1 ] } - @votes = rel_votes + + def each + votes.each {|vote| yield vote} + end + + def votes + unless @votes + if rankings.empty? + @votes = Array.new + else + @votes = rankings.sort.collect { |ranking| ranking.candidate.id } + end + end + + @votes + end + + def votes=(array) + @votes = array end - def save - rankings.each { destroy } unless rankings.empty? - @votes.each_with_index do |candidate, index| + def save_rankings + destroy_rankings + self.votes.each_with_index do |candidate, index| ranking = Ranking.new ranking.rank = index + 1 ranking.candidate = Candidate.find(candidate) self.rankings << ranking end - - super end - def destroy - rankings.each { destroy } - super + def destroy_rankings + rankings.each { |ranking| ranking.destroy } + end + + def confirm! + self.confirmed = 1 + self.save + + token.destroy and token.reload if token + self.token = Token.new + self.save + end + + def confirm? + if confirm == 1 + return true + else + return false + end + end + + def votestring=(string="") + candidate_ids = voter.election.candidates.sort.collect \ + { |candidate| candidate.id.to_i } + + rel_votes = string.split("").collect { |vote| vote.to_i } + + # covert relative orders to absolute candidate ids + self.votes = rel_votes.collect { |vote| candidate_ids[ vote - 1 ] } end end diff --git a/app/models/voter.rb b/app/models/voter.rb index 13b43f5..e9b3e9e 100644 --- a/app/models/voter.rb +++ b/app/models/voter.rb @@ -1,4 +1,17 @@ class Voter < ActiveRecord::Base belongs_to :election has_one :vote + + def initialize(args) + super(args) + + token_generator = UniqueTokenGenerator.new( 16 ) + until password and Voter.find_all( [ "password = ?", password ]).empty? + self.password = token_generator.token + end + end + end + + + diff --git a/app/views/elections/detailed_results.rhtml b/app/views/elections/detailed_results.rhtml new file mode 100644 index 0000000..48296d6 --- /dev/null +++ b/app/views/elections/detailed_results.rhtml @@ -0,0 +1,31 @@ +

The voting rolls for the last election are are follows.

+ +

Voters

+ +<% for voter in @voting_rolls.randomize %> + + + +<% end %> +
<%= voter.email %>
+ +

Votes (by Token)

+ + + + + + +<% for candidate in @election.candidates.sort %> + +<% end %> + +<% for voter in @voting_rolls.randomize %> + + +<% for ranking in voter.vote %> + +<% end %> + +<% end %> +
TokenRank of Candidates
<%= candidate %>
<%= voter.vote.token %><%= ranking %>
diff --git a/app/views/voter/index.rhtml b/app/views/voter/index.rhtml index 00b5136..7e31c92 100644 --- a/app/views/voter/index.rhtml +++ b/app/views/voter/index.rhtml @@ -1,35 +1,8 @@ <% %> -

Vote Below the Line

+

Please enter your password/token to log in and vote:

-

Election: <%= @voter.election.name %>

- -

Voter: <%= @voter.email %>

- -

Candidates:

- -
    -<% for candidate in @voter.election.candidates %> -
  1. <%= candidate.name %>
  2. -<% end %> -
- -

If this information is incorrect, please notify the vote -administrator immediatedly!

- -
- -

Place Your Vote Here

- -

Rank each candidate in order of more preferred to least -preferred. (e.g., 123 or 321 or 213, etc.)

- -<%= form_tag :action => 'review', :id => @voter.password %> -<%= text_field :vote, :votestring -%> -<%= submit_tag "Submit!" %> +<%= form_tag :action => 'index' %> +<%= text_field :vote, :password %> +<%= submit_tag "Log In" %> <%= end_form_tag %> - - - - - diff --git a/app/views/voter/review.rhtml b/app/views/voter/review.rhtml new file mode 100644 index 0000000..d1eb9dc --- /dev/null +++ b/app/views/voter/review.rhtml @@ -0,0 +1,31 @@ +<% %> + +

Please review your vote carefully before confirming it.

+ +

You have ranked the candidates in the following order (from most +preferred to least preferred:

+ +
    + <% for rank in @vote.rankings.sort %> +
  1. <%= rank.candidate.name %>
  2. + <% end %> +
+ + + + + + + + + + + + + + + + + +
<%= button_to 'Confirm', :action => 'confirm', :id => @voter.password %>Confirm this vote now. You will be able to go back and + change it.
<%= button_to 'Change', :action => 'index', :id => @voter.password %>Go back to the voting page and vote again.
<%= button_to 'Discard', :action => 'discard' %>Discard this tentative vote and log out.
diff --git a/app/views/voter/thanks.rhtml b/app/views/voter/thanks.rhtml new file mode 100644 index 0000000..ab8992c --- /dev/null +++ b/app/views/voter/thanks.rhtml @@ -0,0 +1,9 @@ +<% %> + +

Your vote has been recorded.

+ +

Your unique token for this vote is: <%= @voter.vote.token %>

+ +

Please record this token for your records and keep it secret. You +will be able to use this token to verify that your vote was used in the +election and that your vote was recorded correctly.

diff --git a/app/views/voter/vote.rhtml b/app/views/voter/vote.rhtml new file mode 100644 index 0000000..2d30209 --- /dev/null +++ b/app/views/voter/vote.rhtml @@ -0,0 +1,35 @@ +<% %> + +

Vote Below the Line

+ +

Election: <%= @voter.election.name %>

+ +

Voter: <%= @voter.email %>

+ +

Candidates:

+ +
    +<% for candidate in @voter.election.candidates.sort %> +
  1. <%= candidate.name %>
  2. +<% end %> +
+ +

If this information is incorrect, please notify the vote +administrator immediatedly!

+ +
+ +

Place Your Vote Here

+ +

Rank each candidate in order of more preferred to least +preferred. (e.g., 123 or 321 or 213, etc.)

+ +<%= form_tag :action => 'review', :id => @voter.password %> +<%= text_field :vote, :votestring -%> +<%= submit_tag "Submit!" %> +<%= end_form_tag %> + + + + + diff --git a/config/environment.rb b/config/environment.rb index 1ead71b..de8cbaf 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -48,4 +48,7 @@ end # inflect.uncountable %w( fish sheep ) # end -# Include your application configuration below \ No newline at end of file +# Include your application configuration below + +require 'uniq_token' +require 'randarray' diff --git a/db/create.sql b/db/create.sql index b7b50bf..2b32457 100644 --- a/db/create.sql +++ b/db/create.sql @@ -60,6 +60,8 @@ drop table if exists tokens; create table tokens( id int NOT NULL auto_increment, token varchar(100) NOT NULL, + vote_id int NOT NULL, + constraint fk_vote_token foreign key (vote_id) references vote(id), primary key (id) ); @@ -70,9 +72,8 @@ drop table if exists votes; create table votes ( id int NOT NULL auto_increment, voter_id int DEFAULT NULL, - token_id int DEFAULT NULL, + confirmed tinyint NOT NULL DEFAULT 0, constraint fk_vote_voter foreign key (voter_id) references voters(id), - constraint fk_vote_token foreign key (token_id) references token(id), primary key (id) ); diff --git a/lib/randarray.rb b/lib/randarray.rb new file mode 100644 index 0000000..17961d1 --- /dev/null +++ b/lib/randarray.rb @@ -0,0 +1,70 @@ +class Array + # Chooses a random array element from the receiver based on the weights + # provided. If _weights_ is nil, then each element is weighed equally. + # + # [1,2,3].random #=> 2 + # [1,2,3].random #=> 1 + # [1,2,3].random #=> 3 + # + # If _weights_ is an array, then each element of the receiver gets its + # weight from the corresponding element of _weights_. Notice that it + # favors the element with the highest weight. + # + # [1,2,3].random([1,4,1]) #=> 2 + # [1,2,3].random([1,4,1]) #=> 1 + # [1,2,3].random([1,4,1]) #=> 2 + # [1,2,3].random([1,4,1]) #=> 2 + # [1,2,3].random([1,4,1]) #=> 3 + # + # If _weights_ is a symbol, the weight array is constructed by calling + # the appropriate method on each array element in turn. Notice that + # it favors the longer word when using :length. + # + # ['dog', 'cat', 'hippopotamus'].random(:length) #=> "hippopotamus" + # ['dog', 'cat', 'hippopotamus'].random(:length) #=> "dog" + # ['dog', 'cat', 'hippopotamus'].random(:length) #=> "hippopotamus" + # ['dog', 'cat', 'hippopotamus'].random(:length) #=> "hippopotamus" + # ['dog', 'cat', 'hippopotamus'].random(:length) #=> "cat" + def random(weights=nil) + return random(map {|n| n.send(weights)}) if weights.is_a? Symbol + + weights ||= Array.new(length, 1.0) + total = weights.inject(0.0) {|t,w| t+w} + point = rand * total + + zip(weights).each do |n,w| + return n if w >= point + point -= w + end + end + + # Generates a permutation of the receiver based on _weights_ as in + # Array#random. Notice that it favors the element with the highest + # weight. + # + # [1,2,3].randomize #=> [2,1,3] + # [1,2,3].randomize #=> [1,3,2] + # [1,2,3].randomize([1,4,1]) #=> [2,1,3] + # [1,2,3].randomize([1,4,1]) #=> [2,3,1] + # [1,2,3].randomize([1,4,1]) #=> [1,2,3] + # [1,2,3].randomize([1,4,1]) #=> [2,3,1] + # [1,2,3].randomize([1,4,1]) #=> [3,2,1] + # [1,2,3].randomize([1,4,1]) #=> [2,1,3] + def randomize(weights=nil) + return randomize(map {|n| n.send(weights)}) if weights.is_a? Symbol + + weights = weights.nil? ? Array.new(length, 1.0) : weights.dup + + # pick out elements until there are none left + list, result = self.dup, [] + until list.empty? + # pick an element + result << list.random(weights) + # remove the element from the temporary list and its weight + weights.delete_at(list.index(result.last)) + list.delete result.last + end + + result + end +end diff --git a/randarray.rb b/randarray.rb new file mode 100644 index 0000000..17961d1 --- /dev/null +++ b/randarray.rb @@ -0,0 +1,70 @@ +class Array + # Chooses a random array element from the receiver based on the weights + # provided. If _weights_ is nil, then each element is weighed equally. + # + # [1,2,3].random #=> 2 + # [1,2,3].random #=> 1 + # [1,2,3].random #=> 3 + # + # If _weights_ is an array, then each element of the receiver gets its + # weight from the corresponding element of _weights_. Notice that it + # favors the element with the highest weight. + # + # [1,2,3].random([1,4,1]) #=> 2 + # [1,2,3].random([1,4,1]) #=> 1 + # [1,2,3].random([1,4,1]) #=> 2 + # [1,2,3].random([1,4,1]) #=> 2 + # [1,2,3].random([1,4,1]) #=> 3 + # + # If _weights_ is a symbol, the weight array is constructed by calling + # the appropriate method on each array element in turn. Notice that + # it favors the longer word when using :length. + # + # ['dog', 'cat', 'hippopotamus'].random(:length) #=> "hippopotamus" + # ['dog', 'cat', 'hippopotamus'].random(:length) #=> "dog" + # ['dog', 'cat', 'hippopotamus'].random(:length) #=> "hippopotamus" + # ['dog', 'cat', 'hippopotamus'].random(:length) #=> "hippopotamus" + # ['dog', 'cat', 'hippopotamus'].random(:length) #=> "cat" + def random(weights=nil) + return random(map {|n| n.send(weights)}) if weights.is_a? Symbol + + weights ||= Array.new(length, 1.0) + total = weights.inject(0.0) {|t,w| t+w} + point = rand * total + + zip(weights).each do |n,w| + return n if w >= point + point -= w + end + end + + # Generates a permutation of the receiver based on _weights_ as in + # Array#random. Notice that it favors the element with the highest + # weight. + # + # [1,2,3].randomize #=> [2,1,3] + # [1,2,3].randomize #=> [1,3,2] + # [1,2,3].randomize([1,4,1]) #=> [2,1,3] + # [1,2,3].randomize([1,4,1]) #=> [2,3,1] + # [1,2,3].randomize([1,4,1]) #=> [1,2,3] + # [1,2,3].randomize([1,4,1]) #=> [2,3,1] + # [1,2,3].randomize([1,4,1]) #=> [3,2,1] + # [1,2,3].randomize([1,4,1]) #=> [2,1,3] + def randomize(weights=nil) + return randomize(map {|n| n.send(weights)}) if weights.is_a? Symbol + + weights = weights.nil? ? Array.new(length, 1.0) : weights.dup + + # pick out elements until there are none left + list, result = self.dup, [] + until list.empty? + # pick an element + result << list.random(weights) + # remove the element from the temporary list and its weight + weights.delete_at(list.index(result.last)) + list.delete result.last + end + + result + end +end diff --git a/test/fixtures/tokens.yml b/test/fixtures/tokens.yml new file mode 100644 index 0000000..8794d28 --- /dev/null +++ b/test/fixtures/tokens.yml @@ -0,0 +1,5 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +first: + id: 1 +another: + id: 2 diff --git a/test/unit/token_test.rb b/test/unit/token_test.rb new file mode 100644 index 0000000..1c3820e --- /dev/null +++ b/test/unit/token_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class TokenTest < Test::Unit::TestCase + fixtures :tokens + + # Replace this with your real tests. + def test_truth + assert true + end +end -- 2.39.2