Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

using resque_mailer to background all email

some delayed jobs still need work
  • Loading branch information...
commit 47812c33ec69d42e42c3294b8b718ee10b23428d 1 parent a61357d
Sidra authored March 18, 2011

Showing 41 changed files with 628 additions and 579 deletions. Show diff stats Hide diff stats

  1. 7  Gemfile
  2. 30  Gemfile.lock
  3. 1  Rakefile
  4. 4  app/controllers/abuse_reports_controller.rb
  5. 70  app/controllers/admin/admin_users_controller.rb
  6. 6  app/controllers/challenge_assignments_controller.rb
  7. 7  app/controllers/challenge_signups_controller.rb
  8. 4  app/controllers/feedbacks_controller.rb
  9. 20  app/controllers/passwords_controller.rb
  10. 6  app/controllers/potential_matches_controller.rb
  11. 8  app/controllers/users_controller.rb
  12. 11  app/mailers/README
  13. 31  app/mailers/admin_mailer.rb
  14. 43  app/mailers/comment_mailer.rb
  15. 8  app/mailers/kudo_mailer.rb
  16. 173  app/mailers/user_mailer.rb
  17. 86  app/models/challenge_assignment.rb
  18. 7  app/models/challenge_signup.rb
  19. 169  app/models/collection.rb
  20. 67  app/models/collection_item.rb
  21. 64  app/models/comment_observer.rb
  22. 34  app/models/creation_observer.rb
  23. 52  app/models/external_author.rb
  24. 32  app/models/invitation.rb
  25. 2  app/models/kudo_observer.rb
  26. 35  app/models/potential_match.rb
  27. 33  app/models/prompt_restriction.rb
  28. 10  app/models/related_work.rb
  29. 16  app/models/user_invite_request.rb
  30. 16  app/models/work_observer.rb
  31. 10  app/views/admin_mailer/archive_notification.html.erb
  32. 4  app/views/user_mailer/archive_notification.html.erb
  33. 14  app/views/user_mailer/claim_notification.html.erb
  34. 12  app/views/user_mailer/invitation.html.erb
  35. 4  app/views/user_mailer/reset_password.html.erb
  36. 14  config.ru
  37. 9  config/initializers/gem-plugin_config/resque.rb
  38. 1  config/initializers/gem-plugin_config/resque_mailer.rb
  39. 2  config/resque.yml
  40. 82  features/admin_tasks.feature
  41. 3  features/step_definitions/generic_steps.rb
7  Gemfile
@@ -26,13 +26,14 @@ gem 'nokogiri'
26 26
 gem 'mechanize'
27 27
 gem 'sanitize'
28 28
 gem 'rest-client', :require => 'rest_client'
29  
-gem 'delayed_job', '=2.1.1'
30  
-gem 'daemons', '=1.0.10'
  29
+gem 'resque', '>=1.14.0'
  30
+gem 'resque_mailer'
  31
+# https://github.com/defunkt/resque/issues/221
  32
+gem 'sinatra', '~> 1.1.3'
31 33
 gem 'thinking-sphinx',
32 34
   :git     => 'git://github.com/freelancing-god/thinking-sphinx.git',
33 35
   :branch  => 'rails3',
34 36
   :require => 'thinking_sphinx'
35  
-gem 'ts-delayed-delta', :require => 'thinking_sphinx/deltas/delayed_delta'
36 37
 #gem 'daemon-spawn', :require => 'daemon_spawn'
37 38
 gem 'aws-s3', :require => 'aws/s3'
38 39
 # gem 'fastercsv' -- will use this eventually for exporting to excel tsv format
30  Gemfile.lock
@@ -110,11 +110,7 @@ GEM
110 110
     cucumber-rails (0.3.2)
111 111
       cucumber (>= 0.8.0)
112 112
     culerity (0.2.12)
113  
-    daemons (1.0.10)
114 113
     database_cleaner (0.6.0.rc.3)
115  
-    delayed_job (2.1.1)
116  
-      activesupport (~> 3.0)
117  
-      daemons
118 114
     diff-lcs (1.1.2)
119 115
     erubis (2.6.6)
120 116
       abstract (>= 1.0.0)
@@ -166,7 +162,7 @@ GEM
166 162
       rspec (>= 1.3)
167 163
       yard
168 164
     polyglot (0.3.1)
169  
-    rack (1.2.1)
  165
+    rack (1.2.2)
170 166
     rack-mount (0.6.13)
171 167
       rack (>= 1.0.0)
172 168
     rack-openid (1.2.0)
@@ -188,6 +184,15 @@ GEM
188 184
       rake (>= 0.8.7)
189 185
       thor (~> 0.14.4)
190 186
     rake (0.8.7)
  187
+    redis (2.1.1)
  188
+    redis-namespace (0.10.0)
  189
+      redis (< 3.0.0)
  190
+    resque (1.14.0)
  191
+      json (~> 1.4.6)
  192
+      redis-namespace (>= 0.10.0)
  193
+      sinatra (>= 0.9.2)
  194
+      vegas (~> 0.1.2)
  195
+    resque_mailer (1.0.1)
191 196
     rest-client (1.6.1)
192 197
       mime-types (>= 1.16)
193 198
     riddle (1.2.1)
@@ -224,19 +229,22 @@ GEM
224 229
       json_pure
225 230
       rubyzip
226 231
     shoulda (2.11.3)
  232
+    sinatra (1.1.3)
  233
+      rack (~> 1.1)
  234
+      tilt (>= 1.2.2, < 2.0)
227 235
     stringex (1.2.0)
228 236
     term-ansicolor (1.0.5)
229 237
     thor (0.14.6)
  238
+    tilt (1.2.2)
230 239
     treetop (1.4.9)
231 240
       polyglot (>= 0.3.1)
232  
-    ts-delayed-delta (1.1.1)
233  
-      delayed_job (>= 1.8.4)
234  
-      thinking-sphinx (>= 1.3.6)
235 241
     tzinfo (0.3.24)
236 242
     unicorn (2.0.0)
237 243
       kgio (~> 1.3.1)
238 244
       rack
239 245
     vcr (1.6.0)
  246
+    vegas (0.1.8)
  247
+      rack (>= 1.0.0)
240 248
     whenever (0.6.2)
241 249
       aaronh-chronic (>= 0.3.9)
242 250
       activesupport (>= 2.3.4)
@@ -256,9 +264,7 @@ DEPENDENCIES
256 264
   css_parser
257 265
   cucumber (>= 0.9.1)
258 266
   cucumber-rails
259  
-  daemons (= 1.0.10)
260 267
   database_cleaner (>= 0.6.0.rc.3)
261  
-  delayed_job (= 2.1.1)
262 268
   escape_utils
263 269
   factory_girl
264 270
   fakeweb
@@ -275,14 +281,16 @@ DEPENDENCIES
275 281
   pickle
276 282
   rack-openid (>= 0.2.1)
277 283
   rails (= 3.0.4)
  284
