Merge branch 'live' of ssh://ephesus.xvm.mit.edu/org/selectricity/selectricity-live
authorBenjamin Mako Hill <mako@atdot.cc>
Sun, 17 Jun 2012 22:59:36 +0000 (18:59 -0400)
committerBenjamin Mako Hill <mako@atdot.cc>
Sun, 17 Jun 2012 22:59:36 +0000 (18:59 -0400)
Conflicts:
README
app/controllers/voter_controller.rb
app/views/layouts/_footer.rhtml

1  2 
README
app/controllers/quickvote_controller.rb
app/controllers/voter_controller.rb
app/models/election.rb
app/models/vote.rb

diff --combined README
index 9ec9d6212afaf1f645fdbb1b53e278961539d592,afa0a254c62471f21751d52f69a0e31de1d923cc..5ce586b2191f237948d5201ed70ae1b45508902d
--- 1/README
--- 2/README
+++ b/README
@@@ -8,35 -8,28 +8,43 @@@ distribute, or rework Selectricity unde
  course, we'd sure like it if you would send fixes back to us and tell us
  about cool stuff you do with our software!
  
--The best way to get Selectricity is just to download it from our source
- tree.  At the moment, we're hosting our code at Gitorious which is a
- free software hosting provider. You can download Gitorious at the
- project page by following detailed instructions at Gitorious:
++The best way to get Selectricity is just to download it from our
+ source repository. You'll need the Git version control system or
+ source control manager to check it. You can get it here:
  
