Major changes in this commit over include work over several days but that was never...
author<mako@atdot.cc> <>
Tue, 25 Jul 2006 20:45:53 +0000 (16:45 -0400)
committer<mako@atdot.cc> <>
Tue, 25 Jul 2006 20:45:53 +0000 (16:45 -0400)
* 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.

19 files changed:
.bzrignore
app/controllers/elections_controller.rb
app/controllers/voter_controller.rb
app/models/candidate.rb
app/models/ranking.rb
app/models/token.rb [new file with mode: 0644]
app/models/vote.rb
app/models/voter.rb
app/views/elections/detailed_results.rhtml [new file with mode: 0644]
app/views/voter/index.rhtml
app/views/voter/review.rhtml [new file with mode: 0644]
app/views/voter/thanks.rhtml [new file with mode: 0644]
app/views/voter/vote.rhtml [new file with mode: 0644]
config/environment.rb
db/create.sql
lib/randarray.rb [new file with mode: 0644]
randarray.rb [new file with mode: 0644]
test/fixtures/tokens.yml [new file with mode: 0644]
test/unit/token_test.rb [new file with mode: 0644]

index 2371a0a9a03fb5571eb95d216993924198bcc0bd..d2da80804ce572d300f6ad75a209f1da2d77b318 100644 (file)
@@ -3,3 +3,4 @@ development.log
 production.log
 server.log
 test.log
+tmp
index b3406a84ab64941ce8a4c05e415937aec09f4e92..339c77bc8991fe80ef38ab6afddd53e5ca53974d 100644 (file)
@@ -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
index 2ceb42b1d24d0db5af7550569bf48fdd5bf5c23e..252711830ac96f7796062b7ab523cf391ed1eab3 100644 (file)
@@ -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
index a0e492e8c1f98a6848efc5215db07de5b62f4c7a..dfc8b1fec08fc19e87b320ea3a7c3856d1369c08 100644 (file)
@@ -4,5 +4,9 @@ class Candidate < ActiveRecord::Base
   def <=>(other)
    self.name <=> other.name 
   end
+  
+  def to_s
+    name
+  end
 
 end
index f24f0f913dc8b33c7b0423e17701718f4c09943c..4b96b54c2060e5d8053c313a8975b901635e9042 100644 (file)
@@ -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 (file)
index 0000000..2bab513
--- /dev/null
@@ -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
index 2aa1c4bfd929ee5d6cf68faccb7acab1cc07bc43..82f34a929bb6443a7d0b5058ff1263bbb1736ab3 100644 (file)
@@ -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
index 13b43f525ac7882ac85d9c36e86483213d4573e1..e9b3e9e2cfe4901a91df669815d6d8a0b944890f 100644 (file)
@@ -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 (file)
index 0000000..48296d6
--- /dev/null
@@ -0,0 +1,31 @@
+<p>The voting rolls for the last election are are follows.</p>
+
+<h2>Voters</h2>
+<table>
+<% for voter in @voting_rolls.randomize %>
+<tr>
+<td><%= voter.email %></td>
+</tr>
+<% end %>
+</table>
+
+<h2>Votes (by Token)</h2>
+<table border="1">
+<tr>
+  <th rowspan="2">Token</th>
+  <th colspan="<%= @election.candidates.length %>">Rank of Candidates</th>
+</tr>
+<tr>
+<% for candidate in @election.candidates.sort %>
+  <th><%= candidate %></th>
+<% end %>
+</tr>
+<% for voter in @voting_rolls.randomize %>
+<tr>
+<td><%= voter.vote.token %></td>
+<% for ranking in voter.vote %>
+<td><%= ranking %></td>
+<% end %>
+</tr>
+<% end %>
+</table>
index 00b5136cea6381ae4438c778dd3be4abd246f68c..7e31c920a0b7a47400cd1c53c9947510f546509d 100644 (file)
@@ -1,35 +1,8 @@
 <% %>
 
-<h1>Vote Below the Line</h1>
+<p>Please enter your password/token to log in and vote:</p>
 