+  resque (>= 1.14.0)
  285
+  resque_mailer
278 286
   rest-client
279 287
   rspec-rails (>= 2.0.0)
280 288
   ruby-debug19
281 289
   sanitize
282 290
   shoulda
  291
+  sinatra (~> 1.1.3)
283 292
   thinking-sphinx!
284 293
   tolk!
285  
-  ts-delayed-delta
286 294
   unicorn
287 295
   vcr
288 296
   whenever (~> 0.6.2)
1  Rakefile
@@ -3,5 +3,6 @@
3 3
 
4 4
 require File.expand_path('../config/application', __FILE__)
5 5
 require 'rake'
  6
+require 'resque/tasks'
6 7
 
7 8
 Otwarchive::Application.load_tasks
4  app/controllers/abuse_reports_controller.rb
@@ -29,11 +29,11 @@ def create
29 29
           site['/projects/4603/bugs'].post build_post_info(@abuse_report), :content_type => 'application/xml', :accept => 'application/xml'
30 30
         end
31 31
         # Email bug to feedback email address
32  
-        AdminMailer.abuse_report(@abuse_report.email, @abuse_report.url, @abuse_report.comment).deliver
  32
+        AdminMailer.abuse_report(@abuse_report.id).deliver
33 33
         if params[:cc_me]
34 34
           # If user requests, and supplies email address, email them a copy of their message
35 35
           if !@abuse_report.email.blank?
36  
-            UserMailer.abuse_report(@abuse_report).deliver
  36
+            UserMailer.abuse_report(@abuse_report.id).deliver
37 37
           else
38 38
             flash[:error] = t('no_email', :default => "Sorry, we can only send you a copy of your abuse report if you enter a valid email address.")
39 39
             format.html { render :action => "new" }
70  app/controllers/admin/admin_users_controller.rb
... ...
@@ -1,5 +1,5 @@
1 1
 class Admin::AdminUsersController < ApplicationController
2  
-  
  2
+
3 3
   before_filter :admin_only
4 4
 
5 5
   def index
@@ -10,8 +10,8 @@ def index
10 10
       elsif params[:role] == "0"
11 11
         joins = :pseuds
12 12
         conditions = ['pseuds.name LIKE ? OR email = ?', "%#{params[:query]}%", params[:query]]
13  
-      elsif params[:role] == "1"  
14  
-        if !params[:query].blank?      
  13
+      elsif params[:role] == "1"
  14
+        if !params[:query].blank?
15 15
           joins = :pseuds
16 16
           conditions = [('(pseuds.name LIKE ? OR email = ?) AND activated_at IS NULL'), "%#{params[:query]}%", params[:query]]
17 17
         else
@@ -22,7 +22,7 @@ def index
22 22
           joins = [:pseuds, :roles]
23 23
           conditions = ['(pseuds.name LIKE ? OR email = ?) AND roles.name = ?', "%#{params[:query]}%", params[:query], params[:role]]
24 24
         else
25  
-          joins = :roles 
  25
+          joins = :roles
26 26
           conditions = ['roles.name = ?', params[:role]]
27 27
         end
28 28
       end
@@ -37,7 +37,7 @@ def show
37 37
     @user = User.find_by_login(params[:id])
38 38
     unless @user
39 39
       redirect_to :action => "index", :query => params[:query], :role => params[:role]
40  
-    end    
  40
+    end
41 41
     @log_items = @user.log_items.sort_by(&:created_at).reverse
42 42
   end
43 43
 
@@ -59,7 +59,7 @@ def update_user
59 59
       else
60 60
         flash[:error] = ts('There was an error updating user %{name}', :name => params[:id])
61 61
         redirect_to(request.env["HTTP_REFERER"] || root_path)
62  
-      end    
  62
+      end
63 63
     elsif params[:admin_action]
64 64
       @user = User.find_by_login(params[:user_login])
65 65
       @admin_note = params[:admin_note]
@@ -69,7 +69,7 @@ def update_user
69 69
       else
70 70
         if params[:admin_action] == 'warn'
71 71
           @user.create_log_item( options = {:action => ArchiveConfig.ACTION_WARN, :note => @admin_note, :admin_id => current_admin.id})
72  
-          flash[:notice] = ts("Warning was recorded") 
  72
+          flash[:notice] = ts("Warning was recorded")
73 73
           redirect_to(request.env["HTTP_REFERER"] || root_path)
74 74
         elsif params[:admin_action] == 'suspend'
75 75
           if params[:suspend_days].blank?
@@ -81,10 +81,10 @@ def update_user
81 81
             @user.suspended_until = @suspension_days.days.from_now
82 82
             if @user.save && @user.suspended? && !@user.suspended_until.blank?
83 83
               @user.create_log_item( options = {:action => ArchiveConfig.ACTION_SUSPEND, :note => @admin_note, :admin_id => current_admin.id, :enddate => @user.suspended_until})
84  
-              flash[:notice] = ts("User has been temporarily suspended") 
  84
+              flash[:notice] = ts("User has been temporarily suspended")
85 85
               redirect_to(request.env["HTTP_REFERER"] || root_path)
86 86
             else
87  
-              flash[:error] = ts("User could not be suspended") 
  87
+              flash[:error] = ts("User could not be suspended")
88 88
               redirect_to(request.env["HTTP_REFERER"] || root_path)
89 89
             end
90 90
           end
@@ -92,10 +92,10 @@ def update_user
92 92
           @user.banned = true
93 93
           if @user.save && @user.banned?
94 94
             @user.create_log_item( options = {:action => ArchiveConfig.ACTION_BAN, :note => @admin_note, :admin_id => current_admin.id})
95  
-            flash[:notice] = t('success_banned', :default => "User has been permanently suspended") 
  95
+            flash[:notice] = t('success_banned', :default => "User has been permanently suspended")
96 96
             redirect_to(request.env["HTTP_REFERER"] || root_path)
97  
-          else  
98  
-            flash[:error] = t('error_banned', :default => "User could not be permanently suspended") 
  97
+          else
  98
+            flash[:error] = t('error_banned', :default => "User could not be permanently suspended")
99 99
             redirect_to(request.env["HTTP_REFERER"] || root_path)
100 100
           end
101 101
         elsif params[:admin_action] == 'unsuspend'
@@ -104,14 +104,14 @@ def update_user
104 104
             @user.suspended_until = nil
105 105
             if @user.save && !@user.suspended? && @user.suspended_until.blank?
106 106
               @user.create_log_item( options = {:action => ArchiveConfig.ACTION_UNSUSPEND, :note => @admin_note, :admin_id => current_admin.id})
107  
-              flash[:notice] = t('success_unsuspend', :default => "Suspension has been lifted") 
  107
+              flash[:notice] = t('success_unsuspend', :default => "Suspension has been lifted")
108 108
               redirect_to(request.env["HTTP_REFERER"] || root_path)
109 109
             else