-   http://gitorious.org/selectricity
+   http://git-scm.com/
+ Once you have it, getting the source code is pretty easy. You just need
+ to check out a branch with a command like this:
+   git clone http://projects.mako.cc/source/selectricity/.git
+ By default, this will create a working copy with the latest
+ *development* version of our code. If you want the latest production
+ version (i.e., what we're running on the site), you need to switch to
+ the live version of the software which is kept in a branch called
+ "live." Once you cloned the repository above, you can switch into the
+ directory (i.e., run "cd selectricity") and then run the following
+ command:
+   git checkout -b live origin/live
  
  
- In terms of bugs and documentation, we current plan to build this out in
- our Gitorious wiki so feel free to get started with your own efforts
- along these lines there:
-   http://gitorious.org/selectricity/pages/Home
 +===============================================
 +=== Getting Help and Contributing =============
 +===============================================
 +
 +If you have a question, you can always email the core team at:
 +
 +  team@selectricity.org
 +
 +If you want to get involved in development, want to discuss
 +selectricity, or want to participate, please subscribe to our mailing
 +list here:
 +
 +  http://mailman.mit.edu/mailman/listinfo/selectricity
 +
 +
  ===============================================
  === Dependencies ==============================
  ===============================================
@@@ -65,18 -58,88 +73,17 @@@ On Ubuntu, you can install install the 
  Our server configuration uses Mongrel (installed from gems) behind an
  Apache2 load balancing proxy using mod_proxy.
  
 +You'll also need to have a MTA installed. We use Postfix and have not
 +tried it with any other system. Presumably though, anything that
 +provides '/usr/bin/sendmail' should work.
 +
  
  ===============================================
  === Contributors to Selectricity Include ======
  ===============================================
  
   * Benjamin Mako Hill <mako@atdot.cc>
   * John Dong          <jdong@ubuntu.com>
 - * Justin Sharps      <jlsharps@mit.edu>
 -
 -===============================================
 -=== Log =======================================
 -===============================================
 -
 -07/31/07
 -jlsharps: I've added a user authentication system known as
 -"acts_as_authenticated" to the code. The plugin is the the vendor/plugins 
 -directory. The two most noticeable changes are the AccountController and a 
 -redone User model. I've left the UserController in place for now, but the 
 -AccountController works in a different manner, so am switching over to that 
 -gradually. I saved the 5 lines or so in the old User model, overwrote 
 -it with the authenticated generator and then recopied the old stuff back in: 
 -has_many :elections and the name() method. The generator also creates its own 
 -migration file, but since we are using a create.sql file I adopted the 
 -migration file into a new users table in the create.sql file. I have yet to 
 -delete the old table because I haven't fully combed through the code yet and 
 -determined how many of the old attributes (such as first_name, last_name) may 
 -need to be retained. 
 -http://technoweenie.stikipad.com/plugins/show/Acts+as+Authenticated is the 
 -best site for documentation regarding acts_as_authenticaed. Also, currently
 -it only stores the user_id in the session, but i just found a guide to help 
 -me make it store the entire user object, so I'll do that while my battery 
 -charges.
 -
 -08/03/07
 -Handy trick: use the command 'gem_server' from a shell to create a server at 
 -http://localhost:8008 that is an easy to navigate locally-hosted website with 
 -all the documentation on local gems you have in a easy to read format.
 -
 -jlsharps: I added the Gruff plug-in today, which is viewable under the folder
 -vender/plugins/gruff.  I installed it directly using the Gruff plug-in and
 -included controller generate utility. The version 0.1.2, which doesn't seem to
 -be the latest version. I've looked into it and it see and it seems that the
 -latest version is 0.2.8. However, I wasn't sure how including a gem w/o a plugin 
 -would function in end-game rails so I just what I used for now. If you guys 
 -(mako of john) know how to do it, it'd probably be better to upgrade, but it 
 -didn't seem like the best use of my time right now.  I got the plug-in here:
 -http://topfunky.net/svn/plugins/gruff. You can get the gruff gem v 0.2.8 by 
 -typing "sudo gem install gruff", I believe it's also hosted on RubyForge. 
 -
 -I created the GraphsController for Gruff methods to use. In Pollarize I put them 
 -in the ApplicationContorller file, so they would be accessible to all. While 
 -that it also an option here, it would also mean there wouldn't be much room for 
 -playing around because everything in the Application file has to be perfect or 
 -it seems to throw Error Code 500 (basically everything breaks). The show() 
 -is a sample sample provided with Gruff. 
 -
 -Documentation is here:http://gruff.rubyforge.org/  Alternately, if you have the 
 -gem installed, you can use the ri command, or the above mentioned gem_server. 
 -
 -If you guys want more helpful stuff here, let me know.
 -
 -======================================
 -=== XML-RPC INFO                    ==
 -======================================
 -
 -The XML-RPC API is still under development, but is somewhat functional already:
 -
 -To instantiate a client in Ruby, try something like:
 -client=ActionWebService::Client::XmlRpc.new(SelectricityAPI,"http://localhost:3000/selectricity_service/vote")
 -
 -
 -Getting the results of a quickvote is quite simple:
 -?> client.get_quickvote_results("test")
 -=> #<VoteResultStruct:0x336f92c @approval_winners=[1, 2], @borda_winners=[1], @plurality_winners=[1], @ssd_winners=[1], @errors=[], @condorcet_winners=[1]>
 -
 -Casting a quickvote:
 -client.cast_quickvote("test",1,[[1,2]])
 -
 -To figure out what you're voting for:
 ->> client.get_quickvote_candidate_map("test")=> #<CandidateMap:0x335bbc0 @errors=[], @candidate_names=["test", "test2"], @candidate_ids=[1, 2]>
 -
 -
 -
 + * Justin Sharps      <jlsharps@alum.mit.edu>
  
  
index 4b3c0c988d75544b3b9f93945a48e8a016f3fec0,cd16743fcc16e2095f46b1eff115c47f2cd51d39..701005d08ed03f1835326cf2f598aaa663ea6ac7
@@@ -2,8 -2,19 +2,8 @@@
  # Copyright (C) 2007, 2008 Benjamin Mako Hill <mako@atdot.cc>
  # Copyright (C) 2007 Massachusetts Institute of Technology
  #
 -# This program is free software: you can redistribute it and/or modify
 -# it under the terms of the GNU Affero General Public License as
 -# published by the Free Software Foundation, either version 3 of the
 -# License, or (at your option) any later version.
 -#
 -# This program is distributed in the hope that it will be useful, but
 -# WITHOUT ANY WARRANTY; without even the implied warranty of
 -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 -# Affero General Public License for more details.
 -#
 -# You should have received a copy of the GNU Affero General Public
 -# License along with this program.  If not, see
 -# <http://www.gnu.org/licenses/>.
 +# This program is free software. Please see the COPYING file for
 +# details.
  
  class QuickvoteController < ApplicationController
    helper :sparklines
@@@ -44,7 -55,6 +44,7 @@@
  
        #Give registered users additional QuickVote functionality 
        @quickvote.user_id = session[:user][:id] if session[:user]
 +      @quickvote.create_candidates
  
        # try to save, if it fails, show the page again (the flash should
        # still be intact
      # if the person has specified an election, we show them the voting
      # page. otherwise, we redirect back to main the page
      if @election
 +
 +      # if the election is over, redirect to the the results page
 +      unless @election.active?
 +        redirect_to quickaction_url(:ident => params[:ident],
 +                                    :action => 'results')
 +      end
 +
        # look to see that the voter has been created and has voted in
        # this election, and has confirmed their vote
        @voter = QuickVoter.find(:all,
      else
        
        # record the ip address for posterity
-       @voter.ipaddress = request.env["REMOTE_ADDR"]
+       @voter.ipaddress = request.env["HTTP_X_FORWARDED_FOR"]
        @voter.save
        
        # toggle the confirmation bit      
-       @voter.vote.confirm!
-      
-       @voter.reload
-       render :action => 'thanks'
+       if @voter.vote.confirm!
+         @voter.reload
+         render :action => 'thanks'
+       else
+         redirect_to :action => 'index'
+       end
      end
    end
   
      @election=QuickVote.ident_to_quickvote(params[:id])
      @election.voters.each do |voter|
        next unless voter.ipaddress
        location=nil
-       if defined? Cache and location=Cache.get("GEO:#{voter.ipaddress}")
-       elsif defined? Cache
+       if Cache and location=Cache.get("GEO:#{voter.ipaddress}")
+       elsif Cache
          location = GeoKit::Geocoders::IpGeocoder.geocode(voter.ipaddress)
          Cache.set "GEO:#{voter.ipaddress}", location
        else
index 408bdab880ec0f411a1d19f8eea0261f3b5d1d02,78f9a0cdef8da7de802f83800c6484b470500f8e..a550f676cc3d2ced5c851eef5c4f4c8632363159
@@@ -2,8 -2,19 +2,8 @@@
  # Copyright (C) 2007, 2008 Benjamin Mako Hill <mako@atdot.cc>
  # Copyright (C) 2007 Massachusetts Institute of Technology
  #
 -# This program is free software: you can redistribute it and/or modify
 -# it under the terms of the GNU Affero General Public License as
 -# published by the Free Software Foundation, either version 3 of the
 -# License, or (at your option) any later version.
 -#
 -# This program is distributed in the hope that it will be useful, but
 -# WITHOUT ANY WARRANTY; without even the implied warranty of
 -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 -# Affero General Public License for more details.
 -#
 -# You should have received a copy of the GNU Affero General Public
 -# License along with this program.  If not, see
 -# <http://www.gnu.org/licenses/>.
 +# This program is free software. Please see the COPYING file for
 +# details.
  
  class VoterController < ApplicationController
    helper :sparklines
@@@ -12,9 -23,6 +12,9 @@@
    require_dependency "vote"
    require_dependency "election"
  
 +  before_filter :authenticate, :except => [:index, :login, :reminder,
 +                                           :kiosk_ready, :sort_candidates]
 +
    def index
      if params[:election_id]
        @election = Election.find(params[:election_id])
@@@ -22,7 -30,8 +22,7 @@@
          @voter = OpenVoter.find(:all,
            :conditions => ["session_id = ? and election_id = ?",
            session.session_id, @election.id])[0]
 -      
 -
 +     
          @voter = OpenVoter.new unless @voter
  
          @voter.election = @election
        else
          @sidebar_content = render_to_string(:partial => 'vote_sidebar')
          if @election.embeddable? and params[:embed] == "true"
 -          #look for custom theme, and assign to instance variabels for widget use
 +          # look for custom theme, and assign to instance variabels
 +          # for widget use
            if @election.embed_custom_string
              @top_bar = SkinPicture.find(:first,
 -            :conditions => ["filename = ?", @election.embed_custom_string + "top_bar.png"])
 +              :conditions => ["filename = ?",
 +                @election.embed_custom_string + "top_bar.png"])
              @default_image = SkinPicture.find(:first,
 -            :conditions => ["filename = ?", @election.embed_custom_string + "default_image.png"])
 +              :conditions => ["filename = ?",
 +                @election.embed_custom_string + "default_image.png"])
              @bg1 = SkinPicture.find(:first,
 -            :conditions => ["filename = ?", @election.embed_custom_string + "bg1.png"])
 +              :conditions => ["filename = ?",
 +                @election.embed_custom_string + "bg1.png"])
              @bg2 = SkinPicture.find(:first,
 -            :conditions => ["filename = ?", @election.embed_custom_string + "bg2.png"])
 +              :conditions => ["filename = ?",
 +                @election.embed_custom_string + "bg2.png"])
              @bottom_bar = SkinPicture.find(:first,
 -            :conditions => ["filename = ?", @election.embed_custom_string + "bottom_bar.png"])
 +              :conditions => ["filename = ?",
 +                @election.embed_custom_string + "bottom_bar.png"])
            end
            render :template => 'embed/full_vote', :layout => 'embed'
          else
  
    def login
      if params[:vote] and params[:vote][:password]
 -      redirect_to votepassword_url( :action => 'index', :urlpassword => params[:vote][:password])
 +      redirect_to votepassword_url(:action => 'index',
 +        :urlpassword => params[:vote][:password])
      else
        redirect_to :action => 'index'
      end
    end
    
 -  def pref_tables
 -    if authenticate
 -      @election = @voter.election
 -      @results = @election.results
 -      @candidates = {}
 -      @election.candidates.each {|c| @candidates[c.id] = c}
 -      @names = @election.names_by_id
 -      render :template => 'common/pref_tables', :layout => 'basic'
 -    else
 -      redirect_to :action => 'index'
 -    end
 -  end
 -
 -  def details
 -    if authenticate
 -      @election = @voter.election
 -      @votes = @election.votes.select {|v| v.confirmed? }.shuffle
 -      @voters = @votes.collect {|v| v.voter}.shuffle
 -      render :action => 'details'
 -    else
 -      redirect_to :action => 'index'
 -    end
 -  end
 -
    def review
 -    if authenticate
 -      @voter.vote.time = Time.now
 -      @voter.vote.save
 -      @voter.reload
 -    else
 -      redirect_to :action => 'index'
 -    end
 +    @voter.vote.time = Time.now
 +    @voter.vote.save
 +    @voter.reload
    end
  
    def confirm
