The users can vote up submissions by clicking the up arrow next to the submissions:
The way this usually works is through an ajax request that posts the vote to the server. The Rails view that handles that reads like so:
1 - if current_user.nil? or current_user.can_vote_for(submission) 2 = link_to vote_up_path(:id => submission.id), :method => :post, :remote => true, :class => "vote-up", :id => "voting-booth-#{submission.id}", :title => 'vote up' do 3 != icon 'circle-arrow-n' 4 5 %span 6 %span{:class => 'score-bracket'}= "[" 7 %span{:class => 'score', :id => "score-for-submission-#{submission.id}"}>= submission.score.to_i 8 %span{:class => 'score-bracket'}= "]" 9 10 :javascript 11 $(document).ready(function(){ 12 $('#voting-booth-#{submission.id}').bind('ajax:success', function(obj, data){ 13 data = eval("(" + data + ")"); 14 $('#score-for-submission-' + data['id']).text(data['score']); 15 $('#voting-booth-' + data['id']).addClass('invisible'); 16 }); 17 });
Everything works fine with this setup, except when the user is not signed in. In that case, we could hide the up arrow but that's not very user-friendly. We really want to show things like that so the users understand what they are allowed or expected to do.
Usually, when a user that is not registered clicks a link that would require him to be registered; the server will remember that link, show a sign in page, and then redirect to that link. We cannot do that in our case because the link is a post. Further, it's an ajax post so the rendered content makes no sense outside of its usual page.
So the solution we came up with is to remember the current page as the user clicks the arrow and also to remember the post request -- both pieces of information are stored in the session. Then, after the user signs in or signs up, we artificially execute the post before redirecting to the original page -- the one where they triggered the post request.
The first thing we have to do is remember the post request when the user is not signed in. We do that by adding the following to our application controller:
1 class ApplicationController < ActionController::Base 2 3 # this before filter will save an attempted post request for later execution after the user is authenticated 4 def save_post_before_authenticating 5 if request.env['warden'].unauthenticated? and request.post? 6 logger.info "Will save the POST request for later execution" 7 session[:pre_sign_in_post] = {:controller => controller_name, :action => action_name, :params => params.dup} 8 end 9 10 authenticate_user! 11 end 12 13 end
Here we are using warden, the underlying library behind the devise gem.
The save_post_before_authenticating method is added as a before filter to the vote_up action:
before_filter :save_post_before_authenticating, :only => [:vote_up]
Next, we have to handle the 401 response sent back from the server when a vote takes place for a user that is not signed in. The javascript code reads like that:
$(document).ready(function(){
$(document.body).bind('ajax:error', function(status, xhr, err){
if(xhr.status == 401){
window.location = '/sign_in_then_redirect?current_url=' + escape(window.location)
}
});
});
The URL we are now reaching out to (sign_in_then_redirect) will remember the current page and redirect to the sign in page:
1 class ApplicationController < ActionController::Base 2 3 def sign_in_then_redirect 4 flash[:alert] = "Please sign in first." 5 session[:last_get_url] = params[:current_url] 6 redirect_to new_session_url(:user) 7 end 8 9 end
At this point, the user will sign in and we will need to execute the saved post request and redirect them to their previous location. We have all the information we need but artificially executing the post request is tricky. We need to hack together a few things. First off, the after_sign_in_path_for is a method from the devise gem that is executed when trying to figure out where to redirect the users after they sign in. So we hook into that method and do some request processing. We instantiate the controller that would have been created by the post request and we call its action method. Before we call the action method though, we have to make sure that params and request actually point to the old post params and request.
As a bonus, we replace the flash notice with whatever is in flash[:pre_sign_in_notice] so the vote up action can reassure the user that their vote was taken into account.
The code for these two methods is as follows:
1 class ApplicationController < ActionController::Base 2 3 def after_sign_in_path_for resource_or_scope 4 last_get_url = session[:last_get_url] 5 if last_get_url.nil? 6 return super 7 end 8 9 # if a post request was saved in the session then we execute it -- see #save_post_before_authenticating 10 execute_saved_post_request if session[:pre_sign_in_post] 11 12 last_get_url 13 end 14 15 def execute_saved_post_request 16 logger.info "Executing the saved POST request before redirecting" 17 18 begin 19 class_name = session[:pre_sign_in_post][:controller].capitalize + "Controller" 20 controller_instance = class_name.constantize.new 21 22 current_request = request 23 saved_params = session[:pre_sign_in_post][:params] 24 metaclass = (class << controller_instance; self; end) 25 26 metaclass.send(:define_method, :request) { return current_request } 27 metaclass.send(:define_method, :params) { return saved_params } 28 29 controller_instance.send(session[:pre_sign_in_post][:action]) 30 flash[:notice] = flash[:pre_sign_in_notice] if flash[:pre_sign_in_notice] 31 rescue 32 logger.error "An error occurred trying to execute the saved POST request: #{$!}" 33 flash[:alert] = $!.message 34 ensure 35 session[:pre_sign_in_post] = nil 36 end 37 end 38 end
The above solution is far from generic but it works well for socialite. Among other things, if the action method from the saved post request calls out to render, this solution will not work. At the very least, the render call would have to be stubbed.
Feel free to have a better look at the code through the github page. And let me know if you have suggestions to improve this solution.