110  
-              flash[:error] = t('error_unsuspend', :default => "Suspension could not be lifted") 
  110
+              flash[:error] = t('error_unsuspend', :default => "Suspension could not be lifted")
111 111
               redirect_to(request.env["HTTP_REFERER"] || root_path)
112 112
             end
113 113
           else
114  
-            flash[:notice] = t('not_suspended', :default => "User had not been suspended") 
  114
+            flash[:notice] = t('not_suspended', :default => "User had not been suspended")
115 115
             redirect_to(request.env["HTTP_REFERER"] || root_path)
116 116
           end
117 117
         elsif params[:admin_action] == 'unban'
@@ -119,14 +119,14 @@ def update_user
119 119
             @user.banned = false
120 120
             if @user.save && !@user.banned?
121 121
               @user.create_log_item( options = {:action => ArchiveConfig.ACTION_UNSUSPEND, :note => @admin_note, :admin_id => current_admin.id})
122  
-              flash[:notice] = t('success_unsuspend', :default => "Suspension has been lifted") 
  122
+              flash[:notice] = t('success_unsuspend', :default => "Suspension has been lifted")
123 123
               redirect_to(request.env["HTTP_REFERER"] || root_path)
124 124
             else
125  
-              flash[:error] = t('error_unsuspend', :default => "Suspension could not be lifted") 
  125
+              flash[:error] = t('error_unsuspend', :default => "Suspension could not be lifted")
126 126
               redirect_to(request.env["HTTP_REFERER"] || root_path)
127 127
             end
128 128
           else
129  
-            flash[:notice] = t('not_banned', :default => "User had not been permanently suspended") 
  129
+            flash[:notice] = t('not_banned', :default => "User had not been permanently suspended")
130 130
             redirect_to(request.env["HTTP_REFERER"] || root_path)
131 131
           end
132 132
         end
@@ -139,9 +139,9 @@ def update_user
139 139
   def destroy
140 140
     @user = User.find_by_login(params[:id])
141 141
     @user.destroy
142  
-    redirect_to(admin_users_url) 
  142
+    redirect_to(admin_users_url)
143 143
   end
144  
-  
  144
+
145 145
   def notify
146 146
     if params[:letter] && params[:letter].is_a?(String)
147 147
       letter = params[:letter][0,1]
@@ -150,7 +150,7 @@ def notify
150 150
     end
151 151
     @all_users = User.alphabetical.starting_with(letter)
152 152
   end
153  
-  
  153
+
154 154
   def send_notification
155 155
     if !params[:notify_all].blank?
156 156
       if params[:notify_all].include?("0")
@@ -164,19 +164,19 @@ def send_notification
164 164
     elsif params[:user_ids]
165 165
       @users = User.find(params[:user_ids])
166 166
     end
167  
-        
  167
+
168 168
     if @users.nil? || @users.length == 0
169 169
       flash[:error] = ts("Who did you want to notify?")
170 170
       redirect_to :action => :notify and return
171 171
     end
172  
-    
  172
+
173 173
     unless params[:subject] && !params[:subject].blank?
174 174
       flash[:error] = ts("Please enter a subject.")
175 175
       redirect_to :action => :notify and return
176 176
     else
177 177
       @subject = params[:subject]
178 178
     end
179  
-    
  179
+
180 180
     # We need to use content because otherwise html will be stripped
181 181
     unless params[:content] && !params[:content].blank?
182 182
       flash[:error] = ts("What message did you want to send?")
@@ -184,17 +184,13 @@ def send_notification
184 184
     else
185 185
       @message = params[:content]
186 186
     end
187  
-    
  187
+
188 188
     @users.each do |user|
189  
-      if ArchiveConfig.NO_DELAYS
190  
-        UserMailer.archive_notification(current_admin.login, user, @subject, @message).deliver
191  
-      else
192  
-        UserMailer.delay.archive_notification(current_admin.login, user, @subject, @message)
193  
-      end
  189
+      UserMailer.archive_notification(current_admin.login, user.id, @subject, @message).deliver
194 190
     end
195  
-    
196  
-    AdminMailer.archive_notification(current_admin.login, @users, @subject, @message).deliver
197  
-    
  191
+
  192
+    AdminMailer.archive_notification(current_admin.login, @users.map(&:id), @subject, @message).deliver
  193
+
198 194
     flash[:notice] = ts("Notification sent to %{count} user(s).", :count => @users.size)
199 195
     redirect_to :action => :notify
200 196
   end
@@ -204,20 +200,20 @@ def activate
204 200
     @user.activate
205 201
     if @user.active?
206 202
       @user.create_log_item( options = {:action => ArchiveConfig.ACTION_ACTIVATE, :note => 'Manually Activated', :admin_id => current_admin.id})
207  
-      flash[:notice] = t('activated', :default => "User Account Activated") 
  203
+      flash[:notice] = t('activated', :default => "User Account Activated")
208 204
       redirect_to :action => :show
209 205
     else
210 206
       flash[:error] = t('activation_failed', :default => "Attempt to activate account failed.")
211 207
       redirect_to :action => :show
212 208
     end
213 209
   end
214  
-  
  210
+
215 211
   def send_activation
216 212
     @user = User.find_by_login(params[:id])
217  
-    UserMailer.signup_notification(@user).deliver
  213
+    UserMailer.signup_notification(@user.id).deliver
218 214
     flash[:notice] = t('activation_sent', :default => "Activation email sent")
219 215
     redirect_to :action => :show
220 216
   end
221 217
 
222  
-end  
  218
+end
223 219
 
6  app/controllers/challenge_assignments_controller.rb
@@ -163,11 +163,7 @@ def send_out
163 163
     # purge the potential matches! we don't want bazillions of them in our db
164 164
     PotentialMatch.clear!(@collection)
165 165
     
166  
-    if ArchiveConfig.NO_DELAYS
167  
-      ChallengeAssignment.send_out!(@collection)
168  
-    else
169  
-      ChallengeAssignment.delay.send_out!(@collection)
170  
-    end
  166
+    ChallengeAssignment.send_out!(@collection)
171 167
     flash[:notice] = "Assignments are now being sent out."
172 168
     redirect_to collection_assignments_path(@collection)
173 169
   end
7  app/controllers/challenge_signups_controller.rb
@@ -130,12 +130,7 @@ def summary
130 130
         FileUtils.touch(ChallengeSignup.summary_file(@collection))
131 131
 
132 132
         # generate the page
133  
-        if ArchiveConfig.NO_DELAYS
134  
-          ChallengeSignup.generate_summary(@collection)
135  
-        else
136  
-          # start a delayed job to generate the page
137  
-          ChallengeSignup.delay.generate_summary(@collection)
138  
-        end
  133
+        ChallengeSignup.generate_summary(@collection)
139 134
       end
140 135
     else
141 136
       # generate it on the fly
4  app/controllers/feedbacks_controller.rb
@@ -23,10 +23,10 @@ def create
23 23
           site['/projects/4911/bugs'].post build_post_info(@feedback), :content_type => 'application/xml', :accept => 'application/xml'
