merged in from code from the other master
[selectricity-live] / app / models / election.rb
1 # Selectricity: Voting Machinery for the Masses
2 # Copyright (C) 2007, 2008 Benjamin Mako Hill <mako@atdot.cc>
3 # Copyright (C) 2007 Massachusetts Institute of Technology
4 #
5 # This program is free software. Please see the COPYING file for
6 # details.
7
8 class Election < ActiveRecord::Base
9   has_many :candidates
10   has_many :voters
11   has_many :votes
12   belongs_to :user
13   validates_presence_of :name, :description
14
15   # enforce constraints associated with dependencies (i.e., a kiosk
16   # election can't also be unauthenticated)
17   before_save :enforce_constraints
18
19   #validate that method is one of the listed election types
20   attr_reader :plurality_result
21   attr_reader :approval_result
22   attr_reader :condorcet_result
23   attr_reader :ssd_result
24   attr_reader :borda_result
25   
26   require 'date'
27  
28   def initialize(params={})
29     super
30     self.enddate = read_attribute( :enddate ) || \
31                    Time.now + 30.days - 1.second
32   end
33
34   def other_methods
35     if election_method
36       @other_methods = ELECTION_TYPES.keys.reject {|i| i == election_method}
37     else
38       @other_methods = nil
39     end
40     @other_methods
41   end
42
43   def startdate
44     read_attribute( :startdate ) || Time.now
45   end
46
47   def votes
48     votes = Array.new
49     self.voters.each do |voter|
50       votes << voter.vote
51     end
52     return votes
53   end
54
55   def destroy
56     self.candidates.each do |candidate|
57       candidate.destroy
58     end
59     super
60   end
61
62   def start_blockers
63     reasons = []
64     if self.candidates.length <= 1
65       reasons << "You must have at least two candidates."
66     end
67     
68     if self.voters.length <= 1 and self.authenticated?
69       reasons << "You must have at least two voters."
70     end
71
72     reasons
73   end
74
75   def activate!
76     self.active = 1
77     self.save
78   end
79   
80   def quickvote?
81     self.class == QuickVote
82   end
83
84   def active?
85     active == 1
86   end 
87
88   def done?
89     active == 2
90   end
91
92   def authenticated?
93     authenticated
94   end
95
96   def kiosk?
97     kiosk
98   end
99   
100   def shortdesc
101     shortdesc = description.split(/\n/)[0]
102   end
103
104   def longdesc
105     longdesc = description.split(/\n/)[1..-1].join("")
106     longdesc.length > 0 ? longdesc : nil 
107   end
108   
109   #Calculate results if not in memcache
110   def results
111     # Assignment is intentional
112     if Cache and c = Cache.get("election_results:#{id}:#{self.votes.length}")
113       @plurality_result = c['plurality']
114       @approval_result = c['approval']
115       @condorcet_result = c['condorcet']
116       @ssd_result = c['ssd']
117       @borda_result = c['borda']
118       return c
119     elsif Cache
120       # memcache is available, but missed.
121       results = self.results!
122       Cache.set("election_results:#{id}:#{self.votes.length}", results)
123       return results
124     else
125       return self.results!
126     end
127   end
128
129   #Always Calculate Election Results
130   def results!
131     # initalize the tallies to empty arrays
132     preference_tally = Array.new
133     plurality_tally = Array.new
134     approval_tally = Array.new
135
136     self.voters.each do |voter|
137       # skip if the voter has not voted or has an unconfirmed vote
138       next unless voter.voted?
139
140       plurality_tally << voter.vote.rankings.sort[0].candidate.id
141       approval_tally << voter.vote.rankings.sort[0..1].collect \
142         { |ranking| ranking.candidate.id }
143       preference_tally << voter.vote.rankings.sort.collect \
144         { |ranking| ranking.candidate.id }
145     end
146     
147     @plurality_result = PluralityVote.new(plurality_tally).result
148     @approval_result = ApprovalVote.new(approval_tally).result
149     @condorcet_result = PureCondorcetVote.new(preference_tally).result
150     @ssd_result = CloneproofSSDVote.new(preference_tally).result
151     @borda_result = BordaVote.new(preference_tally).result
152     
153     { 'plurality' => @plurality_result,
154       'approval' => @approval_result,
155       'condorcet' => @condorcet_result,
156       'ssd' => @ssd_result,
157       'borda' => @borda_result }
158     end
159   
160   def names_by_id
161     names = Hash.new
162     
163     competitors = self.candidates.sort.collect {|candidate| candidate.id}
164     competitors.each do |candidate|
165       names[candidate] = Candidate.find(candidate).name
166     end
167     
168     names
169   end
170
171   def candidate_hash
172     hash = {}
173     self.candidates.each {|c| hash[c.id] = c}
174     return hash
175   end
176
177  
178   # TODO now that this code is in here, we should go ahead and remove
179   # date checking from other places in the code
180   def after_find
181     if self.active < 2 and self.enddate < Time.now 
182       self.active = 2
183       self.save
184     end
185   end
186
187   private
188   def enforce_constraints
189     # kiosks can't be authenticated
190     self.authenticated = false if kiosk?
191     return true
192   end
193
194 end
195
196

Benjamin Mako Hill || Want to submit a patch?