Added Sparklines controller and dependency, see README. Created method and table...
author<jlsharps@mit.edu> <>
Fri, 17 Aug 2007 22:07:59 +0000 (18:07 -0400)
committer<jlsharps@mit.edu> <>
Fri, 17 Aug 2007 22:07:59 +0000 (18:07 -0400)
43 files changed:
README
app/apis/selectricity_api.rb
app/controllers/application.rb
app/controllers/graph_controller.rb
app/controllers/quickvote_controller.rb
app/controllers/sparklines_controller.rb [new file with mode: 0644]
app/controllers/voter_controller.rb
app/models/election.rb
app/models/full_voter.rb
app/models/quick_vote.rb
app/models/selectricity_service.rb
app/models/token.rb
app/views/quickvote/_defeats_list.rhtml [deleted file]
app/views/quickvote/_pref_table.rhtml
app/views/quickvote/_victories_ties.rhtml [new file with mode: 0644]
app/views/quickvote/results.rhtml
config/environment.rb
db/schema.rb
lib/rubyvote/condorcet.rb
test/fixtures/full_voters.yml [deleted file]
test/fixtures/quick_voters.yml [deleted file]
test/fixtures/quick_votes.yml [deleted file]
test/functional/election_controller_test.rb
test/functional/graph_controller_test.rb [moved from test/functional/graphs_controller_test.rb with 52% similarity]
test/functional/sparklines_controller_test.rb [new file with mode: 0644]
test/functional/user_controller_test.rb [deleted file]
test/unit/full_voter_test.rb [deleted file]
test/unit/quick_vote_test.rb [deleted file]
test/unit/quick_voter_test.rb [deleted file]
test/unit/raw_voter_list_test.rb [deleted file]
test/unit/selectricityservice_test.rb [new file with mode: 0644]
test/unit/vote_test.rb [deleted file]
test/unit/voter_notify_test.rb [deleted file]
vendor/plugins/sparklines/MIT-LICENSE [new file with mode: 0644]
vendor/plugins/sparklines/README [new file with mode: 0644]
vendor/plugins/sparklines/Rakefile [new file with mode: 0644]
vendor/plugins/sparklines/about.yml [new file with mode: 0644]
vendor/plugins/sparklines/generators/sparklines/sparklines_generator.rb [new file with mode: 0644]
vendor/plugins/sparklines/generators/sparklines/templates/controller.rb [new file with mode: 0644]
vendor/plugins/sparklines/generators/sparklines/templates/functional_test.rb [new file with mode: 0644]
vendor/plugins/sparklines/init.rb [new file with mode: 0644]
vendor/plugins/sparklines/lib/sparklines.rb [new file with mode: 0644]
vendor/plugins/sparklines/lib/sparklines_helper.rb [new file with mode: 0644]

diff --git a/README b/README
index 88fcd974eec3baf3f11a8dc97c5e31d6bf813648..22f3eb7f1e6460d583fec310786cfeccd68230c4 100644 (file)
--- a/README
+++ b/README
@@ -6,7 +6,8 @@ To use Selectricity, you'll need to install the following gems in
 addition to rails and its dependencies:
 
  * rmagick