24 24
         end
25 25
         # Email bug to feedback email address
26  
-        AdminMailer.feedback(@feedback).deliver
  26
+        AdminMailer.feedback(@feedback.id).deliver
27 27
         # If user supplies email address, email them an auto-response
28 28
         if !@feedback.email.blank?
29  
-          UserMailer.feedback(@feedback).deliver
  29
+          UserMailer.feedback(@feedback.id).deliver
30 30
         end
31 31
         flash[:notice] = t('successfully_sent', :default => 'Your message was sent to the archive team - thank you!')
32 32
         format.html { redirect_back_or_default(root_path) }
20  app/controllers/passwords_controller.rb
... ...
@@ -1,29 +1,29 @@
1 1
 # Use for resetting lost passwords
2  
-class PasswordsController < ApplicationController      
  2
+class PasswordsController < ApplicationController
3 3
   skip_before_filter :store_location
4 4
   layout "session"
5  
-  
  5
+
6 6
   def new
7 7
   end
8  
-  
  8
+
9 9
   def create
10 10
     @user = User.find_by_login(params[:login]) || User.find_by_email(params[:login])
11 11
     if @user.nil?
12  
-      flash[:login] = '<br /><br /><div class="flash notice">'.html_safe + 
13  
-                      t('try_again', :default => "We couldn't find an account with that email address or username. Please try again?") + 
  12
+      flash[:login] = '<br /><br /><div class="flash notice">'.html_safe +
  13
+                      t('try_again', :default => "We couldn't find an account with that email address or username. Please try again?") +
14 14
                       "</div>".html_safe
15 15
       render :action => "new"
16 16
     else
17 17
       @user.reset_user_password
18 18
       @user.save
19  
-      @user_session = UserSession.find  
  19
+      @user_session = UserSession.find
20 20
       if @user_session
21  
-        @user_session.destroy 
  21
+        @user_session.destroy
22 22
       end
23  
-      UserMailer.reset_password(@user).deliver
  23
+      UserMailer.reset_password(@user.id, @user.password).deliver
24 24
       flash[:notice] = t('check_email', :default => 'Check your email for your new password.')
25 25
       redirect_to login_path
26 26
     end
27  
-  end    
28  
-  
  27
+  end
  28
+
29 29
 end
6  app/controllers/potential_matches_controller.rb
@@ -92,11 +92,7 @@ def generate
92 92
       
93 93
       flash[:notice] = ts("Beginning generation of potential matches. This may take some time, especially if your challenge is large.")
94 94
       PotentialMatch.set_up_generating(@collection)
95  
-      if ArchiveConfig.NO_DELAYS
96  
-        PotentialMatch.generate!(@collection)
97  
-      else
98  
-        PotentialMatch.delay.generate!(@collection)
99  
-      end
  95
+      PotentialMatch.generate!(@collection)
100 96
     end
101 97
 
102 98
     # redirect to index
8  app/controllers/users_controller.rb
@@ -79,8 +79,8 @@ def show
79 79
     @bookmarks = visible_bookmarks.order("updated_at DESC").limit(ArchiveConfig.NUMBER_OF_ITEMS_VISIBLE_IN_DASHBOARD)
80 80
 
81 81
     if current_user.respond_to?(:subscriptions)
82  
-      @subscription = current_user.subscriptions.where(:subscribable_id => @user.id, 
83  
-                                                       :subscribable_type => 'User').first || 
  82
+      @subscription = current_user.subscriptions.where(:subscribable_id => @user.id,
  83
+                                                       :subscribable_type => 'User').first ||
84 84
                       current_user.subscriptions.build
85 85
     end
86 86
   end
@@ -206,7 +206,7 @@ def create
206 206
         end
207 207
       end
208 208
       if @user.save
209  
-        UserMailer.signup_notification(@user).deliver
  209
+        UserMailer.signup_notification(@user.id).deliver
210 210
         flash[:notice] = ts("During testing you can activate via <a href='%{activation_url}'>your activation url</a>.",
211 211
                             :activation_url => activate_path(@user.activation_code)) if Rails.env.development?
212 212
         render "confirmation"
@@ -228,7 +228,7 @@ def activate
228 228
           flash[:error] = ts("Your account has already been activated.")
229 229
           redirect_to @user and return
230 230
         end
231  
-        @user.activate && UserMailer.activation(@user).deliver
  231
+        @user.activate && UserMailer.activation(@user.id).deliver
232 232
         flash[:notice] = ts("Signup complete! Please log in.")
233 233
         @user.create_log_item( options = {:action => ArchiveConfig.ACTION_ACTIVATE})
234 234
         # assign over any external authors that belong to this user
11  app/mailers/README
... ...
@@ -0,0 +1,11 @@
  1
+All mailers include Resque::Mailer
  2
+
  3
+This uses the resque_mailer gem to send email asyncronously by default when using deliver (puts it into a background job)
  4
+
  5
+Jobs are persisted to queues as JSON objects. Because of this all jobs must only accept arguments that can be JSON encoded.
  6
+
  7
+This is why most methods pass object IDs instead of passing around the objects.
  8
+
  9
+While this is less convenient than passing the object, it gives you a slight advantage: your jobs will be run against the most recent version of an object because they need to pull from the DB or cache. If your jobs were run against objects, they could potentially be operating on a stale record with out-of-date information.
  10
+
  11
+The only exception to this are methods which are called with deliver! (instead of deliver). These methods are not put into a queue, but are instead run immediately (synchronously). Only do this if the state of the current object is important, and the up-to-date record cannot be used. Although it's better to pass the information that may change as a string, and refactor the method to send asynchronously.
31  app/mailers/admin_mailer.rb
... ...
@@ -1,18 +1,21 @@
1 1
 class AdminMailer < ActionMailer::Base
  2
+  include Resque::Mailer # see README in this directory
2 3
 
3 4
   default :from => ArchiveConfig.RETURN_ADDRESS
4  
-    
5  
-  def abuse_report(email,url,comment)
6  
-    @email = email
7  
-    @url = url
8  
-    @comment = comment
  5
+
  6
+  def abuse_report(abuse_report_id)
  7
+    abuse_report = AbuseReport.find(abuse_report_id)
  8
+    @email = abuse_report.email
  9
+    @url = abuse_report.url
  10
+    @comment = abuse_report.comment
9 11
     mail(
10 12
       :to => ArchiveConfig.ABUSE_ADDRESS,
11 13
       :subject  => "#{ArchiveConfig.APP_NAME}" + " - " + "Admin Abuse Report"
12 14
     )
13 15
   end
14  
-  
15  
-  def feedback(feedback)
  16
+
  17
+  def feedback(feedback_id)
  18
+    feedback = Feedback.find(feedback_id)
16 19
     @summary = feedback.summary
17 20
     @comment = feedback.comment