-<p><strong>Election:</strong> <%= @voter.election.name %></p>
-
-<p><strong>Voter:</strong> <%= @voter.email %></p>
-
-<p><strong>Candidates:</strong></p>
-
-<ol>
-<% for candidate in @voter.election.candidates %>
-  <li><%= candidate.name %></li>
-<% end %>
-</ol>
-
-<p>If this information is incorrect, please notify the vote
-administrator immediatedly!</p>
-
-<hr />
-
-<h2>Place Your Vote Here</h2>
-
-<p>Rank each candidate in order of more preferred to least
-preferred. (e.g., 123 or 321 or 213, etc.)</p>
-
-<%= 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 (file)
index 0000000..d1eb9dc
--- /dev/null
@@ -0,0 +1,31 @@
+<% %>
+
+<h1>Please review your vote carefully before confirming it.</h1>
+
+<p>You have ranked the candidates in the following order (from most
+preferred to least preferred:</p>
+
+<ol>
+  <% for rank in @vote.rankings.sort %>
+  <li><%= rank.candidate.name %> </li>
+  <% end %>
+</ol>
+
+<table>
+<tr>
+  <td valign="top"><%= button_to 'Confirm', :action => 'confirm', :id => @voter.password %></td>
+  <td valign="top">Confirm this vote now. You will be able to go back and
+    change it.</td>
+</tr>
+
+<tr>
+  <td valign="top"><%= button_to 'Change', :action => 'index', :id => @voter.password %></td>
+  <td valign="top">Go back to the voting page and vote again.</td>
+</tr>
+
+<tr>
+  <td valign="top"><%= button_to 'Discard', :action => 'discard' %></td>
+  <td valign="top">Discard this tentative vote and log out.</td>
+</tr>
+
+</table>
diff --git a/app/views/voter/thanks.rhtml b/app/views/voter/thanks.rhtml
new file mode 100644 (file)
index 0000000..ab8992c
--- /dev/null
@@ -0,0 +1,9 @@
+<% %>
+
+<p>Your vote has been recorded.</p>
+
+<p>Your unique token for this vote is: <strong><%= @voter.vote.token %></strong></p>
+
+<p>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.</p>
diff --git a/app/views/voter/vote.rhtml b/app/views/voter/vote.rhtml
new file mode 100644 (file)
index 0000000..2d30209
--- /dev/null
@@ -0,0 +1,35 @@
+<% %>
+
+<h1>Vote Below the Line</h1>
+
+<p><strong>Election:</strong> <%= @voter.election.name %></p>
+
+<p><strong>Voter:</strong> <%= @voter.email %></p>
+
+<p><strong>Candidates:</strong></p>
+
+<ol>
+<% for candidate in @voter.election.candidates.sort %>
+  <li><%= candidate.name %></li>
+<% end %>
+</ol>
+
+<p>If this information is incorrect, please notify the vote
+administrator immediatedly!</p>
+
+<hr />
+
+<h2>Place Your Vote Here</h2>
+
+<p>Rank each candidate in order of more preferred to least
+preferred. (e.g., 123 or 321 or 213, etc.)</p>
+
+<%= form_tag :action => 'review', :id => @voter.password %>
+<%= text_field :vote, :votestring -%>
+<%= submit_tag "Submit!" %>
+<%= end_form_tag %>
+
+
+
+
+
index 1ead71b75c34a98179303f795d795bf36f7ce2cd..de8cbafdbeb2f33f1dd364192e16b0dcd593da39 100644 (file)
@@ -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' 
index b7b50bfa70320f61ee9d4b74c79ecd8119abd9f2..2b32457befb0fdde307c147e214bc623dc689007 100644 (file)
@@ -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 (file)
index 0000000..17961d1
--- /dev/null
@@ -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 (file)
index 0000000..17961d1
--- /dev/null
@@ -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 (file)
index 0000000..8794d28
--- /dev/null
@@ -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 (file)
index 0000000..1c3820e
--- /dev/null
@@ -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

Benjamin Mako Hill || Want to submit a patch?