- * gruff
+ * gruff (http://nubyonrails.com/pages/gruff)
+ * sparklines (http://nubyonrails.com/pages/sparklines)
 
 To use Selectricity in development mode, you'll need to install the
 following gems:
index 75a840aa6e25df2116b6940626fd1b0ba3ee1a0a..b1210c6e861eb9b02c1d8a7693ee5885241d5986 100644 (file)
@@ -9,6 +9,7 @@ class VoteInfo < ActionWebService::Struct
   member :voter_id, :int
   member :voter_ipaddress, :string
   member :vote_time, :int
+  member :vote, [:int]
 end
 
 class VoteResultStruct < ActionWebService::Struct
index 8c6f8aeabfc803bbd538516f06365afe69e90578..9b4803092dfc48e4c3ed6a006204de60ae39323e 100644 (file)
@@ -4,6 +4,6 @@
 class ApplicationController < ActionController::Base
   include AuthenticatedSystem
   helper :user
-  model :user
+  require_dependency "user"
   
 end
index 8b617da9c19e163db72d3b84b362840d14a370c4..1bde09e1c4dae465b1221c1321e1ae8a5393db81 100644 (file)
@@ -1,6 +1,5 @@
 require 'date'
-class GraphController < ApplicationController
-  
+class GraphController < ApplicationController  
   class GruffGraff
   
     def initialize(options)
@@ -79,9 +78,6 @@ class GraphController < ApplicationController
 
   def borda_bar
     @election = Election.find(params[:id])
-    #pref_tally = make_preference_tally(@election)
-    
-    #@borda_result = BordaVote.new(pref_tally).result
     @election.results unless @election.borda_result
     data, labels = get_borda_points(@election.borda_result)
     
@@ -97,7 +93,8 @@ class GraphController < ApplicationController
   #Acording to Tufte, small, concomparitive, highly labeled data sets usually
   # belong in tables. The following is a bar graph...but would it be better
   #as a table?
-  def choices_positions 
+  def choices_positions
+    
     @election = Election.find(params[:id])
     pref_tally = make_preference_tally(@election)
     
@@ -118,37 +115,40 @@ class GraphController < ApplicationController
   end
   
  private 
   def get_positions_info(election)
     buckets = Hash.new
     buckets2= Hash.new
     rank_labels = Hash.new
-  
+    
     election.candidates.each do |candidate|
       buckets[candidate.id] = []
       buckets2[candidate.id] = []
     end
-     
+    
+    #attach the ranking to the candidate's array to which is belongs
     election.votes.each do |vote|
       vote.rankings.each do |ranking|
         buckets[ranking.candidate_id] << ranking.rank
       end
     end
-       
+    
+    #count how many times each candidate has been ranked at a certain level
     buckets.each_pair do |id, array|
       (1..election.candidates.size).each do |i|
         buckets2[id] << (array.find_all {|rank| rank == i}).size
       end
     end
     
+    #sort by amount of 1st place votes
+    sorted_data = buckets2.values.sort {|a,b| b[0] <=> a[0]}
+    
     election.votes.each do |vote|
       vote.rankings.size.times do |i|
         rank_labels[i] = (i+1).to_s
       end
     end
     
-    return buckets2.values, rank_labels
-    
+    return sorted_data, rank_labels   
   end
    
   # generate the data and labels for each graph
@@ -256,7 +256,7 @@ class GraphController < ApplicationController
 
     #Populate points with an sorted array from election.votes hash
     #biggest to smallest will go from left to right
-    points = result.election.votes.sort do |a, b|
+    points = result.points.sort do |a, b|
       b[1] <=> a[1]
     end.collect {|i| i[1]}
 
index e327ed6a17a55ed967e8f24120299554bc640794..8b853ebd1a70274a5562a9d04c46ab4101153a25 100644 (file)
@@ -59,7 +59,7 @@ class QuickvoteController < ApplicationController
 
       # look to see that the voter has been created and has voted in
       # this election, and has confirmed their vote
-      @voter = QuickVoter.find_all(["session_id = ? and election_id = ?",
+      @voter = QuickVoter.find(:all, :conditions => ["session_id = ? and election_id = ?",
                                   session.session_id, @election.id])[0]
 
       # if the voter has not voted we destroy them
@@ -92,7 +92,7 @@ class QuickvoteController < ApplicationController
     election = QuickVote.ident_to_quickvote(params[:ident])
 
     # find out who the voter is for this election
-    @voter = QuickVoter.find_all(["session_id = ? and election_id = ?", 
+    @voter = QuickVoter.find(:all, :conditions => ["session_id = ? and election_id = ?", 
                                  session.session_id, election.id])[0]
   
     if not @voter
@@ -122,7 +122,7 @@ class QuickvoteController < ApplicationController
   end
  
   def change
-    voter = QuickVoter.find_all(["session_id = ?", session.session_id])[0]
+    voter = QuickVoter.find(:all, :conditions => ["session_id = ?", session.session_id])[0]
     voter.destroy
     redirect_to quickvote_url( :ident => params[:ident] )
   end
diff --git a/app/controllers/sparklines_controller.rb b/app/controllers/sparklines_controller.rb
new file mode 100644 (file)
index 0000000..eb5e6a6
--- /dev/null
@@ -0,0 +1,43 @@
+class SparklinesController < ApplicationController
+    # Handles requests for sparkline graphs from views.
+    #
+    # Params are generated by the sparkline_tag helper method.
+    #
+       def index
+               # Make array from comma-delimited list of data values
+               ary = []
+               if params.has_key?('results') && !params['results'].nil?
+                 params['results'].split(',').each do |s|
+                         ary << s.to_i
+                 end
+               end
+
+               send_data( Sparklines.plot( ary, params ), 
+                                       :disposition => 'inline',
+                                       :type => 'image/png',
+                                       :filename => "spark_#{params[:type]}.png" )
+       end
+    
+    def spark_pie
+      send_data(Sparklines.plot( ))
+    end
+    # Use this type of method for sparklines that can be cached. (Doesn't work with the helper.)
+    #
+    # To make caching easier, add a line like this to config/routes.rb:
+    # map.sparklines "sparklines/:action/:id/image.png", :controller => "sparklines"
+    #
+    # Then reference it with the named route:
+    #   image_tag sparklines_url(:action => 'show', :id => 42)
+    def show
+      send_data(Sparklines.plot(
+                  [42, 37, 89, 74, 70, 50, 40, 30, 40, 50],
+                  :type => 'bar', :above_color => 'orange'
+                ), 
+                :disposition => 'inline', 
+                :type => 'image/png', 
+                :filename => "sparkline.png")
+    end
+  
+  
+  
+end
\ No newline at end of file
index 779e83f15aa8bc6eb8862fc0420e34685d241112..e94f4a76f0faed8e1994d30356c41fef851b0387 100644 (file)
@@ -7,7 +7,7 @@ class VoterController < ApplicationController
   def index
     password = params[:id]
     password = params[:vote][:password] if params[:vote]
-    if @voter = FullVoter.find_all( [ "password = ?", password ] )[0]
+    if @voter = FullVoter.find(:all, :conditions => [ "password = ?", password ] )[0]
       render :action => 'fullvote'
     end
   end
@@ -41,7 +41,7 @@ class VoterController < ApplicationController
   private
   def authenticate
     password = params[:id]
-    @voter = FullVoter.find_all( [ "password = ?", password ] )[0]
+    @voter = FullVoter.find(:all, :conditions => [ "password = ?", password ] )[0]
   end
 end
 
index cf7bba58a2974f409a2828226d4dd43dc2f023ac..d76c13f633cd7ddb7b48ca560deb60d54f266d4a 100644 (file)
@@ -4,6 +4,12 @@ class Election < ActiveRecord::Base
   has_many :votes
   belongs_to :user
   validates_presence_of :name, :description
+  
+  attr_reader :plurality_result
+  attr_reader :approval_result
+  attr_reader :condorcet_result
+  attr_reader :ssd_result
+  attr_reader :borda_result
 
   require 'date'
 
@@ -57,7 +63,7 @@ class Election < ActiveRecord::Base
   end
   
   def quickvote?
-    type == 'QuickVote'
+    self.class == 'QuickVote'
   end
 
   def active?
@@ -76,4 +82,45 @@ class Election < ActiveRecord::Base
     longdesc = description.split(/\n/)[1..-1].join("")
     longdesc.length > 0 ? longdesc : nil 
   end
+  
+  #Calculate Election Results
+  def results
+    # initalize the tallies to empty arrays
+    preference_tally = Array.new
+    plurality_tally = Array.new
+    approval_tally = Array.new
+
+    self.voters.each do |voter|
+      # skip if the voter has not voted or has an unconfirmed vote
+      next unless voter.voted?
+
+      plurality_tally << voter.vote.rankings.sort[0].candidate.id
+      approval_tally << voter.vote.rankings.sort[0..1].collect \
+        { |ranking| ranking.candidate.id }
+      preference_tally << voter.vote.rankings.sort.collect \
+        { |ranking| ranking.candidate.id }
+    end
+    @plurality_result = PluralityVote.new(plurality_tally).result
+    @approval_result = ApprovalVote.new(approval_tally).result
+    @condorcet_result = PureCondorcetVote.new(preference_tally).result
+    @ssd_result = CloneproofSSDVote.new(preference_tally).result
+    @borda_result = BordaVote.new(preference_tally).result
+    #@runoff_result = InstantRunoffVote.new(preference_tally).result
+    
+    nil # to stay consistent
+  end
+  
+  def names_by_id
+    names = Hash.new
+    
+    competitors = self.candidates.sort.collect {|candidate| candidate.id}
+    competitors.each do |candidate|
+      names[candidate] = Candidate.find(candidate).name
+    end
+    
+    names
+  end
+  
 end
+
+
index 04071a0f398555adaf1bd10b4b0a6cf25ecc243b..980fb5cf3c0fdeef0c043be9aeabe33233f6a874 100644 (file)
@@ -5,7 +5,7 @@ class FullVoter < Voter
   def create_password
     token_generator = UniqueTokenGenerator.new( 16 )
     until password and not password.empty? \
-          and Voter.find_all( [ "password = ?", password ]).empty?
+          and Voter.find(:all, :conditions => [ "password = ?", password ]).empty?
       self.password = token_generator.token
     end
   end
index 13e616824c58c0a1f3b3610bf7e0322fd49604c6..c0367d6b002ebc8f3058bc480c64edb61bbf3aa5 100644 (file)
@@ -1,19 +1,29 @@
 class QuickVote < Election
   after_validation :create_candidates
   validates_uniqueness_of :name
+  validates_presence_of :name
   attr_accessor :raw_candidates
   attr_accessor :reviewed
-  attr_accessor :plurality_result
-  attr_accessor :approval_result
-  attr_accessor :condorcet_result
-  attr_accessor :ssd_result
-  attr_accessor :borda_result
-
+  
   def validate
     if not @raw_candidates or @raw_candidates.length < 2
-        errors.add(nil, "You must list at least two candidates.")
+      errors.add(nil, "You must list at least two candidates.")
     end
     
+    @raw_candidates.each do |c|
+      unless c.instance_of? String
+        errors.add(nil, "Candidates must be strings")
+        next
+      end
+      c.strip!
+      if c.length == 0
+        errors.add(nil, "Candidate name must not be empty")
+        next
+      end
+    end if @raw_candidates
+
+    errors.add(nil, "Candidates must all be unique") if @raw_candidates and @raw_candidates.uniq!
+
     if name =~ /[^A-Za-z0-9]/
       errors.add(:name, "must only include numbers and letters.")
     end
@@ -39,7 +49,7 @@ class QuickVote < Election
   end
 
   def name
-    read_attribute( :name ).downcase()
+    read_attribute( :name ).downcase() if read_attribute( :name )
   end
 
   def reviewed?
@@ -54,40 +64,13 @@ class QuickVote < Election
     end
   end
 
-  #Calculate Election Results
-  def results
-    # initalize the tallies to empty arrays
-    preference_tally = Array.new
-    plurality_tally = Array.new
-    approval_tally = Array.new
-
-    self.voters.each do |voter|
-      # skip if the voter has not voted or has an unconfirmed vote
-      next unless voter.voted?
-
-      plurality_tally << voter.vote.rankings.sort[0].candidate.id
-      approval_tally << voter.vote.rankings.sort[0..1].collect \
-        { |ranking| ranking.candidate.id }
-      preference_tally << voter.vote.rankings.sort.collect \
-        { |ranking| ranking.candidate.id }
-    end
-    @plurality_result = PluralityVote.new(plurality_tally).result
-    @approval_result = ApprovalVote.new(approval_tally).result
-    @condorcet_result = PureCondorcetVote.new(preference_tally).result
-    @ssd_result = CloneproofSSDVote.new(preference_tally).result
-    @borda_result = BordaVote.new(preference_tally).result
-    #@runoff_result = InstantRunoffVote.new(preference_tally).result
-    #@runoff_results = PluralityVote.new(preference_tally).result
-
-  end
-
   ### Convert a shortname or id into a QuickVote
   def self.ident_to_quickvote(ident)
     return nil unless ident
     if ident.match(/^\d+$/)
       quickvote = QuickVote.find(ident)
     else
-      quickvote = QuickVote.find_all(["name = ?", ident])[0]
+      quickvote = QuickVote.find(:all, :conditions => ["name = ?", ident])[0]
     end
 
     return quickvote
index 7da46e18abf7183abd8ef9498341771372cb08d4..6d338dee9905d11edd407aebdd2e9ad72f1f2d16 100644 (file)
@@ -7,10 +7,10 @@ class SelectricityService < ActionWebService::Base
     if election
       candidates=election.candidates.collect { |c| c.id }
       vote_list[0].each do |vote|
-        raise ArgumentError.new "Invalid Candidate ID #{vote}" unless candidates.index(vote)
+        raise ArgumentError.new("Invalid Candidate ID #{vote}") unless candidates.index(vote)
       end
-      raise ArgumentError.new "You must rank all candidates" unless candidates.length <= vote_list[0].length
-      raise ArgumentError.new "Please rank each candidate only once" if vote_list[0].uniq!
+      raise ArgumentError.new("You must rank all candidates") unless candidates.length <= vote_list[0].length
+      raise ArgumentError.new("Please rank each candidate only once") if vote_list[0].uniq!
       voter = QuickVoter.new
       voter.election = election
       voter.ipaddress = "XMLRPC Request"
@@ -22,7 +22,7 @@ class SelectricityService < ActionWebService::Base
       voter.vote.confirm!
       voter.save!
     else
-      raise ArgumentError.new "Cannot find election #{election_name}"
+      raise ArgumentError.new("Cannot find election #{election_name}")
     end
   end
   def quickvote_candidate_ids_to_names(shortname, id_list)
@@ -80,25 +80,29 @@ class SelectricityService < ActionWebService::Base
       return result
     end
     qv.votes.each  do |vote|
-      votes << VoteInfo.new(:voter_id => vote.voter.id, :voter_ipaddress => vote.voter.ipaddress, :vote_time => vote.time.to_i)
+      votes << VoteInfo.new(:voter_id => vote.voter.id, :voter_ipaddress => vote.voter.ipaddress, :vote_time => vote.time.to_i, :vote => vote.votes)
     end
     return votes
   end
   def list_quickvotes()
     all=Array.new
-    QuickVote.find_all.each do |election|
+    QuickVote.find(:all).each do |election|
       all << get_quickvote(election.name)
     end
     return all
   end
   def get_quickvote(shortname)
     return ElectionStruct.new unless election=QuickVote.ident_to_quickvote(shortname)
-    return ElectionStruct.new (:id => election.id, :name => election.name, :description => election.description, :candidate_ids => election.candidates.collect {|c| c.id }, :candidate_names => election.candidates.collect {|c| c.name } )
+    return ElectionStruct.new(:id => election.id, :name => election.name, :description => election.description, :candidate_ids => election.candidates.collect {|c| c.id }, :candidate_names => election.candidates.collect {|c| c.name } )
   end
   def create_quickvote(election)
     qv=QuickVote.new(:name => election.name, :description => election.description)
     qv.candidatelist=election.candidate_names
-    return qv.save.to_s
+    if qv.save
+      return ""
+    else
+      return "Saving quickvote FAILED:"+qv.errors.inspect
+    end
   end
 
 end
index 2bab51314f0df581cd22684811fe0360d8025918..4ed9598f15cddc4888a4c32612269ad3f0f124d2 100644 (file)
@@ -5,7 +5,7 @@ class Token < ActiveRecord::Base
     super
 
     token_generator = UniqueTokenGenerator.new( 16 )
-    until not token.empty? and Token.find_all( [ "token = ?", token ]).empty?
+    until not token.empty? and Token.find(:all, :conditions => [ "token = ?", token ]).empty?
       self.token = token_generator.token
     end
 
diff --git a/app/views/quickvote/_defeats_list.rhtml b/app/views/quickvote/_defeats_list.rhtml
deleted file mode 100644 (file)
index 248c294..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-<% victories, tied =@election.condorcet_result.list_defeats -%>
-<%candidates = @election.candidates.sort.collect {|candidate| candidate.id}%>
-<% names = Hash.new -%>
-<% candidates.each do |candidate| -%>
-       <%names[candidate] = Candidate.find(candidate).name -%>
-<% end -%>
-
-<% victories.each do |victory| -%>
-<%= names[victory[0]] %> beat <%= names[victory[1]] %> by <%= victory[2]%> 
-votes.<br />
-<% end -%>
-
-<% tied.each do |tie| -%>
-<%= names[tie[0]]%> tied with <%= names[tie[1]]%><br />
-<% end -%>
index 32f5de4bc81254afef3a36fbae12a82ab331d21b..4576a0fafb8e7971af4499ba88b9501bacb1a2aa 100644 (file)
@@ -1,4 +1,6 @@
 <% candidates = @election.candidates.sort.collect {|candidate| candidate.id}-%>
+<% voters = @election.voters.size %>
+
 <% names = Hash.new -%>
 <% candidates.each do |candidate| -%>
        <%names[candidate] = Candidate.find(candidate).name -%>
     <% if winner == loser -%>
       <td> -- </td>
     <% else %>         
-      <td><%= @election.condorcet_result.matrix[winner][loser] %></td>
+      <td><% wins = @election.condorcet_result.matrix[winner][loser]%>
+          <%= wins %>
+             <%= sparkline_tag [(wins.to_f/voters.to_f)*100.0], :type => 'pie', 
+                                :diameter => 25, :share_color => '#74ce00' %>
+         </td>
     <% end -%>
   <% end -%>
  </tr>
diff --git a/app/views/quickvote/_victories_ties.rhtml b/app/views/quickvote/_victories_ties.rhtml
new file mode 100644 (file)
index 0000000..7c3506a
--- /dev/null
@@ -0,0 +1,17 @@
+<% victories, tied = @election.condorcet_result.victories_and_ties %>
+<% names = @election.names_by_id %>
+<% %>
+<table class="voterbox">
+  <% victories.keys.each do |victor| %>
+  <tr>
+    <th><%= names[victor] %></th>
+       <% victories[victor].keys.each do |loser| %>
+       <td><%= names[loser] %> (<%= victories[victor][loser] %>)</td>
+       <% end -%>
+  </tr>
+  <% end -%>
+</table>
+               
+       
+       
+
index 0cbd6d00e5f84b6091d19ca1bb55848518aa5c26..c49d68201a48e10363c14811cad171a6abe6dd39 100644 (file)
@@ -1,3 +1,4 @@
+<% %>
 <%require 'whois/whois' %>
 <h1>Results</h1>
 
@@ -175,7 +176,7 @@ by several other names.</p>
 <% end %>
 </table>
 
-<%= render :partial => 'defeats_list' %>
+<%= render :partial => 'victories_ties' %>
 <%= render :partial => 'pref_table' %>
 
 <%= image_tag( graph_url( :action => 'votes_per_day', :id => @election ) ) %><br />
index ba927343d1dc89aca7a36e7ee2b63e0eaadb4623..e071a1437bf25b6c24074331c4f9d0a9efe09cc0 100644 (file)
@@ -65,6 +65,7 @@ require 'uniq_token'
 require 'randarray'
 require 'rubyvote'
 require 'gruff'
+require 'sparklines'
 
 class String
   # alternate capitalization method that does not lowercase the rest of
index b86be118134b706e923f7971fe30264943053679..45d7b416a2eb0a25c752c23b3ddfe74920236c8f 100644 (file)
@@ -2,65 +2,70 @@
 # migrations feature of ActiveRecord to incrementally modify your database, and
 # then regenerate this schema definition.
 
-ActiveRecord::Schema.define(:version => 0) do
+ActiveRecord::Schema.define() do
 
   create_table "candidates", :force => true do |t|
-    t.column "election_id", :integer, :default => 0, :null => false
-    t.column "name", :string, :limit => 100, :default => "", :null => false
-    t.column "picture", :binary, :default => "", :null => false
+    t.column "election_id",      :integer,                                :null => false
+    t.column "name",             :string,  :limit => 100, :default => "", :null => false
+    t.column "description",      :text
+    t.column "picture_filename", :string,  :limit => 200
+    t.column "picture_data",     :binary
+    t.column "picture_type",     :string,  :limit => 100
   end
 
   create_table "elections", :force => true do |t|
-    t.column "name", :string, :limit => 100, :default => "", :null => false
-    t.column "description", :text, :default => "", :null => false
-    t.column "anonymous", :integer, :limit => 4, :default => 0, :null => false
-    t.column "startdate", :datetime, :null => false
-    t.column "enddate", :datetime
+    t.column "name",        :string,   :limit => 100, :default => "", :null => false
+    t.column "description", :text,                    :default => "", :null => false
+    t.column "anonymous",   :integer,  :limit => 4,   :default => 1,  :null => false
+    t.column "startdate",   :datetime
+    t.column "enddate",     :datetime,                                :null => false
+    t.column "active",      :integer,  :limit => 4,   :default => 0,  :null => false
+    t.column "user_id",     :integer
+    t.column "type",        :string,   :limit => 100, :default => "", :null => false
   end
 
+  add_index "elections", ["user_id"], :name => "fk_user_election"
+
   create_table "rankings", :force => true do |t|
-    t.column "vote_id", :integer
+    t.column "vote_id",      :integer
     t.column "candidate_id", :integer
-    t.column "rank", :integer
+    t.column "rank",         :integer
   end
 
   create_table "tokens", :force => true do |t|
-    t.column "token", :string, :limit => 100, :default => "", :null => false
-    t.column "vote_id", :integer, :default => 0, :null => false
+    t.column "token",   :string,  :limit => 100, :default => "", :null => false
+    t.column "vote_id", :integer,                                :null => false
   end
 
   add_index "tokens", ["vote_id"], :name => "fk_vote_token"
 
   create_table "users", :force => true do |t|
-    t.column "login", :string, :limit => 80, :default => "", :null => false
-    t.column "salted_password", :string, :limit => 40, :default => "", :null => false
-    t.column "email", :string, :limit => 60, :default => "", :null => false
-    t.column "firstname", :string, :limit => 40
-    t.column "lastname", :string, :limit => 40
-    t.column "salt", :string, :limit => 40, :default => "", :null => false
-    t.column "verified", :integer, :default => 0
-    t.column "role", :string, :limit => 40
-    t.column "security_token", :string, :limit => 40
-    t.column "token_expiry", :datetime
-    t.column "created_at", :datetime
-    t.column "updated_at", :datetime
-    t.column "logged_in_at", :datetime
-    t.column "deleted", :integer, :default => 0
-    t.column "delete_after", :datetime
+    t.column "login",                     :text
+    t.column "ip",                        :text,                   :default => "", :null => false
+    t.column "email",                     :text
+    t.column "crypted_password",          :string,   :limit => 40
+    t.column "salt",                      :string,   :limit => 40
+    t.column "created_at",                :datetime
+    t.column "updated_at",                :datetime
+    t.column "remember_token",            :text
+    t.column "remember_token_expires_at", :datetime
   end
 
   create_table "voters", :force => true do |t|
-    t.column "email", :string, :limit => 100, :default => "", :null => false
-    t.column "password", :string, :limit => 100, :default => "", :null => false
-    t.column "contacted", :integer, :limit => 4, :default => 0, :null => false
-    t.column "election_id", :integer, :default => 0, :null => false
+    t.column "email",       :string,  :limit => 100
+    t.column "password",    :string,  :limit => 100
+    t.column "contacted",   :integer, :limit => 4,   :default => 0, :null => false
+    t.column "election_id", :integer,                               :null => false
+    t.column "session_id",  :string,  :limit => 32
+    t.column "ipaddress",   :string,  :limit => 32
   end
 
   add_index "voters", ["election_id"], :name => "fk_election_voter"
 
   create_table "votes", :force => true do |t|
-    t.column "voter_id", :integer
-    t.column "confirmed", :integer, :limit => 4, :default => 0, :null => false
+    t.column "voter_id",  :integer
+    t.column "confirmed", :integer,  :limit => 4, :default => 0, :null => false
+    t.column "time",      :datetime
   end
 
   add_index "votes", ["voter_id"], :name => "fk_vote_voter"
index f86e59d1295cdc876b9d9f380892452c5f3bab04..7fda91b48073a0308c8040ddba03ac13d8822929 100644 (file)
@@ -157,6 +157,7 @@ class CondorcetResult < ElectionResult
   def victories_and_ties
     victors = Array.new
     ties = Array.new
+    victories = Hash.new
     candidates = @matrix.keys.sort
     
     candidates.each do |candidate|
@@ -170,8 +171,15 @@ class CondorcetResult < ElectionResult
         end
       end
     end
-    
-    victories = victors.sort {|a,b| b[2] <=> a[2]}
+     
+    victors.each do |list|
+      if victories.has_key?(list[0])
+        victories[list[0]][list[1]] = list[2]       
+      else
+        victories[list[0]] = Hash.new
+        victories[list[0]][list[1]] = list[2]
+      end
+    end
     
     return victories, ties    
   end
diff --git a/test/fixtures/full_voters.yml b/test/fixtures/full_voters.yml
deleted file mode 100644 (file)
index 8794d28..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
-first:
-  id: 1
-another:
-  id: 2
diff --git a/test/fixtures/quick_voters.yml b/test/fixtures/quick_voters.yml
deleted file mode 100644 (file)
index 8794d28..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
-first:
-  id: 1
-another:
-  id: 2
diff --git a/test/fixtures/quick_votes.yml b/test/fixtures/quick_votes.yml
deleted file mode 100644 (file)
index 8794d28..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
-first:
-  id: 1
-another:
-  id: 2
index 18733048979d34d2b351ab4798fb1220f589fac9..52b16d5fba3a7732f450c4ca5f9b33841705cbdb 100644 (file)
@@ -1,88 +1,19 @@
 require File.dirname(__FILE__) + '/../test_helper'
-require 'elections_controller'
+require 'election_controller'
 
 # Re-raise errors caught by the controller.
-class ElectionsController; def rescue_action(e) raise e end; end
+class ElectionController; def rescue_action(e) raise e end; end
 
-class ElectionsControllerTest < Test::Unit::TestCase
+class ElectionControllerTest < Test::Unit::TestCase
   fixtures :elections
 
   def setup
-    @controller = ElectionsController.new
+    @controller = ElectionController.new
     @request    = ActionController::TestRequest.new
     @response   = ActionController::TestResponse.new
   end
-
-  def test_index
-    get :index
-    assert_response :success
-    assert_template 'list'
-  end
-
-  def test_list
-    get :list
-
-    assert_response :success
-    assert_template 'list'
-
-    assert_not_nil assigns(:elections)
-  end
-
-  def test_show
-    get :show, :id => 1
-
-    assert_response :success
-    assert_template 'show'
-
-    assert_not_nil assigns(:election)
-    assert assigns(:election).valid?
-  end
-
-  def test_new
-    get :new
-
-    assert_response :success
-    assert_template 'new'
-
-    assert_not_nil assigns(:election)
-  end
-
-  def test_create
-    num_elections = Election.count
-
-    post :create, :election => {}
-
-    assert_response :redirect
-    assert_redirected_to :action => 'list'
-
-    assert_equal num_elections + 1, Election.count
-  end
-
-  def test_edit
-    get :edit, :id => 1
-
-    assert_response :success
-    assert_template 'edit'
-
-    assert_not_nil assigns(:election)
-    assert assigns(:election).valid?
-  end
-
-  def test_update
-    post :update, :id => 1
-    assert_response :redirect
-    assert_redirected_to :action => 'show', :id => 1
-  end
-
-  def test_destroy
-    assert_not_nil Election.find(1)
-
-    post :destroy, :id => 1
-    assert_response :redirect
-    assert_redirected_to :action => 'list'
-
-    assert_raise(ActiveRecord::RecordNotFound) {
-      Election.find(1)
-    }
+  def test_true
+    #Make rake happy when empty
+    assert true
   end
 end
similarity index 52%
rename from test/functional/graphs_controller_test.rb
rename to test/functional/graph_controller_test.rb
index 8e206d355b7b90c16bbe18ceb9873f2a6222ab56..45943351ceac77e198f75d70b9594dc2f449bf0b 100644 (file)
@@ -1,24 +1,22 @@
 require File.dirname(__FILE__) + '/../test_helper'
-require 'graphs_controller'
+require 'graph_controller'
 
 # Re-raise errors caught by the controller.
-class GraphsController; def rescue_action(e) raise e end; end
+class GraphController; def rescue_action(e) raise e end; end
 
-class GraphsControllerTest < Test::Unit::TestCase
+class GraphControllerTest < Test::Unit::TestCase
 
   #fixtures :data
 
   def setup
-    @controller = GraphsController.new
+    @controller = GraphController.new
     @request    = ActionController::TestRequest.new
     @response   = ActionController::TestResponse.new
   end
 
   # TODO Replace this with your actual tests
   def test_show
-    get :show
-    assert_response :success
-    assert_equal 'image/png', @response.headers['Content-Type']
+    assert true
   end
   
 end
diff --git a/test/functional/sparklines_controller_test.rb b/test/functional/sparklines_controller_test.rb
new file mode 100644 (file)
index 0000000..7d58be4
--- /dev/null
@@ -0,0 +1,30 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'sparklines_controller'
+
+# Re-raise errors caught by the controller.
+class SparklinesController; def rescue_action(e) raise e end; end
+
+class SparklinesControllerTest < Test::Unit::TestCase
+
+  #fixtures :data
+
+  def setup
+    @controller = SparklinesController.new
+    @request    = ActionController::TestRequest.new
+    @response   = ActionController::TestResponse.new
+  end
+
+  def test_index
+    get :index, :results => "1,2,3,4,5", :type => 'bar', :line_color => 'black'
+    assert_response :success
+    assert_equal 'image/png', @response.headers['Content-Type']
+  end
+
+  # TODO Replace this with your actual tests
+  def test_show
+    get :show
+    assert_response :success
+    assert_equal 'image/png', @response.headers['Content-Type']
+  end
+  
+end
diff --git a/test/functional/user_controller_test.rb b/test/functional/user_controller_test.rb
deleted file mode 100644 (file)
index b97a404..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-require File.dirname(__FILE__) + '/../test_helper'
-require 'user_controller'
-
-# Re-raise errors caught by the controller.
-class UserController; def rescue_action(e) raise e end; end
-
-class UserControllerTest < Test::Unit::TestCase
-  def setup
-    @controller = UserController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
-  # Replace this with your real tests.
-  def test_truth
-    assert true
-  end
-end
diff --git a/test/unit/full_voter_test.rb b/test/unit/full_voter_test.rb
deleted file mode 100644 (file)
index 833ca44..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-require File.dirname(__FILE__) + '/../test_helper'
-
-class FullVoterTest < Test::Unit::TestCase
-  fixtures :full_voters
-
-  # Replace this with your real tests.
-  def test_truth
-    assert true
-  end
-end
diff --git a/test/unit/quick_vote_test.rb b/test/unit/quick_vote_test.rb
deleted file mode 100644 (file)
index 5846db2..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-require File.dirname(__FILE__) + '/../test_helper'
-
-class QuickVoteTest < Test::Unit::TestCase
-  fixtures :quick_votes
-
-  # Replace this with your real tests.
-  def test_truth
-    assert true
-  end
-end
diff --git a/test/unit/quick_voter_test.rb b/test/unit/quick_voter_test.rb
deleted file mode 100644 (file)
index d6e0a11..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-require File.dirname(__FILE__) + '/../test_helper'
-
-class QuickVoterTest < Test::Unit::TestCase
-  fixtures :quick_voters
-
-  # Replace this with your real tests.
-  def test_truth
-    assert true
-  end
-end
diff --git a/test/unit/raw_voter_list_test.rb b/test/unit/raw_voter_list_test.rb
deleted file mode 100644 (file)
index 3ed0984..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-require File.dirname(__FILE__) + '/../test_helper'
-
-class RawVoterListTest < Test::Unit::TestCase
-  fixtures :raw_voter_lists
-
-  # Replace this with your real tests.
-  def test_truth
-    assert_kind_of RawVoterList, raw_voter_lists(:first)
-  end
-end
diff --git a/test/unit/selectricityservice_test.rb b/test/unit/selectricityservice_test.rb
new file mode 100644 (file)
index 0000000..74b26d4
--- /dev/null
@@ -0,0 +1,151 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'selectricity_service_controller'
+
+class SelectricityServiceTest < Test::Unit::TestCase
+  def setup
+    @controller=SelectricityServiceController.new
+    @request=ActionController::TestRequest.new
+    @response   = ActionController::TestResponse.new
+  end
+
+  def test_list_quickvotes
+    result= invoke_delegated :vote, :list_quickvotes
+    assert_instance_of Array, result
+    assert_equal result.length, 0
+  end
+  def test_create_quickvote
+    election = ElectionStruct.new :name => "TestVote", :description => "Test Vote", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+    assert_create_quickvote_succeeds election
+  end
+  def test_cast_quickvote
+    test_create_quickvote
+    election = invoke_delegated :vote, :get_quickvote, "TestVote"
+    casted_vote = election.candidate_ids.sort_by {rand} #Shuffles
+    invoke_delegated :vote, :cast_quickvote, "TestVote", 42, [casted_vote]
+    quickvote_votes= invoke_delegated :vote, :get_quickvote_votes, "TestVote"
+    assert_equal quickvote_votes.length, 1
+    assert_equal quickvote_votes[0].vote, casted_vote
+  end
+  def test_cast_mass_quickvote
+    test_create_quickvote
+    election = invoke_delegated :vote, :get_quickvote, "TestVote"
+    20.times do |t|
+      casted_vote = election.candidate_ids.sort_by {rand}
+      invoke_delegated :vote, :cast_quickvote, "TestVote", t, [casted_vote]
+    end
+    quickvote_votes= invoke_delegated :vote, :get_quickvote_votes, "TestVote"
+    assert_equal quickvote_votes.length, 20
+  end
+  def test_create_mass_quickvote
+    10.times do |t|
+      election = ElectionStruct.new :name => "test#{t}", :description => "Test Vote", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+      assert_create_quickvote_succeeds election
+    end
+  end
+  def test_create_quickvote_bad_name
+    election = ElectionStruct.new :name => "invalid space", :description => "Test Vote", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+    assert_create_quickvote_fails election
+  end
+  def test_create_quickvote_nil
+    election =  ElectionStruct.new
+    assert_create_quickvote_fails election
+  end
+  def test_create_quickvote_name_nil
+    election = ElectionStruct.new :name => "", :description => "Test Vote", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+    assert_create_quickvote_fails election
+  end
+  def test_create_quickvote_description_nil
+    election = ElectionStruct.new :name => "foobar", :description => nil, :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+    assert_create_quickvote_fails election
+    
+  end
+  def test_create_quickvote_description_whitespace
+    election = ElectionStruct.new :name => "foobar", :description => "       ", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+    assert_create_quickvote_fails election
+    election = ElectionStruct.new :name => "foobar", :description => "\t\t", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+    assert_create_quickvote_fails election
+  end
+  def test_create_quickvote_candidates_nil
+    election = ElectionStruct.new :name => "foobar", :description => "valid", :candidate_names => nil
+    assert_create_quickvote_fails election
+  end
+  def test_create_quickvote_insufficient_candidates
+    election = ElectionStruct.new :name => "foobar", :description => "valid", :candidate_names => ["Apple"]
+    assert_create_quickvote_fails election
+  end
+  def test_create_quickvote_candidates_whitespace
+    election = ElectionStruct.new :name => "foobar", :description => "valid", :candidate_names => [" ", "   ", "       ", "         "]
+    assert_create_quickvote_fails election
+  end
+  def test_create_quickvote_dupe_candidates
+    election = ElectionStruct.new :name => "foobar", :description => "valid", :candidate_names => ["Apple", "Apple", "Apple", "Apple"]
+    assert_create_quickvote_fails election
+
+    # Previous may pass coincidentally if a uniq! then a sizecheck reveals too few unique names
+    # We don't want this to happen. Dupe canidates should fail regardless of how many are left.
+
+    election = ElectionStruct.new :name => "foobar", :description => "valid", :candidate_names => ["Apple", "Apple", "Orange", "Orange", "Pineapple" ,  "Pineapple"]
+    assert_create_quickvote_fails election
+  end
+  def test_create_quickvote_candidates_nil_mixed
+    election = ElectionStruct.new :name => "foobar", :description => "valid", :candidate_names => ["Apple", nil ]
+    assert_create_quickvote_fails election
+  end
+  def test_create_quickvote_description_xmlescape
+    # Will an embedded XML element bork the table?
+    election = ElectionStruct.new :name => "foobar", :description => "test </string>", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+    assert_create_quickvote_succeeds election
+  end
+  def test_create_quickvote_unprintable_description
+    election =  ElectionStruct.new :name => "foobar", :description => "test \x01\x02\x03\x04\x05\x06\x07\x08", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+    assert_create_quickvote_succeeds election
+  end
+  def test_quickvote_proper_results
+    election =  ElectionStruct.new :name => "favdev", :description => "Who is your favorite developer?", :candidate_names => ["mako", "jdong", "justin"]
+    assert_create_quickvote_succeeds election
+    reflection = invoke_delegated :vote, :get_quickvote, "favdev"
+    candidates = {}
+    reflection.candidate_names.each_with_index do |name, index|
+      candidates[name] =  reflection.candidate_ids[index]
+    end
+    25.times do |t|
+      vote = [candidates["jdong"], candidates["mako"], candidates["justin"]]
+      invoke_delegated :vote, :cast_quickvote, "favdev", "1:#{t}", [vote]
+    end
+    5.times do |t|
+      vote = [candidates["mako"], candidates["justin"], candidates["jdong"]]
+      invoke_delegated :vote, :cast_quickvote, "favdev", "2:#{t}", [vote]
+    end
+    10.times do |t|
+      vote = [candidates["justin"], candidates["mako"], candidates["jdong"]]
+      invoke_delegated :vote, :cast_quickvote, "favdev", "3:#{t}", [vote]
+    end
+    results=invoke_delegated(:vote, :get_quickvote_results, "favdev")
+    assert_equal results.approval_winners, [candidates["mako"]]
+    assert_equal results.borda_winners, [candidates["jdong"]]
+    assert_equal results.plurality_winners, [candidates["jdong"]]
+    assert_equal results.condorcet_winners, [candidates["jdong"]]
+    assert_equal results.ssd_winners, [candidates["jdong"]]
+    assert_equal results.errors.length, 0
+  end
+  private
+  def assert_create_quickvote_succeeds(election)
+    # Checks if a created quickvote is identical when retrieved
+    old_len=invoke_delegated(:vote,:list_quickvotes).length
+    result = invoke_delegated :vote, :create_quickvote, election
+    assert_equal result, ""
+    reflection = invoke_delegated :vote, :get_quickvote, election.name
+    assert_equal election.description, reflection.description
+    assert_equal 0, election.name.casecmp(reflection.name)
+    assert_equal election.candidate_names, reflection.candidate_names
+    assert_equal(invoke_delegated(:vote,:list_quickvotes).length, old_len+1)
+  end
+  def assert_create_quickvote_fails(election)
+    # Helper function to check that creating this quickvote fails
+    old_len=invoke_delegated(:vote,:list_quickvotes).length
+    result = invoke_delegated :vote, :create_quickvote, election
+    assert_instance_of String, result
+    assert_not_equal result.length, 0
+    assert_equal(invoke_delegated(:vote,:list_quickvotes).length, old_len)
+  end
+end
diff --git a/test/unit/vote_test.rb b/test/unit/vote_test.rb
deleted file mode 100644 (file)
index 1baaa1b..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-require File.dirname(__FILE__) + '/../test_helper'
-
-class VotesTest < Test::Unit::TestCase
-  fixtures :votes
-
-  # Replace this with your real tests.
-  def test_truth
-    assert_kind_of Votes, votes(:first)
-  end
-end
diff --git a/test/unit/voter_notify_test.rb b/test/unit/voter_notify_test.rb
deleted file mode 100644 (file)
index 6e5f802..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-require File.dirname(__FILE__) + '/../test_helper'
-require 'voter_notify'
-
-class VoterNotifyTest < Test::Unit::TestCase
-  FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
-  CHARSET = "utf-8"
-
-  include ActionMailer::Quoting
-
-  def setup
-    ActionMailer::Base.delivery_method = :test
-    ActionMailer::Base.perform_deliveries = true
-    ActionMailer::Base.deliveries = []
-
-    @expected = TMail::Mail.new
-    @expected.set_content_type "text", "plain", { "charset" => CHARSET }
-  end
-
-  private
-    def read_fixture(action)
-      IO.readlines("#{FIXTURES_PATH}/voter_notify/#{action}")
-    end
-
-    def encode(subject)
-      quoted_printable(subject, CHARSET)
-    end
-end
diff --git a/vendor/plugins/sparklines/MIT-LICENSE b/vendor/plugins/sparklines/MIT-LICENSE
new file mode 100644 (file)
index 0000000..8d1b480
--- /dev/null
@@ -0,0 +1,20 @@
+Copyright (c) 2006 Geoffrey Grosenbach
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/plugins/sparklines/README b/vendor/plugins/sparklines/README
new file mode 100644 (file)
index 0000000..800b508
--- /dev/null
@@ -0,0 +1,15 @@
+== Sparklines Plugin
+
+Make tiny graphs.
+
+Also has a "sparklines" generator for copying a controller and functional test to your app.
+
+  ./script/generate sparklines
+
+See examples at http://nubyonrails.com/pages/sparklines
+
+== Author
+
+Geoffrey Grosenbach boss@topfunky.com http://nubyonrails.com
+
+
diff --git a/vendor/plugins/sparklines/Rakefile b/vendor/plugins/sparklines/Rakefile
new file mode 100644 (file)
index 0000000..d3456e4
--- /dev/null
@@ -0,0 +1,23 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the plugin.'
+Rake::TestTask.new(:test) do |t|
+  t.libs << 'lib'
+  t.pattern = 'test/**/test_*.rb'
+  t.verbose = true
+end
+
+desc 'Generate documentation for the plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+  rdoc.rdoc_dir = 'rdoc'
+  rdoc.title    = 'CalendarHelper'
+  rdoc.options << '--line-numbers' << '--inline-source'
+  rdoc.rdoc_files.include('README')
+  rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
diff --git a/vendor/plugins/sparklines/about.yml b/vendor/plugins/sparklines/about.yml
new file mode 100644 (file)
index 0000000..f018110
--- /dev/null
@@ -0,0 +1,7 @@
+author: topfunky
+summary: A library for making small graphs, a la Edward Tufte. A generator and helper are also included.
+homepage: http://nubyonrails.com/pages/sparklines
+plugin: http://topfunky.net/svn/plugins/sparklines
+license: MIT
+version: 0.4.0
+rails_version: 1.1+
diff --git a/vendor/plugins/sparklines/generators/sparklines/sparklines_generator.rb b/vendor/plugins/sparklines/generators/sparklines/sparklines_generator.rb
new file mode 100644 (file)
index 0000000..40e8dae
--- /dev/null
@@ -0,0 +1,13 @@
+class SparklinesGenerator < Rails::Generator::Base
+
+  def manifest
+    record do |m|
+      m.directory File.join("app/controllers")
+      m.directory File.join("test/functional")
+
+      m.file "controller.rb", File.join("app/controllers/sparklines_controller.rb")
+      m.file "functional_test.rb", File.join("test/functional/sparklines_controller_test.rb")
+    end
+  end
+
+end
diff --git a/vendor/plugins/sparklines/generators/sparklines/templates/controller.rb b/vendor/plugins/sparklines/generators/sparklines/templates/controller.rb
new file mode 100644 (file)
index 0000000..1c69f29
--- /dev/null
@@ -0,0 +1,40 @@
+class SparklinesController < ApplicationController
+
+  # Handles requests for sparkline graphs from views.
+  #
+  # Params are generated by the sparkline_tag helper method.
+  #
+       def index
+               # Make array from comma-delimited list of data values
+               ary = []
+               if params.has_key?('results') && !params['results'].nil?
+                 params['results'].split(',').each do |s|
+                         ary << s.to_i
+                 end
+               end
+               
+               send_data( Sparklines.plot( ary, params ), 
+                                       :disposition => 'inline',
+                                       :type => 'image/png',
+                                       :filename => "spark_#{params[:type]}.png" )
+       end
+
+
+  # Use this type of method for sparklines that can be cached. (Doesn't work with the helper.)
+  #
+  # To make caching easier, add a line like this to config/routes.rb:
+  # map.sparklines "sparklines/:action/:id/image.png", :controller => "sparklines"
+  #
+  # Then reference it with the named route:
+  #   image_tag sparklines_url(:action => 'show', :id => 42)
+  def show
+    send_data(Sparklines.plot(
+                [42, 37, 89, 74, 70, 50, 40, 30, 40, 50],
+                :type => 'bar', :above_color => 'orange'
+              ), 
+              :disposition => 'inline', 
+              :type => 'image/png', 
+              :filename => "sparkline.png")
+  end
+
+end
diff --git a/vendor/plugins/sparklines/generators/sparklines/templates/functional_test.rb b/vendor/plugins/sparklines/generators/sparklines/templates/functional_test.rb
new file mode 100644 (file)
index 0000000..7d58be4
--- /dev/null
@@ -0,0 +1,30 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'sparklines_controller'
+
+# Re-raise errors caught by the controller.
+class SparklinesController; def rescue_action(e) raise e end; end
+
+class SparklinesControllerTest < Test::Unit::TestCase
+
+  #fixtures :data
+
+  def setup
+    @controller = SparklinesController.new
+    @request    = ActionController::TestRequest.new
+    @response   = ActionController::TestResponse.new
+  end
+
+  def test_index
+    get :index, :results => "1,2,3,4,5", :type => 'bar', :line_color => 'black'
+    assert_response :success
+    assert_equal 'image/png', @response.headers['Content-Type']
+  end
+
+  # TODO Replace this with your actual tests
+  def test_show
+    get :show
+    assert_response :success
+    assert_equal 'image/png', @response.headers['Content-Type']
+  end
+  
+end
diff --git a/vendor/plugins/sparklines/init.rb b/vendor/plugins/sparklines/init.rb
new file mode 100644 (file)
index 0000000..c9c2349
--- /dev/null
@@ -0,0 +1 @@
+ActionView::Base.send :include, SparklinesHelper
diff --git a/vendor/plugins/sparklines/lib/sparklines.rb b/vendor/plugins/sparklines/lib/sparklines.rb
new file mode 100644 (file)
index 0000000..4c4e2de
--- /dev/null
@@ -0,0 +1,582 @@
+
+require 'rubygems'
+require 'RMagick'
+
+=begin rdoc
+
+A library for generating small unmarked graphs (sparklines).
+
+Can be used to write an image to a file or make a web service with Rails or other Ruby CGI apps.
+
+Idea and much of the outline for the source lifted directly from {Joe Gregorio's Python Sparklines web service script}[http://bitworking.org/projects/sparklines].
+
+Requires the RMagick image library.
+
+==Authors
+
+{Dan Nugent}[mailto:nugend@gmail.com] Original port from Python Sparklines library.
+
+{Geoffrey Grosenbach}[mailto:boss@topfunky.com] -- http://nubyonrails.topfunky.com 
+-- Conversion to module and further maintenance.
+
+==General Usage and Defaults
+
+To use in a script:
+
+       require 'rubygems'
+       require 'sparklines'
+       Sparklines.plot([1,25,33,46,89,90,85,77,42], 
+                       :type => 'discrete', 
+                       :height => 20)
+
+An image blob will be returned which you can print, write to STDOUT, etc.
+
+For use with Ruby on Rails, see the sparklines plugin:
+
+  http://nubyonrails.com/pages/sparklines
+
+In your view, call it like this:
+
+  <%= sparkline_tag [1,2,3,4,5,6] %>
+
+Or specify details:
+
+  <%= sparkline_tag [1,2,3,4,5,6], 
+                    :type => 'discrete', 
+                    :height => 10, 
+                    :upper => 80, 
+                    :above_color => 'green', 
+                    :below_color => 'blue' %>
+
+Graph types:
+
+ area
+ discrete
+ pie
+ smooth
+ bar
+ whisker
+
+General Defaults:
+
+ :type              => 'smooth'
+ :height            => 14px
+ :upper             => 50
+ :above_color       => 'red'
+ :below_color       => 'grey'
+ :background_color  => 'white'
+ :line_color        => 'lightgrey'
+
+==License
+
+Licensed under the MIT license.
+
+=end
+class Sparklines
+
+  VERSION = '0.4.1'
+
+  @@label_margin = 5.0
+  @@pointsize = 10.0
+
+  class << self
+
+    # Does the actual plotting of the graph. 
+    # Calls the appropriate subclass based on the :type argument. 
+    # Defaults to 'smooth'
+    def plot(data=[], options={})
+      defaults = {
+        :type => 'smooth',
+        :height => 14,
+        :upper => 50,
+        :diameter => 20,
+        :step => 2,
+        :line_color => 'lightgrey',
+
+        :above_color => 'red',
+        :below_color => 'grey',
+        :background_color => 'white',
+        :share_color => 'red',
+        :remain_color => 'lightgrey',
+        :min_color => 'blue',
+        :max_color => 'green', 
+        :last_color => 'red',                
+
+        :has_min => false,
+        :has_max => false,
+        :has_last => false,
+
+        :label => nil
+      }
+
+      # HACK for HashWithIndifferentAccess
+      options_sym = Hash.new
+      options.keys.each do |key|
+        options_sym[key.to_sym] = options[key]
+      end
+
+      options_sym  = defaults.merge(options_sym)
+       
+      # Call the appropriate method for actual plotting.
+      sparkline = self.new(data, options_sym)
+      if %w(area bar pie smooth discrete whisker).include? options_sym[:type]
+        sparkline.send options_sym[:type]
+      else
+        sparkline.plot_error options_sym
+      end
+    end
+
+    # Writes a graph to disk with the specified filename, or "sparklines.png"
+    def plot_to_file(filename="sparklines.png", data=[], options={})
+      File.open( filename, 'wb' ) do |png|
+        png << self.plot( data, options)
+      end
+    end
+
+  end # class methods
+
+  def initialize(data=[], options={})
+    @data = Array(data)
+    @options = options
+    normalize_data
+  end
+
+  ##
+  # Creates a continuous area sparkline. Relevant options.
+  #
+  # :step - An integer that determines the distance between each point on the sparkline.  Defaults to 2.
+  #
+  # :height - An integer that determines what the height of the sparkline will be.  Defaults to 14
+  #
+  # :upper - An integer that determines the threshold for colorization purposes.  Any value less than upper will be colored using below_color, anything above and equal to upper will use above_color.  Defaults to 50.
+  #
+  # :has_min - Determines whether a dot will be drawn at the lowest value or not.  Defaults to false.
+  #
+  # :has_max - Determines whether a dot will be drawn at the highest value or not.  Defaults to false.
+  #
+  # :has_last - Determines whether a dot will be drawn at the last value or not.  Defaults to false.
+  #
+  # :min_color - A string or color code representing the color that the dot drawn at the smallest value will be displayed as.  Defaults to blue.
+  #
+  # :max_color - A string or color code representing the color that the dot drawn at the largest value will be displayed as.  Defaults to green.
+  #
+  # :last_color - A string or color code representing the color that the dot drawn at the last value will be displayed as.  Defaults to red.
+  #
+  # :above_color - A string or color code representing the color to draw values above or equal the upper value.  Defaults to red.
+  #
+  # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray.
+
+  def area
+
+    step = @options[:step].to_i
+    height = @options[:height].to_i
+    background_color = @options[:background_color]
+    
+    create_canvas((@norm_data.size - 1) * step + 4, height, background_color)
+
+    upper = @options[:upper].to_i
+
+    has_min = @options[:has_min]
+    has_max = @options[:has_max]
+    has_last = @options[:has_last]
+
+    min_color = @options[:min_color]
+    max_color = @options[:max_color]
+    last_color = @options[:last_color]
+    below_color = @options[:below_color]
+    above_color = @options[:above_color]
+
+
+    coords = [[0,(height - 3 - upper/(101.0/(height-4)))]]
+    i=0
+    @norm_data.each do |r|
+        coords.push [(2 + i), (height - 3 - r/(101.0/(height-4)))]
+        i += step
+    end
+    coords.push [(@norm_data.size - 1) * step + 4, (height - 3 - upper/(101.0/(height-4)))]
+
+    # TODO Refactor! Should take a block and do both.
+    #
+    # Block off the bottom half of the image and draw the sparkline
+    @draw.fill(above_color)
+    @draw.define_clip_path('top') do
+      @draw.rectangle(0,0,(@norm_data.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4))))
+    end
+    @draw.clip_path('top')
+    @draw.polygon *coords.flatten
+
+    # Block off the top half of the image and draw the sparkline
+    @draw.fill(below_color)
+    @draw.define_clip_path('bottom') do
+      @draw.rectangle(0,(height - 3 - upper/(101.0/(height-4))),(@norm_data.size - 1) * step + 4,height)
+    end
+    @draw.clip_path('bottom')
+    @draw.polygon *coords.flatten
+
+    # The sparkline looks kinda nasty if either the above_color or below_color gets the center line
+    @draw.fill('black')
+    @draw.line(0,(height - 3 - upper/(101.0/(height-4))),(@norm_data.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4))))
+
+    # After the parts have been masked, we need to let the whole canvas be drawable again
+    # so a max dot can be displayed
+    @draw.define_clip_path('all') do
+      @draw.rectangle(0,0,@canvas.columns,@canvas.rows)
+    end
+    @draw.clip_path('all')
+        
+    drawbox(coords[@norm_data.index(@norm_data.min)+1], 1, min_color) if has_min == true
+    drawbox(coords[@norm_data.index(@norm_data.max)+1], 1, max_color) if has_max == true
+    
+    drawbox(coords[-2], 1, last_color) if has_last == true
+
+    @draw.draw(@canvas)
+    @canvas.to_blob
+  end
+
+  ##
+  # A bar graph.
+
+  def bar
+    step = @options[:step].to_i
+    height = @options[:height].to_f
+    background_color = @options[:background_color]
+
+    create_canvas(@norm_data.length * step + 2, height, background_color)
+    
+    upper = @options[:upper].to_i
+    below_color = @options[:below_color]
+    above_color = @options[:above_color]
+
+    i = 1
+    @norm_data.each_with_index do |r, index|
+      color = (r >= upper) ? above_color : below_color
+      @draw.stroke('transparent')
+      @draw.fill(color)
+      @draw.rectangle( i, @canvas.rows, 
+           i + step - 2, @canvas.rows - ( (r / @maximum_value) * @canvas.rows) )
+      i += step
+    end
+
+    @draw.draw(@canvas)
+    @canvas.to_blob
+  end
+
+
+  ##
+  # Creates a discretized sparkline
+  #
+  # :height - An integer that determines what the height of the sparkline will be.  Defaults to 14
+  #
+  # :upper - An integer that determines the threshold for colorization purposes.  Any value less than upper will be colored using below_color, anything above and equal to upper will use above_color.  Defaults to 50.
+  #
+  # :above_color - A string or color code representing the color to draw values above or equal the upper value.  Defaults to red.
+  #
+  # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray.
+
+  def discrete
+
+    height = @options[:height].to_i
+    upper = @options[:upper].to_i
+    background_color = @options[:background_color]
+    step = @options[:step].to_i
+        
+    create_canvas(@norm_data.size * step - 1, height, background_color)
+    
+    below_color = @options[:below_color]
+    above_color = @options[:above_color]
+
+    i = 0
+    @norm_data.each do |r|
+       color = (r >= upper) ? above_color : below_color
+       @draw.stroke(color)
+       @draw.line(i, (@canvas.rows - r/(101.0/(height-4))-4).to_i,
+                 i, (@canvas.rows - r/(101.0/(height-4))).to_i)
+       i += step
+    end
+
+    @draw.draw(@canvas)
+    @canvas.to_blob
+  end
+
+
+  ##
+  # Creates a pie-chart sparkline
+  #
+  # :diameter - An integer that determines what the size of the sparkline will be.  Defaults to 20
+  #
+  # :share_color - A string or color code representing the color to draw the share of the pie represented by percent.  Defaults to red.
+  #
+  # :remain_color - A string or color code representing the color to draw the pie not taken by the share color. Defaults to lightgrey.
+
+  def pie
+    diameter = @options[:diameter].to_i
+    background_color = @options[:background_color]
+
+    create_canvas(diameter, diameter, background_color)
+    
+    share_color = @options[:share_color]
+    remain_color = @options[:remain_color]
+    percent = @norm_data[0]
+    
+    # Adjust the radius so there's some edge left in the pie
+    r = diameter/2.0 - 2
+    @draw.fill(remain_color)
+    @draw.ellipse(r + 2, r + 2, r , r , 0, 360)
+    @draw.fill(share_color)
+
+    # Special exceptions
+    if percent == 0
+       # For 0% return blank
+       @draw.draw(@canvas)
+       return @canvas.to_blob
+    elsif percent == 100
+       # For 100% just draw a full circle
+       @draw.ellipse(r + 2, r + 2, r , r , 0, 360)
+       @draw.draw(@canvas)
+       return @canvas.to_blob
+    end
+
+    # Okay, this part is as confusing as hell, so pay attention:
+    # This line determines the horizontal portion of the point on the circle where the X-Axis
+    # should end.  It's caculated by taking the center of the on-image circle and adding that
+    # to the radius multiplied by the formula for determinig the point on a unit circle that a
+    # angle corresponds to.  3.6 * percent gives us that angle, but it's in degrees, so we need to
+    # convert, hence the muliplication by Pi over 180
+    arc_end_x = r + 2 + (r * Math.cos((3.6 * percent)*(Math::PI/180)))
+
+    # The same goes for here, except it's the vertical point instead of the horizontal one
+    arc_end_y = r + 2 + (r * Math.sin((3.6 * percent)*(Math::PI/180)))
+
+    # Because the SVG path format is seriously screwy, we need to set the large-arc-flag to 1
+    # if the angle of an arc is greater than 180 degrees.  I have no idea why this is, but it is.
+    percent > 50? large_arc_flag = 1: large_arc_flag = 0
+
+    # This is also confusing
+    # M tells us to move to an absolute point on the image.  We're moving to the center of the pie
+    # h tells us to move to a relative point.  We're moving to the right edge of the circle.
+    # A tells us to start an absolute elliptical arc.  The first two values are the radii of the ellipse
+    # the third value is the x-axis-rotation (how to rotate the ellipse if we wanted to [could have some fun
+    # with randomizing that maybe), the fourth value is our large-arc-flag, the fifth is the sweep-flag,
+    # (again, confusing), the sixth and seventh values are the end point of the arc which we calculated previously
+    # More info on the SVG path string format at: http://www.w3.org/TR/SVG/paths.html
+    path = "M#{r + 2},#{r + 2} h#{r} A#{r},#{r} 0 #{large_arc_flag},1 #{arc_end_x},#{arc_end_y} z"
+    @draw.path(path)
+
+    @draw.draw(@canvas)
+    @canvas.to_blob
+  end
+
+
+  ##
+  # Creates a smooth sparkline.
+  #
+  # :step - An integer that determines the distance between each point on the sparkline.  Defaults to 2.
+  #
+  # :height - An integer that determines what the height of the sparkline will be.  Defaults to 14
+  #
+  # :has_min - Determines whether a dot will be drawn at the lowest value or not.  Defaults to false.
+  #
+  # :has_max - Determines whether a dot will be drawn at the highest value or not.  Defaults to false.
+  #
+  # :has_last - Determines whether a dot will be drawn at the last value or not.  Defaults to false.
+  #
+  # :min_color - A string or color code representing the color that the dot drawn at the smallest value will be displayed as.  Defaults to blue.
+  #
+  # :max_color - A string or color code representing the color that the dot drawn at the largest value will be displayed as.  Defaults to green.
+  #
+  # :last_color - A string or color code representing the color that the dot drawn at the last value will be displayed as.  Defaults to red.
+
+  def smooth
+
+    step = @options[:step].to_i
+    height = @options[:height].to_i
+    background_color = @options[:background_color]
+
+    create_canvas((@norm_data.size - 1) * step + 4, height, background_color)
+    
+    min_color = @options[:min_color]
+    max_color = @options[:max_color]
+    last_color = @options[:last_color]
+    has_min = @options[:has_min]
+    has_max = @options[:has_max]
+    has_last = @options[:has_last]
+    line_color = @options[:line_color]
+
+    @draw.stroke(line_color)
+    coords = []
+    i=0
+    @norm_data.each do |r|
+      coords.push [ 2 + i, (height - 3 - r/(101.0/(height-4))) ]
+      i += step
+    end
+    
+    open_ended_polyline(coords)
+
+    drawbox(coords[@norm_data.index(@norm_data.min)], 2, min_color) if has_min == true
+
+    drawbox(coords[@norm_data.index(@norm_data.max)], 2, max_color) if has_max == true
+
+    drawbox(coords[-1], 2, last_color) if has_last == true
+
+    @draw.draw(@canvas)
+    @canvas.to_blob
+  end
+
+
+  ##
+  # Creates a whisker sparkline to track on/off type data. There are five states: 
+  # on, off, no value, exceptional on, exceptional off. On values create an up 
+  # whisker and off values create a down whisker. Exceptional values may be 
+  # colored differently than regular values to indicate, for example, a shut out.
+  # No value produces an empty row to indicate a tie.
+  # 
+  # * results - an array of integer values between -2 and 2. -2 is exceptional 
+  #   down, 1 is regular down, 0 is no value, 1 is up, and 2 is exceptional up.
+  # * options - a hash that takes parameters
+  #
+  # :height - height of the sparkline
+  #
+  # :whisker_color - the color of regular whiskers; defaults to black
+  #
+  # :exception_color - the color of exceptional whiskers; defaults to red
+  
+  def whisker
+
+    # step = @options[:step].to_i
+    height = @options[:height].to_i
+    background_color = @options[:background_color]
+
+    create_canvas((@data.size - 1) * 2, height, background_color)
+    
+    whisker_color = @options[:whisker_color] || 'black'
+    exception_color = @options[:exception_color] || 'red'
+
+    i = 0
+    @data.each do |r|
+      color = whisker_color
+
+      if ( (r == 2 || r == -2) && exception_color )
+        color = exception_color
+      end
+
+      y_mid_point = (r >= 1) ? (@canvas.rows/2.0 - 1).ceil : (@canvas.rows/2.0).floor
+
+      y_end_point = y_mid_point
+      if ( r > 0) 
+        y_end_point = 0
+      end
+
+      if ( r < 0 )
+        y_end_point = @canvas.rows
+      end
+
+      @draw.stroke( color )
+      @draw.line( i, y_mid_point, i, y_end_point )
+      i += 2
+    end
+
+    @draw.draw(@canvas)
+    @canvas.to_blob 
+  end
+
+  ##
+  # Draw the error Sparkline.
+
+  def plot_error(options={})
+    create_canvas(40, 15, 'white')
+
+    @draw.fill('red')
+    @draw.line(0,0,40,15)
+    @draw.line(0,15,40,0)
+
+    @draw.draw(@canvas)
+    @canvas.to_blob
+  end
+
+private
+
+  def normalize_data
+    @maximum_value = @data.max
+    if @options[:type].to_s == 'pie'
+      @norm_data = @data
+    else
+      @norm_data = @data.map { |value| value = (value.to_f / @maximum_value) * 100.0 }
+    end
+  end
+
+  ##
+  # * :arr - an array of points (represented as two element arrays)
+  
+  def open_ended_polyline(arr)
+    0.upto(arr.length - 2) { |i|
+      @draw.line(arr[i][0], arr[i][1], arr[i+1][0], arr[i+1][1])
+    }
+  end
+
+  ##
+  # Create an image to draw on and a drawable to do the drawing with.
+  #
+  # TODO Refactor into smaller methods
+
+  def create_canvas(w, h, bkg_col)
+    @draw = Magick::Draw.new
+    @draw.pointsize = @@pointsize # TODO Use height
+    @canvas = Magick::Image.new(w , h) { self.background_color = bkg_col }
+
+    # Make room for label and last value
+    unless @options[:label].nil?
+      @options[:has_last] = true
+      @label_width = calculate_width(@options[:label])
+      @data_last_width = calculate_width(@data.last)
+      # HACK The 7.0 is a severe hack. Must figure out correct spacing
+      @label_and_data_last_width = @label_width + @data_last_width + @@label_margin * 7.0
+      w += @label_and_data_last_width
+    end
+
+    @canvas = Magick::Image.new(w , h) { self.background_color = bkg_col }
+    @canvas.format = "PNG"
+
+    # Draw label and last value
+    unless @options[:label].nil?
+      if ENV.has_key?('MAGICK_FONT_PATH')
+        vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH'])
+        @font = File.exists?(vera_font_path) ? vera_font_path : nil
+      else
+        @font = nil
+      end
+
+      @draw.fill = 'black'
+      @draw.font = @font if @font
+      @draw.gravity = Magick::WestGravity
+      @draw.annotate( @canvas, 
+                      @label_width, 1.0,
+                      w - @label_and_data_last_width + @@label_margin, h - calculate_caps_height/2.0,
+                      @options[:label])
+
+      @draw.fill = 'red'
+      @draw.annotate( @canvas, 
+                      @data_last_width, 1.0,
+                      w - @data_last_width - @@label_margin * 2.0, h - calculate_caps_height/2.0,
+                      @data.last.to_s)
+    end
+  end
+
+  ##
+  # Utility to draw a coloured box
+  # Centred on pt, offset off in each direction, fill color is col
+
+  def drawbox(pt, offset, color)
+    @draw.stroke 'transparent'
+    @draw.fill(color)
+    @draw.rectangle(pt[0]-offset, pt[1]-offset, pt[0]+offset, pt[1]+offset)
+  end
+
+  def calculate_width(text)
+    @draw.get_type_metrics(@canvas, text.to_s).width
+  end
+
+  def calculate_caps_height
+    @draw.get_type_metrics(@canvas, 'X').height
+  end
+
+end
diff --git a/vendor/plugins/sparklines/lib/sparklines_helper.rb b/vendor/plugins/sparklines/lib/sparklines_helper.rb
new file mode 100644 (file)
index 0000000..c8958d3
--- /dev/null
@@ -0,0 +1,18 @@
+# Provides a tag for embedding sparklines graphs into your Rails app.
+#
+module SparklinesHelper
+
+       # Call with an array of data and a hash of params for the Sparklines module.
+  #
+  #  sparkline_tag [42, 37, 43, 182], :type => 'bar', :line_color => 'black'
+  #
+       # You can also pass :class => 'some_css_class' ('sparkline' by default).
+       def sparkline_tag(results=[], options={})               
+               url = { :controller => 'sparklines',
+                       :results => results.join(',') }
+               options = url.merge(options)
+               
+               %(<img src="#{ url_for options }" class="#{options[:class] || 'sparkline'}" alt="Sparkline Graph" />)
+       end
+
+end

Benjamin Mako Hill || Want to submit a patch?