18 21
     mail(
@@ -21,20 +24,20 @@ def feedback(feedback)
21 24
       :subject => "#{ArchiveConfig.APP_NAME}" + ": Support - " + feedback.summary,
22 25
     )
23 26
   end
24  
-  
25  
-  def archive_notification(admin, users, subject, message)
26  
-    @admin = admin
  27
+
  28
+  def archive_notification(admin_login, user_ids, subject, message)
  29
+    @admin_login = admin_login
27 30
     @subject = subject
28 31
     @message = message
29  
-    @users = if users.size < 20
30  
-      users.map(&:login).join(", ")
  32
+    @user_login_string = if user_ids.size < 20
  33
+      User.find(user_ids).map(&:login).join(", ")
31 34
     else
32  
-      users.size.to_s + " users, including: " + users[0..20].map(&:login).join(", ")
  35
+      user_ids.size.to_s + " users, including: " + User.limit(20).find(user_ids).map(&:login).join(", ")
33 36
     end
34 37
     mail(
35 38
       :to => ArchiveConfig.WEBMASTER_ADDRESS,
36 39
       :subject  => "#{ArchiveConfig.APP_NAME}" + " - " + "Admin Archive Notification Sent"
37 40
     )
38 41
   end
39  
-  
  42
+
40 43
 end
43  app/mailers/comment_mailer.rb
... ...
@@ -1,55 +1,58 @@
1 1
 class CommentMailer < ActionMailer::Base
  2
+  include Resque::Mailer # see README in this directory
2 3
 
3 4
   default :from => ArchiveConfig.RETURN_ADDRESS
4 5
 
5 6
   # Sends email to an owner of the top-level commentable when a new comment is created
6  
-  def comment_notification(user, comment)
7  
-    @comment = comment
  7
+  def comment_notification(user_id, comment_id)
  8
+    user = User.find(user_id)
  9
+    @comment = Comment.find(comment_id)
8 10
     mail(
9 11
       :to => user.email,
10  
-      :subject => "[#{ArchiveConfig.APP_NAME}] Comment on " + (@comment.ultimate_parent.is_a?(Tag) ? "the tag " : "") + comment.ultimate_parent.commentable_name
  12
+      :subject => "[#{ArchiveConfig.APP_NAME}] Comment on " + (@comment.ultimate_parent.is_a?(Tag) ? "the tag " : "") + @comment.ultimate_parent.commentable_name
11 13
     )
12 14
   end
13 15
 
14 16
   # Sends email to an owner of the top-level commentable when a comment is edited
15  
-  def edited_comment_notification(user, comment)
16  
-    @comment = comment
  17
+  def edited_comment_notification(user_id, comment_id)
  18
+    user = User.find(user_id)
  19
+    @comment = Comment.find(comment_id)
17 20
     mail(
18 21
       :to => user.email,
19  
-      :subject => "[#{ArchiveConfig.APP_NAME}] Edited comment on " + (@comment.ultimate_parent.is_a?(Tag) ? "the tag " : "") + comment.ultimate_parent.commentable_name
  22
+      :subject => "[#{ArchiveConfig.APP_NAME}] Edited comment on " + (@comment.ultimate_parent.is_a?(Tag) ? "the tag " : "") + @comment.ultimate_parent.commentable_name
20 23
     )
21 24
   end
22 25
 
23 26
   # Sends email to commenter when a reply is posted to their comment
24 27
   # This may be a non-user of the archive
25  
-  def comment_reply_notification(your_comment, comment)
26  
-    @your_comment = your_comment
27  
-    @comment = comment
  28
+  def comment_reply_notification(your_comment_id, comment_id)
  29
+    @your_comment = Comment.find(your_comment_id)
  30
+    @comment = Comment.find(comment_id)
28 31
     mail(
29  
-      :to => your_comment.comment_owner_email,
  32
+      :to => @your_comment.comment_owner_email,
30 33
       :subject => "[#{ArchiveConfig.APP_NAME}] Reply to your comment on " + (@comment.ultimate_parent.is_a?(Tag) ? "the tag " : "") + @comment.ultimate_parent.commentable_name
31 34
     )
32 35
   end
33  
-   
  36
+
34 37
   # Sends email to commenter when a reply to their comment is edited
35 38
   # This may be a non-user of the archive
36  
-  def edited_comment_reply_notification(your_comment, edited_comment)
37  
-    @your_comment = your_comment
38  
-    @comment = edited_comment
  39
+  def edited_comment_reply_notification(your_comment_id, edited_comment_id)
  40
+    @your_comment = Comment.find(your_comment_id)
  41
+    @comment = Comment.find(edited_comment_id)
39 42
     mail(
40  
-      :to => your_comment.comment_owner_email,
  43
+      :to => @your_comment.comment_owner_email,
41 44
       :subject => "[#{ArchiveConfig.APP_NAME}] Edited reply to your comment on " + (@comment.ultimate_parent.is_a?(Tag) ? "the tag " : "") + @comment.ultimate_parent.commentable_name
42 45
     )
43 46
   end
44 47
 
45  
-  # Sends email to the poster of a comment 
46  
-  def comment_sent_notification(comment)
47  
-    @comment = comment
  48
+  # Sends email to the poster of a comment
  49
+  def comment_sent_notification(comment_id)
  50
+    @comment = Comment.find(comment_id)
48 51
     @noreply = true # don't give reply link to your own comment
49 52
     mail(
50  
-      :to => comment.comment_owner_email,
  53
+      :to => @comment.comment_owner_email,
51 54
       :subject => "[#{ArchiveConfig.APP_NAME}] Comment you left on " + (@comment.ultimate_parent.is_a?(Tag) ? "the tag " : "") + @comment.ultimate_parent.commentable_name
52 55
     )
53 56
   end
54  
-   
  57
+
55 58
 end
8  app/mailers/kudo_mailer.rb
... ...
@@ -1,14 +1,16 @@
1 1
 class KudoMailer < ActionMailer::Base
  2
+  include Resque::Mailer # see README in this directory
2 3
 
3 4
   default :from => ArchiveConfig.RETURN_ADDRESS
4 5
 
5  
-  def kudo_notification(user, kudo)
6  
-    @kudo = kudo
  6
+  def kudo_notification(user_id, kudo_id)
  7
+    user = User.find(user_id)
  8
+    kudo = Kudo.find(kudo_id)
7 9
     @pseud = kudo.pseud
8 10
     @commentable = kudo.commentable
9 11
     mail(
10 12
       :to => user.email,
11  
-      :subject => "[#{ArchiveConfig.APP_NAME}] Kudos on " + kudo.commentable.commentable_name
  13
+      :subject => "[#{ArchiveConfig.APP_NAME}] Kudos on " + @commentable.commentable_name
12 14
     )
13 15
   end
14 16
 
173  app/mailers/user_mailer.rb
... ...
@@ -1,4 +1,5 @@
1 1
 class UserMailer < ActionMailer::Base
  2