-     @voter.vote.confirm!
-     if @voter.election.embeddable? and params[:embed] == "true" \
-       and @voter.election.early_results?
-       redirect_to :action => :results, :id => @password, :embed => 'true'
-     elsif not(@voter.election.verifiable) \
-       and @voter.election.kiosk and params[:kiosk] == "true"
-       redirect_to :action => "kiosk_ready", :id => @password, :kiosk => true
 -    if authenticate
 -      if @voter.vote.confirm!
 -        if @voter.election.embeddable? and params[:embed] == "true" \
 -          and @voter.election.early_results?
 -          redirect_to :action => :results, :id => @password, :embed => 'true'
 -        else
 -          render :action => 'thanks'
 -        end
++    if @voter.vote.confirm!
++      if @voter.election.embeddable? and params[:embed] == "true" \
++        and @voter.election.early_results?
++        redirect_to :action => :results, :id => @password, :embed => 'true'
++      elsif not(@voter.election.verifiable) \
++        and @voter.election.kiosk and params[:kiosk] == "true"
++        redirect_to :action => "kiosk_ready", :id => @password, :kiosk => true
+       else
 -        redirect_to :action => 'index'
++        render :action => 'thanks'
+       end
      else
-       render :action => 'thanks'
 -        redirect_to :action => 'index'