+  include Resque::Mailer # see README in this directory
2 3
 
3 4
   helper :application
4 5
   helper :tags
@@ -6,63 +7,70 @@ class UserMailer < ActionMailer::Base
6 7
   include HtmlCleaner
7 8
 
8 9
   default :from => ArchiveConfig.RETURN_ADDRESS
9  
-  
  10
+
10 11
   # Sends an invitation to join the archive
  12
+  # Must be sent synchronously as it is rescued
  13
+  # TODO refactor to make it asynchronous
11 14
   def invitation(invitation)
12 15
     @invitation = invitation
13  
-    @user_name = (invitation.creator.is_a?(User) ? invitation.creator.login : '')
  16
+    @user_name = (@invitation.creator.is_a?(User) ? @invitation.creator.login : '')
14 17
     mail(
15  
-      :to => invitation.invitee_email,
  18
+      :to => @invitation.invitee_email,
16 19
       :subject => "[#{ArchiveConfig.APP_NAME}] Invitation"
17 20
     )
18 21
   end
19  
-  
  22
+
20 23
   # Sends an invitation to join the archive and claim stories that have been imported as part of a bulk import
21  
-  def invitation_to_claim(invitation, archivist)
  24
+  # Must be sent synchronously as it is rescued
  25
+  # TODO refactor to make it asynchronous
  26
+  def invitation_to_claim(invitation, archivist_login)
22 27
     @external_author = invitation.external_author
23  
-    @archivist = archivist || "An archivist"
  28
+    @archivist = archivist_login || "An archivist"
24 29
     @token = invitation.token
25 30
     mail(
26 31
       :to => invitation.invitee_email,
27 32
       :subject => "[#{ArchiveConfig.APP_NAME}] Invitation To Claim Stories"
28 33
     )
29 34
   end
30  
-  
  35
+
31 36
   # Notifies a writer that their imported works have been claimed
32  
-  def claim_notification(external_author, claimed_works)
33  
-    @email = external_author.email
34  
-    @claimed_works = claimed_works
  37
+  def claim_notification(external_author_id, claimed_work_ids)
  38
+    external_author = ExternalAuthor.find(external_author_id)
  39
+    @external_email = external_author.email
  40
+    @claimed_works = Work.find(claimed_work_ids)
35 41
     mail(
36 42
       :to => external_author.user.email,
37 43
       :subject => "[#{ArchiveConfig.APP_NAME}] Stories Uploaded"
38 44
     )
39 45
   end
40  
-  
41  
-  def subscription_notification(user, subscription, creation)
42  
-    @subscription = subscription
43  
-    @creation = creation
  46
+
  47
+  def subscription_notification(user_id, subscription_id, creation_id, creation_class_name)
  48
+    user = User.find(user_id)
  49
+    @subscription = Subscription.find(subscription_id)
  50
+    @creation = creation_class_name.constantize.find(creation_id)
44 51
     mail(
45 52
       :to => user.email,
46  
-      :subject => "[#{ArchiveConfig.APP_NAME}] Subscription Notice for #{@subscription.name}"      
  53
+      :subject => "[#{ArchiveConfig.APP_NAME}] Subscription Notice for #{@subscription.name}"
47 54
     )
48 55
   end
49 56
 
50 57
   # Emails a user to say they have been given more invitations for their friends
51  
-  def invite_increase_notification(user, total)
52  
-    @user = user
53  
-    @total = total 
  58
+  def invite_increase_notification(user_id, total)
  59
+    @user = User.find(user_id)
  60
+    @total = total
54 61
     mail(
55  
-      :to => user.email,
  62
+      :to => @user.email,
56 63
       :subject => "[#{ArchiveConfig.APP_NAME}] New Invitations"
57 64
     )
58 65
   end
59 66
 
60 67
   # Sends an admin message to a user
61  
-  def archive_notification(admin, user, subject, message)
  68
+  def archive_notification(admin_login, user_id, subject, message)
  69
+    @user = User.find(user_id)
62 70
     @message = message
63  
-    @admin = admin
  71
+    @admin_login = admin_login
64 72
     mail(
65  
-      :to => user.email,
  73
+      :to => @user.email,
66 74
       :subject => "[#{ArchiveConfig.APP_NAME}] Admin Message #{subject}"
67 75
     )
68 76
   end
@@ -73,106 +81,113 @@ def mass_archive_notification(admin, users, subject, message)
73 81
       archive_notification(admin, user, subject, message)
74 82
     end
75 83
   end
76  
-  
77  
-  def collection_notification(collection, subject, message)
  84
+
  85
+  def collection_notification(collection_id, subject, message)
78 86
     @message = message
79  
-    @collection = collection
  87
+    @collection = Collection.find(collection_id)
80 88
     mail(
81  
-      :to => collection.get_maintainers_email,
82  
-      :subject => "[#{ArchiveConfig.APP_NAME}][#{collection.title}] #{subject}"
  89
+      :to => @collection.get_maintainers_email,
  90
+      :subject => "[#{ArchiveConfig.APP_NAME}][#{@collection.title}] #{subject}"
83 91
     )
84 92
   end
85 93
 
86  
-  def potential_match_generation_notification(collection)
87  
-    @collection = collection
  94
+  def potential_match_generation_notification(collection_id)
  95
+    @collection = Collection.find(collection_id)
88 96
     mail(
89  
-      :to => collection.get_maintainers_email,
90  
-      :subject => "[#{ArchiveConfig.APP_NAME}][#{collection.title}] Potential Match Generation Complete"
  97
+      :to => @collection.get_maintainers_email,
  98
+      :subject => "[#{ArchiveConfig.APP_NAME}][#{@collection.title}] Potential Match Generation Complete"
91 99
     )
92 100
   end
93 101
 
94  
-  def challenge_assignment_notification(collection, assigned_user, assignment)
95  
-    @collection = collection
96  
-    @assigned_user = assigned_user
  102
+  def challenge_assignment_notification(collection_id, assigned_user_id, assignment_id)
  103
+    @collection = Collection.find(collection_id)
  104
+    @assigned_user = User.find(assigned_user_id)
  105
+    assignment = ChallengeAssignment.find(assignment_id)
97 106
     @request = (assignment.request_signup || assignment.pinch_request_signup)
98 107
     mail(
99  
-      :to => assigned_user.email,
100  
-      :subject => "[#{ArchiveConfig.APP_NAME}][#{collection.title}] Your Assignment!"
  108
+      :to => @assigned_user.email,
  109
+      :subject => "[#{ArchiveConfig.APP_NAME}][#{@collection.title}] Your Assignment!"
101 110
     )
102 111
   end
103 112
 
104 113
   # Asks a user to validate and activate their new account
105  
-  def signup_notification(user)
106  
-    @user = user
  114
+  def signup_notification(user_id)
  115
+    @user = User.find(user_id)
107 116
     mail(
108  
-      :to => user.email,
  117
+      :to => @user.email,
109 118
       :subject => "[#{ArchiveConfig.APP_NAME}] Please activate your new account"
110 119
     )