++      redirect_to :action => 'index'
      end
    end
    
    def reminder
      if params[:email]
 -      voter_array= FullVoter.find(:all, :conditions => ["email = ?", params[:email]])
 +      voter_array= FullVoter.find(:all,
 +        :conditions => ["email = ?", params[:email]])
        voter_array.delete_if {|voter| voter.election.active == 0}
        unless voter_array.empty?
          VoterNotify.deliver_reminder(voter_array)
         or @voter.election.enddate < Time.now)
        
        @election = @voter.election
 -      # compute and display results
 -
 -      @results = @election.results
 -      @candidates = {}
 -      @election.candidates.each {|c| @candidates[c.id] = c}
 -      @names = @election.names_by_id
 -        
 -      @sidebar_content = render_to_string(:partial => 'results_sidebar')
 -      #look for custom theme, and assign to instance variabels for widget use
 +      @sidebar_content = \
 +        render_to_string(:partial => 'full_results_sidebar')
 +
 +      # look for custom theme, and assign to instance variabels for
 +      # widget use
        if @election.embed_custom_string
          @top_bar = SkinPicture.find(:first,
 -        :conditions => ["filename = ?", @election.embed_custom_string + "top_bar.png"])
 +          :conditions => ["filename = ?",
 +            @election.embed_custom_string + "top_bar.png"])
          @default_image = SkinPicture.find(:first,
 -        :conditions => ["filename = ?", @election.embed_custom_string + "default_image.png"])
 +          :conditions => ["filename = ?",
 +            @election.embed_custom_string + "default_image.png"])
          @bg1 = SkinPicture.find(:first,
 -        :conditions => ["filename = ?", @election.embed_custom_string + "bg1.png"])
 +          :conditions => ["filename = ?",
 +            @election.embed_custom_string + "bg1.png"])
          @bg2 = SkinPicture.find(:first,
 -        :conditions => ["filename = ?", @election.embed_custom_string + "bg2.png"])
 +          :conditions => ["filename = ?",
 +            @election.embed_custom_string + "bg2.png"])
          @bottom_bar = SkinPicture.find(:first,
 -        :conditions => ["filename = ?", @election.embed_custom_string + "bottom_bar.png"])
 +          :conditions => ["filename = ?",
 +            @election.embed_custom_string + "bottom_bar.png"])
        end
        if @election.embeddable? and params[:embed] == "true"
          render :template => 'embed/results', :layout => 'embed'
        else
 -        render :action => 'results'
 +        render :template => 'common/results'
        end
      else
        redirect_to :action => 'index'
      end
    end
 -  
 + 
 +  def pref_tables
 +    @election = @voter.election
 +    render :template => 'common/pref_tables_wrapper', :layout => 'basic'
 +  end
 +
 +  def details
 +    @election = @voter.election
 +    render :template => 'common/details'
 +  end
 +
 +  def kiosk_ready
 +    reset_session
 +
 +    if not authenticate
 +      redirect_to :action => 'index'
 +    end
 +  end
 +
    private
    def authenticate
      password = params[:id]
      if password == "open"
        election = Election.find(params[:format])
  
 -      # double check to make sure the election is not authenticated
 -      unless election.authenticated?
 +      # if it's not actually open, lets redirect
 +      if election.authenticated
 +        redirect_to :action => 'index'
 +      
 +      # otherwise, lets see if they've before
 +      else
          @voter = OpenVoter.find(:all,
            :conditions => ["session_id = ? and election_id = ?",
                            session.session_id, election.id])[0]
  
 -        # if the election is over, proceed
 -        if (not @voter) and (election.enddate < Time.now)
 -          @voter = OpenVoter.new
 -          @voter.election = election
 +        # when (a) there is no voter or (b) when there is a voter but
 +        # it's kiosk mode on the right page, rewrite with a blank voter
 +        if ((not @voter) and  (election.enddate < Time.now)) \
 +          or (params[:action] == 'kiosk_ready' and election.kiosk)
 +          @voter = OpenVoter.new unless @voter
          end
  
 +        # now that we have a voter (one way or another), set things
 +        # right
 +        @voter.election = election
 +        @voter.session_id = session.session_id
          @password = "open." + election.id.to_s
        end
  
      else
        @voter = FullVoter.find(:all,
          :conditions => [ "password = ?", password ] )[0]
 -      @password = @voter.password
 +
 +      if @voter
 +        @password = @voter.password
 +      else
 +        redirect_to :Action => 'index'
 +      end
      end
 -    @voter
    end
  end
  