111 120
   end
112  
-   
  121
+
113 122
   # Emails a user to confirm that their account is validated and activated
114  
-  def activation(user)
115  
-    @user = user
  123
+  def activation(user_id)
  124
+    @user = User.find(user_id)
116 125
     mail(
117  
-      :to => user.email,
  126
+      :to => @user.email,
118 127
       :subject => "[#{ArchiveConfig.APP_NAME}] Your account has been activated."
119 128
     )
120  
-  end 
121  
-  
  129
+  end
  130
+
122 131
   # Confirms to a user that their password was reset
123  
-  def reset_password(user)
124  
-    @user = user
  132
+  # NOTE: the password is not saved in the database, it's a virtual method in authlogic, so it must be passed
  133
+  def reset_password(user_id, password)
  134
+    @user = User.find(user_id)
  135
+    @password = password
125 136
     mail(
126  
-      :to => user.email,
  137
+      :to => @user.email,
127 138
       :subject => "[#{ArchiveConfig.APP_NAME}] Password reset"
128 139
     )
129 140
   end
130  
-   
  141
+
131 142
   ### WORKS NOTIFICATIONS ###
132  
-  
  143
+
133 144
   # Sends email when a user is added as a co-author
134  
-  def coauthor_notification(user, creation)
135  
-    @user = user
136  
-    @creation = creation
  145
+  def coauthor_notification(user_id, creation_id, creation_class_name)
  146
+    @user = User.find(user_id)
  147
+    @creation = creation_class_name.constantize.find(creation_id)
137 148
     mail(
138  
-      :to => user.email,
  149
+      :to => @user.email,
139 150
       :subject => "[#{ArchiveConfig.APP_NAME}] Co-Author Notification"
140 151
     )
141  
-  end 
142  
-   
  152
+  end
  153
+
143 154
   # Sends emails to authors whose stories were listed as the inspiration of another work
144  
-  def related_work_notification(user, related_work)
145  
-    @user = user
146  
-    @related_work = related_work
  155
+  def related_work_notification(user_id, related_work_id)
  156
+    @user = User.find(user_id)
  157
+    @related_work = RelatedWork.find(related_work_id)
147 158
     @related_parent_link = url_for(:controller => :works, :action => :show, :id => @related_work.parent)
148 159
     @related_child_link = url_for(:controller => :works, :action => :show, :id => @related_work.work)
149 160
     mail(
150  
-      :to => user.email,
  161
+      :to => @user.email,
151 162
       :subject => "[#{ArchiveConfig.APP_NAME}] Related work notification"
152 163
     )
153 164
   end
154 165
 
155 166
   # Emails a recipient to say that a gift has been posted for them
156  
-  def recipient_notification(user, work, collection=nil)
157  
-    @work = work
158  
-    @collection = collection
  167
+  def recipient_notification(user_id, work_id, collection_id=nil)
  168
+    user = User.find(user_id)
  169
+    @work = Work.find(work_id)
  170
+    @collection = Collection.find(collection_id) if collection_id
159 171
     mail(
160 172
       :to => user.email,
161  
-      :subject => "[#{ArchiveConfig.APP_NAME}]#{collection ? '[' + collection.title + ']' : ''} A Gift Story For You #{collection ? 'From ' + collection.title : ''}"
  173
+      :subject => "[#{ArchiveConfig.APP_NAME}]#{@collection ? '[' + @collection.title + ']' : ''} A Gift Story For You #{@collection ? 'From ' + @collection.title : ''}"
162 174
     )
163 175
   end
164  
-  
  176
+
165 177
   # Emails a prompter to say that a response has been posted to their prompt
166  
-  def prompter_notification(user, work, collection=nil)
167  
-    @work = work
168  
-    @collection = collection
  178
+  def prompter_notification(user_id, work_id, collection_id=nil)
  179
+    user = User.find(user_id)
  180
+    @work = Work.find(work_id)
  181
+    @collection = Collection.find(collection_id) if collection_id
169 182
     mail(
170 183
       :to => user.email,
171  
-      :subject => "[#{ArchiveConfig.APP_NAME}]#{collection ? '[' + collection.title + ']' : ''} A Response to your Prompt #{collection ? 'From ' + collection.title : ''}"
  184
+      :subject => "[#{ArchiveConfig.APP_NAME}] A Response to your Prompt"
172 185
     )
173 186
   end
174  
-   
  187
+
175 188
   # Sends email to coauthors when a work is edited
  189
+  # NOTE: this must be sent synchronously! otherwise the new version will be sent.
  190
+  # TODO refactor to make it asynchronous by passing the content in the method
176 191
   def edit_work_notification(user, work)
177 192
     @user = user
178 193
     @work = work
@@ -181,8 +196,10 @@ def edit_work_notification(user, work)
181 196
       :subject => "[#{ArchiveConfig.APP_NAME}] Your story has been updated"
182 197
     )
183 198
   end
184  
-   
  199
+
185 200
   # Sends email to authors when a creation is deleted
  201
+  # NOTE: this must be sent synchronously! otherwise the work will no longer be there to send
  202
+  # TODO refactor to make it asynchronous by passing the content in the method
186 203
   def delete_work_notification(user, work)
187 204
     @user = user
188 205
     @work = work
@@ -196,11 +213,12 @@ def delete_work_notification(user, work)
196 213
       :subject => "[#{ArchiveConfig.APP_NAME}] Your story has been deleted"
197 214
     )
198 215
   end
199  
-  
  216
+
200 217
   ### OTHER NOTIFICATIONS ###
201  
-  
  218
+
202 219
   # archive feedback
203  
-  def feedback(feedback)
  220
+  def feedback(feedback_id)
  221
+    feedback = Feedback.find(feedback_id)
204 222
     return unless feedback.email
205 223
     @summary = feedback.summary
206 224
     @comment = feedback.comment
@@ -208,9 +226,10 @@ def feedback(feedback)
208 226
       :to => feedback.email,
209 227
       :subject => "#{ArchiveConfig.APP_NAME}: Support - #{strip_html_breaks_simple(feedback.summary)}"
210 228
     )
211  
-  end  
  229
+  end
212 230
 
213  
-  def abuse_report(report)
  231
+  def abuse_report(report_id)
  232
+    report = AbuseReport.find(report_id)
214 233
     setup_email_without_name(report.email)
215 234
     @url = report.url
216 235
     @comment = report.comment
@@ -240,7 +259,7 @@ def generate_attachment_content_from_work(work)
240 259
     end
241 260
     return attachment_string
242 261
   end
243  
-  
  262
+
244 263
   protected
245 264
 
246 265
 end
86  app/models/challenge_assignment.rb
... ...
@@ -1,5 +1,5 @@
1 1
 class ChallengeAssignment < ActiveRecord::Base
2  
-  # We use "-1" to represent all the requested items matching 
  2