diff --combined app/models/election.rb
index 4ce571a468c4aa91a39fd6696e7a76161e706afd,7c15dcfcd785298c1e524702801d69fdfd4f3147..b48c9238c0bf39a2a79f1a044d0ec723626a4395
@@@ -2,8 -2,19 +2,8 @@@
  # Copyright (C) 2007, 2008 Benjamin Mako Hill <mako@atdot.cc>
  # Copyright (C) 2007 Massachusetts Institute of Technology
  #
 -# This program is free software: you can redistribute it and/or modify
 -# it under the terms of the GNU Affero General Public License as
 -# published by the Free Software Foundation, either version 3 of the
 -# License, or (at your option) any later version.
 -#
 -# This program is distributed in the hope that it will be useful, but
 -# WITHOUT ANY WARRANTY; without even the implied warranty of
 -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 -# Affero General Public License for more details.
 -#
 -# You should have received a copy of the GNU Affero General Public
 -# License along with this program.  If not, see
 -# <http://www.gnu.org/licenses/>.
 +# This program is free software. Please see the COPYING file for
 +# details.
  
  class Election < ActiveRecord::Base
    has_many :candidates
    has_many :votes
    belongs_to :user
    validates_presence_of :name, :description
 -  
 +
 +  # enforce constraints associated with dependencies (i.e., a kiosk
 +  # election can't also be unauthenticated)
 +  before_save :enforce_constraints
 +
    #validate that method is one of the listed election types
 -  
    attr_reader :plurality_result
    attr_reader :approval_result
    attr_reader :condorcet_result
@@@ -24,7 -32,7 +24,7 @@@
    attr_reader :borda_result
    
    require 'date'
 -  
 + 
    def initialize(params={})
      super
      self.enddate = read_attribute( :enddate ) || \
@@@ -74,7 -82,7 +74,7 @@@
  
    def activate!
      self.active = 1
 -    self.save!
 +    self.save
    end
    
    def quickvote?
    def authenticated?
      authenticated
    end
 +
 +  def kiosk?
 +    kiosk
 +  end
    
    def shortdesc
      shortdesc = description.split(/\n/)[0]
        # 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
+       plurality_tally << voter.vote.rankings.sort[0].candidate_id
        approval_tally << voter.vote.rankings.sort[0..1].collect \
-         { |ranking| ranking.candidate.id }
+         { |ranking| ranking.candidate_id }
        preference_tally << voter.vote.rankings.sort.collect \
-         { |ranking| ranking.candidate.id }
+         { |ranking| ranking.candidate_id }
      end
      
      @plurality_result = PluralityVote.new(plurality_tally).result
      
      names
    end
 +
 +  def candidate_hash
 +    hash = {}
 +    self.candidates.each {|c| hash[c.id] = c}
 +    return hash
 +  end
 +
 + 
 +  # TODO now that this code is in here, we should go ahead and remove
 +  # date checking from other places in the code
 +  def after_find
 +    if self.active < 2 and self.enddate < Time.now 
 +      self.active = 2
 +      self.save
 +    end
 +  end
 +
 +  private
 +  def enforce_constraints
 +    # kiosks can't be authenticated
 +    self.authenticated = false if kiosk?
 +    return true
 +  end
 +
  end
  
  
diff --combined app/models/vote.rb
index cc423110264b43edbefd6ff00e1ec2e9a2230ad6,719aa7b33f9cb7163049b3bc27389d49c4605251..12dd64d763e1f9fbd47d0bb776c5d118684b1b0b
@@@ -2,8 -2,19 +2,8 @@@
  # Copyright (C) 2007, 2008 Benjamin Mako Hill <mako@atdot.cc>
  # Copyright (C) 2007 Massachusetts Institute of Technology
  #
 -# This program is free software: you can redistribute it and/or modify
 -# it under the terms of the GNU Affero General Public License as
 -# published by the Free Software Foundation, either version 3 of the
 -# License, or (at your option) any later version.
 -#
 -# This program is distributed in the hope that it will be useful, but
 -# WITHOUT ANY WARRANTY; without even the implied warranty of
 -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 -# Affero General Public License for more details.
 -#
 -# You should have received a copy of the GNU Affero General Public
 -# License along with this program.  If not, see
 -# <http://www.gnu.org/licenses/>.
 +# This program is free software. Please see the COPYING file for
 +# details.
  
  class Vote < ActiveRecord::Base
    # relationships to other classes
    end
  
    def confirm!
-     self.confirmed = 1
-     self.time = Time.now
-     self.save
-     
-     unless self.voter.election.quickvote?
-       token.destroy and token.reload if token
-       self.token = Token.new
+     if self.voter.election.candidates.length == self.rankings.length
+       self.confirmed = 1
+       self.time = Time.now
        self.save
+     
+       unless self.voter.election.quickvote?
+         token.destroy and token.reload if token
+         self.token = Token.new
+         self.save
+       end
+       return false
+     else
+       return true
      end
    end
  

Benjamin Mako Hill || Want to submit a patch?