+  # We use "-1" to represent all the requested items matching
3 3
   ALL = -1
4 4
 
5 5
   belongs_to :collection
@@ -30,44 +30,44 @@ class ChallengeAssignment < ActiveRecord::Base
30 30
   }
31 31
 
32 32
   scope :in_collection, lambda {|collection| where('challenge_assignments.collection_id = ?', collection.id) }
33  
-  
  33
+
34 34
   scope :defaulted, where("defaulted_at IS NOT NULL")
35 35
   scope :undefaulted, where("defaulted_at IS NULL")
36 36
   scope :uncovered, where("covered_at IS NULL")
37 37
   scope :covered, where("covered_at IS NOT NULL")
38  
-  
  38
+
39 39
   scope :with_offer, where("offer_signup_id IS NOT NULL")
40 40
   scope :with_request, where("request_signup_id IS NOT NULL")
41 41
   scope :with_no_request, where("request_signup_id IS NULL")
42 42
   scope :with_no_offer, where("offer_signup_id IS NULL")
43  
-  scope :unposted, where("challenge_assignments.creation_id IS NULL")  
  43
+  scope :unposted, where("challenge_assignments.creation_id IS NULL")
44 44
 
45  
-  REQUESTING_PSEUD_JOIN = "INNER JOIN challenge_signups ON (challenge_assignments.request_signup_id = challenge_signups.id 
  45
+  REQUESTING_PSEUD_JOIN = "INNER JOIN challenge_signups ON (challenge_assignments.request_signup_id = challenge_signups.id
46 46
                                                             OR challenge_assignments.pinch_request_signup_id = challenge_signups.id)
47 47
                            INNER JOIN pseuds ON challenge_signups.pseud_id = pseuds.id"
48 48
 
49  
-  OFFERING_PSEUD_JOIN = "INNER JOIN challenge_signups ON challenge_assignments.offer_signup_id = challenge_signups.id 
  49
+  OFFERING_PSEUD_JOIN = "INNER JOIN challenge_signups ON challenge_assignments.offer_signup_id = challenge_signups.id
50 50
                          INNER JOIN pseuds ON (challenge_assignments.pinch_hitter_id = pseuds.id OR challenge_signups.pseud_id = pseuds.id)"
51 51
 
52  
-  COLLECTION_ITEMS_JOIN = "INNER JOIN collection_items ON (collection_items.collection_id = challenge_assignments.collection_id AND 
53  
-                                                           collection_items.item_id = challenge_assignments.creation_id AND 
  52
+  COLLECTION_ITEMS_JOIN = "INNER JOIN collection_items ON (collection_items.collection_id = challenge_assignments.collection_id AND
  53
+                                                           collection_items.item_id = challenge_assignments.creation_id AND
54 54
                                                            collection_items.item_type = challenge_assignments.creation_type)"
55 55
 
56  
-  COLLECTION_ITEMS_LEFT_JOIN =  "LEFT JOIN collection_items ON (collection_items.collection_id = challenge_assignments.collection_id AND 
57  
-                                                                collection_items.item_id = challenge_assignments.creation_id AND 
  56
+  COLLECTION_ITEMS_LEFT_JOIN =  "LEFT JOIN collection_items ON (collection_items.collection_id = challenge_assignments.collection_id AND
  57
+                                                                collection_items.item_id = challenge_assignments.creation_id AND
58 58
                                                                 collection_items.item_type = challenge_assignments.creation_type)"
59 59
 
60  
-  scope :order_by_requesting_pseud, joins(REQUESTING_PSEUD_JOIN).order("pseuds.name") 
61  
-  
  60
+  scope :order_by_requesting_pseud, joins(REQUESTING_PSEUD_JOIN).order("pseuds.name")
  61
+
62 62
   scope :order_by_offering_pseud, joins(OFFERING_PSEUD_JOIN).order("pseuds.name")
63 63
 
64  
-  scope :fulfilled, 
  64
+  scope :fulfilled,
65 65
     joins(COLLECTION_ITEMS_JOIN).
66  
-    where('challenge_assignments.creation_id IS NOT NULL AND collection_items.user_approval_status = ? AND collection_items.collection_approval_status = ?', 
  66
+    where('challenge_assignments.creation_id IS NOT NULL AND collection_items.user_approval_status = ? AND collection_items.collection_approval_status = ?',
67 67
                     CollectionItem::APPROVED, CollectionItem::APPROVED)
68  
-  
  68
+
69 69
   # has to be a left join to get works that don't have a collection item
70  
-  scope :unfulfilled, 
  70
+  scope :unfulfilled,
71 71
     joins(COLLECTION_ITEMS_LEFT_JOIN).
72 72
     where('challenge_assignments.creation_id IS NULL OR collection_items.user_approval_status != ? OR collection_items.collection_approval_status != ?', CollectionItem::APPROVED, CollectionItem::APPROVED)
73 73
 
@@ -83,12 +83,12 @@ def clear_assignment
83 83
       request_signup.save
84 84
     end
85 85
   end
86  
-  
  86
+
87 87
   def get_collection_item
88 88
     return nil unless self.creation
89 89
     CollectionItem.where("collection_id = ? AND item_id = ? AND item_type = ?", self.collection_id, self.creation_id, self.creation_type).first
90 90
   end
91  
-  
  91
+
92 92
   def fulfilled?
93 93
     self.creation && (item = get_collection_item) && item.approved?
94 94
   end
@@ -100,15 +100,15 @@ def defaulted=(value)
100 100
       self.defaulted_at = nil
101 101
     end
102 102
   end
103  
-      
  103
+
104 104
   def defaulted
105 105
     !self.defaulted_at.nil?
106 106
   end
107 107
 
108 108
   include Comparable
109  
-  # sort in order that puts assignments with no request ahead of assignments with no offer, 
110  
-  # ahead of assignments with both request and offer, and within each group sorts by 
111  
-  # request byline and then offer byline. 
  109
+  # sort in order that puts assignments with no request ahead of assignments with no offer,
  110
+  # ahead of assignments with both request and offer, and within each group sorts by
  111
+  # request byline and then offer byline.
112 112
   def <=>(other)
113 113
     return -1 if self.request_signup.nil? && other.request_signup
114 114
     return 1 if other.request_signup.nil? && self.request_signup
@@ -118,27 +118,27 @@ def <=>(other)
118 118
     return cmp if cmp != 0
119 119
     self.offer_byline.downcase <=> other.offer_byline.downcase
120 120
   end
121  
-  
  121
+
122 122
   def title
123 123
     "#{self.collection.title} (#{self.request_byline})"
124 124
   end
125  
-  
  125
+
126 126
   def offering_user
127 127
     offering_pseud ? offering_pseud.user : nil
128 128
   end
129  
-  
  129
+
130 130
   def offering_pseud
131 131
     offer_signup ? offer_signup.pseud : pinch_hitter
132 132
   end
133  
-  
  133
+
134 134
   def requesting_pseud