One thing you can do on my rap lyric explanation site is "like" explanations (once you're logged in):
http://dl.getdropbox.com/u/2792776/screenshots/2010-01-17_1645.png
I'd like to show the "Like" links to users who aren't logged in, and then, when a non-logged in user clicks "Like", show him a lightbox with a "Login or Register" form (like Digg / Reddit)
http://dl.getdropbox.com/u/2792776/screenshots/2010-01-17_1650.png
What's the best way to accomplish this?
Currently I'm using this approach:
Clicking "Like" POSTs to /annotations/:id/vote (the POST body indicates whether the user is liking or "unliking").
The vote Annotation controller action has a require_user before_filter that looks like this:
def require_user
unless current_user
store_desired_location
flash[:notice] = "You'll need to login or register to do that"
redirect_to login_path # map.login '/login', :controller => 'user_sessions', :action => 'new'
return false
end
end
user_sessions#new looks like this:
def new
#user_session = UserSession.new
respond_to do |format|
format.html {}
format.js {
render :layout => false
}
end
end
The problem is that the redirect doesn't seem to work correctly over javascript:
http://dl.getdropbox.com/u/2792776/screenshots/2010-01-17_1700.png
How do I get this to redirect correctly?
Also, is this the right general approach? Another thought I had was to attach a different handler to the "Like" links in javascript when there was no logged in user (but I don't think this method scales well to other actions that I'd like to handle the same way)
There's a few problems to overcome here.
Browsers in general do not allow redirecting to a POST request.
redirect_to doesn't preserve format without additional input.
Store location does not preserve form data.
All these problems can be solved by eliminating redirects.
Here is how I've handed it in the past:
Instead of redirecting in required_user, render. If a before filter redirects or renders the pending action is cancelled. (No need to return false either). Unfortunately going this route blurs controller boundaries. But allows for simple html fallback, and lends its self to DRYness.
The high level view of the new work flow will be:
Request to annotations#vote (POST)
required_user filter fails
render new session
submit login information and original POST data back to annotations#vote (POST)
new filter in vote captures session information and logs in. vote proceeds as expected. If login fails return to 3.
annotations#vote redirects/renders as it should
Start by reworking the require_user to render the user_sessions#new template.
def require_user
unless current_user
flash[:notice] = "You'll need to login or register to do that"
#user_session ||= UserSession.new
respond_to do |format|
format.html {render :template => 'user_sessions/new'}
format.js {
render :template => 'user_sessions/new', :layout => false
}
end
end
end
The #user_session ||= UserSession.new ensures we can return validation errors to the form.
Now we've got to beef up your user_session#new template so that it can remember the action. Also if you plan on using lightboxes, this should be a partial rendered rendered by relevant RJS or the new.html.erb.
First we create a partial to create hidden fields preserving the POST data that would have been lost in a redirect:
<% if params[:controller] == "annotations" %>
<% content_for :old_form do %>
<%= hidden_field_tag "annotation[song_id]", params[:annotation][:song_id] %>
<%= hidden_field_tag "annotation[vote]", params[:annotation][:vote] %>
<% end %>
<% end %>
Then render that partial in the login partial that will occupy your lightbox:
<%= render :partial => vote_form_replica %>
<% url = params[:controller] == "user_sessions ? user_sessions_url : {} %>
<% form_tag #user_session, :url => url do |f| %>
<%= yield :old_form %>
<%= f.label :user_name %>
<%= f.text_field :user_name %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= submit_tag %>
<%end%>
The empty hash for url in the form_tag looks like an error, but isn't. It ensures that the form data is posted to the url that rendered the form. Which at this point should be annotations/:id/vote
Now for the new filter to login. Essentially it will be doing what ever UserSessionsController#create does without the render/redirect. The following is copied from the RESTful authentication plugin.
def authenticate
self.current_user = User.authenticate(params[:login], params[:password])
if logged_in?
if params[:remember_me] == "1"
current_user.remember_me unless current_user.remember_token?
cookies[:auth_token] = { :value => self.current_user.remember_token,
:expires => self.current_user.remember_token_expires_at }
end
end
end
All that's left is to make sure the filter order is right.
before_filter :authenticate, :require_user, :only => :vote
N.B.: You're probably not going to use this version of require_user without this version of authenticate so it makes sense to combine them into a single filter.
And that's it. The way this has been set up allows for robust DRY easily reuseable code. By placing the new filters into ApplicationController they're available in any controller. From this point, adding this functionality to any other controllers/actions takes only 3 simple steps:
Create a new partial modelled after the vote_form_replica partial.
Add the corresponding render statement to the new session template.
Apply the filters to your actions.
I would approach this in the way you describe at the bottom of your question. Before displaying the page initially, check if the user is logged in. If they are, the "Like" links should use their normal behavior. If not, bind a click event to show the register/login panel. There's nothing about this that can't be reused. In fact, we use this exact method at my job. Any user action that requires authentication either follows its normal behavior or pops up a generic login panel depending on login state at the time the page loads.
Related
I have a very rudimentary sign-in form where the user enters in his/her name (First Last).
Here is the code in ERB for the form:
<%= form_for :current_user, url: { :controller => 'application', :action=>'current_user' } do |f| %>
<%= f.text_field :name, placeholder: 'First Last', :id => 'currentUser' %>
<%= f.submit 'Submit', :class => 'btn btn-primary' %>
<% end %>
Here is the action current_user:
def current_user
#current_user = User.find_by_name(params[:current_user][:name])
redirect_to '/calendar'
end
And here is it's route
match 'calendar/signin', to: 'application#current_user', via: 'post'
The form works, I tested it by placing a debugger in the current_user action. When I typed in params it returned the user's name that I had entered in as it should have. But once it went back to the /calendar view #current_user became undefined again. Should I be using something rather than redirect_to '/calendar'? Thanks
As a general rule, you should be calling redirect_to with the appropriate path method, for example:
redirect_to calendar_path
Using a literal URL defeats the purpose of the routing system. You'll need to track down and change all instances of /calendar to something else should that ever change. With the routing method, you can change the presentation, the user-facing URL, from the implementation.
Likewise, you should be using the same path notation in your url argument to form_for. Calling it by controller and action is the older way of doing it and isn't without its share of problems, as the URL it uses is not always what you'd prefer.
Your current_user method only assigns #current_user if it's called, and your form requires #current_user to be defined in advance. If for some reason it isn't called, your form won't work, there's no object. :current_user is a reference to #current_user in the context of the view, where the view automatically inherits instance variables from the controller.
Normally you'd have a method like this:
def load_current_user
#current_user = User.find_by(name: ...)
end
Then you trigger this to load all the time:
before_filter :load_current_user
Normally I create a controller class that requires logins and all the "log-in only" controllers inherit from it. That way you should be reasonably assured you don't have pages that are not properly restricted.
Obviously you'd want to skip this on pages that don't require a login, for example your session controller where you log in:
skip_before_filter :load_current_user, only: [ :new, :create ]
I am trying to figure out the best way to do the following (there are a few ways I can think of, but I want to know what the best way to handle it is):
A user is putting together a shipment, and then clicks the "Send" link, which sends him to the /shipments/:id/confirm page. The confirm action checks to see if the user has a completed ShippingAddress; if not, it sends him to the ShippingAddress#new. (If he does, it render the confirm page.
I want the user to be able to complete the ShippingAddress#new page, submit it, and then be redirect back to the /shipments/:id/confirm. How can I do that? How can I pass the :id to the ShippingAddress#new page without doing something like redirect_to new_shipping_address_path(shipment_id: #shipment.id) in the Shipment#confirm action? Or is that the best way to do that?
class ShipmentsController < ApplicationController
def confirm
#shipment = Shipment.where(id: params[:id]).first
unless current_user.has_a_shipping_address?
# Trying to avoid having a query string, but right now would do the below:
# in reality, there's a bit more logic in my controller, handling the cases
# where i should redirect to the CardProfiles instead, or where I don't pass the
# shipment_id, and instead use the default shipment.
redirect_to new_shipping_address_path(shipment_id: #shipment.id)
end
end
end
class ShippingAddressesController < ApplicationController
def new
#shipment = Shipment.where(id: params[:shipment_id]).first
end
def create
#shipment = Shipment.where(id: params[:shipment_id]).first
redirect_to confirm_shipment_path(#shipment)
end
end
[In reality, there is also a CardProfiles#new page that needs to be filled out after the shipping address is].
Try calling render instead of redirect_to, and set the id into an instance variable. Adjust the view logic to pull that instance variable if it exists.
#shipment_id = #shipment.id
render new_shipping_address_path
In the view
<%= form_for #shipment_address do |f| %>
<% if #shipment_id %>
<%= hidden_field_tag :shipment_id, #shipment_id %>
<% end %>
I don't know your view logic entirely, but giving an example.
Ok, I have search Google, API's as well as StackOverflow and have found no real decisive help for my issue. So here goes!
I have a Polymorphic model setup named Favorite and it ties to the User. Being that Favorite is Polymorphic I of course can use the relationship to allow my user to add pretty much any entity in my application as their Favorite.
Each of these Favorite relationships between the user and a specific model I want to be able to call different things such as 'Favorite' or 'Like' or 'Friends'. This allows me to have a different Controller with Views to manage each of these different relationships so they are more understandable to the user and myself. Hence I am covering the global generic idea of Favorites with a more precise idea of a 'Friend'.
So I went ahead and created a Friend controller with its associated views to handle the Favorite relationship between a user and other user's in the system.
But what I have found is that Rails expects me to pass a 'Friend' model in all of my interactions between views and controller even though I want to use the Favorite model and I get 'uninitialized constant Friend' as an error in my view. How do I get past this 'convention', how do I make the controller and views if necessary understand that I am using the Favorite model as my underlying model not the Friend?
I considered creating a new model named 'Friend' and inheriting it from 'Favorite' just to fool the controller, but man that just seems like a waste of energy to me. Any ideas out there?
CODE EXAMPLE this is using the Favorite polymorphic model to ButtSlap another User. Each form partial is pass the User as a local variable called local_entity.
ButtSlapController
class ButtSlapsController < AuthorizedResourceController
def create
#favorite = current_user.favorites.build(params[:favorite])
respond_to do |format|
if #favorite.save
flash[:success] = 'butt slap successful!'
format.html { redirect_to('/lounge') }
# format.js { render :action => "create_success"}
else
flash[:success] = 'ah poop!'
format.html { redirect_to('/lounge') }
# format.js { render :action => "create_failure"}
end
end
end
def destroy
#favorite = current_user.favorites.find(params[:id])
respond_to do |format|
if #favorite.destroy
flash[:success] = 'butt slap has been successfully removed.'
format.html { redirect_to('/lounge') }
# format.js { render :action => "create_success"}
else
flash[:success] = 'ah poop!'
format.html { redirect_to('/lounge') }
# format.js { render :action => "create_failure"}
end
end
end
end
Creates The ButtSlap
<%= form_for current_user.favorites.build, :as => :favorite, :url => butt_slaps_path do |f| %>
<div><%= f.hidden_field :favorable_id, :value => local_entity.id %></div>
<div><%= f.hidden_field :favorable_type, :value => local_entity.class.to_s %></div>
<div class="actions"><%= f.submit "butt slap!" %></div>
<% end %>
Removes the ButtSlap
<%= form_for current_user.get_favorites(
{:id => local_entity.id,
:type => local_entity.class.to_s}),
:html => { :method => :delete }, :url => butt_slaps_path do |f| %>
<div class="actions"><%= f.submit "take back" %></div>
<% end %>
Well it all turned out to be a little gem called CanCan v1.6.4
I have been using CanCan for Authorization within my application and when declaring your authorization rules in your Ability class you can either do it by Model or by Controller or a mixture.
In order to handle this I setup 2 root Controllers which inherited from ApplicationController. The first 'AuthorizedController' is used for all controllers which do not use a Model and the second 'AuthorizedResourceController' is used for all controllers which are backed by a Model.
Turns out that for my ButtSlap controller I had it setup as an AuthorizedResourceController and by doing so CanCan was automatically looking to pull and authorize either a collection or a single model based off of the controller's name 'ButtSlap'. But due to the fact that I was using the Favorite model on the backend every time I tried to post to the controller CanCan tried to load its imaginary model based off of its convention. And I thus received the errors messages 'Uninitialized Constant 'ModelName''.
Once I switched the ButtSlapController from an AuthorizedResourceController over to a AuthorizedController CanCan no longer looked to instantiate and authorize a model based off the controller name and it moved to controller based authorization instead and just like everyone was saying 'Poof' my confusion as to why Rails was looking for a Model tied to a controller name was gone.
You really have to love bugs like this, they really stretch your limits as well as your keyboard stockpile (I tend to throw keyboards when I get frustrated ;)
Say I have a site like this (generic Q&A site) in Rails and I wanted this "ask" page w/ a text box to be the first page a user sees, even if he's not logged in. He enters a question, and on the 'new' method I check that he's not logged in, and bounced him to /session/new, where he can either log in or create a new account. Question is, how do I (and what is the best way to) preserve that question that he initially asked all through this process?
I'm understanding the flow of action described in the question to be
user is presented with a form
user is redirected to log in page on submit
user is redirected back to form on successful log in
repopulate form on load (Question asks how to do this step)
user finally submits their form.
With steps 2-4 omitted if the user is logged in.
I'm sorry, but I see your question more as a symptom of an underlying UI issue than a rails question.
If only logged in users can post questions, then why display the text box?
If a user is going to have log in any way, why not get that out of the way first. An even better solution is to integrate the log in and form.
Something like this in the view:
<% form_for :question do |form| %>
<% unless logged_in? %>
<% fields_for :session do |session_form|%>
<%= session_form.label :login %>
<%= session_form.text_field :login %>
<%= session_form.label :password %>
<%= session_form.password_field :password %>
<%end%>
<%end%>
<%= form.text_area :question %>
<%end%>
And in the controller
def new
...
unless params[:session].nil?
self.current_user = User.authenticate(params[:session][:login], params[:session][:password])
end
if logged_in?
flash[:notice] = "Logged in successfully"
else
flash[:error] = "Incorrect username and or password."
end
if logged_in? && #question.save
.... process successful entry
else
... process unsuccessful entry
end
end
Edit: Mohamad's raises the question of reusing this pattern across multiple controllers and forms. So the answer was updated to address reuse of this pattern.
To simplify this for reuse, you could put this block in a helper function that is referenced in the before_filter for actions that require it.
def login
unless params[:session].nil?
self.current_user = User.authenticate(params[:session][:login], params[:session][:password])
if logged_in?
flash[:notice] = "Logged in successfully"
else
flash[:error] = "Incorrect username and or password."
end
end
end
as in:
before_filter :login => :only [:new , :edit, :update, :delete]
On the view side, it shouldn't be too hard to construct a new variant of form_for that embeds the session parameters. Maybe form_for_with_session?
As for handling an unsuccessful response, I would suggest helper function that takes a block of code. Sorry I don't have time to write out or test one for you.
You keep it in the session. So after logging in, when the user goes back to asking his question, you see there's already something in session.
And you can directly display it.
def create
if current_user # Implement this method in your auth framework
#question = Question.new(params[:question] || session.delete[:question])
# (the usual stuff you'd do to save)
else
session[:question] = params[:question]
redirect_to :controller => :sessions, :action => "new"
end
end
Then, after your user creation and authentication stuff is all done in your login action, just make sure you POST back to this create action if session[:question] is defined.
How can I render after executing an action in a restful controller instead of redirecting.
I have a controller with standard actions, and I added a special action that adds data to the resource in question, via a form on the #show page (Think comments on a post). I am validating the input from the form and want to re-render the show action on error and redirect to the show action on success.
I want to render to save the user from inputting their info twice, but when I try to render the show action with an error in the flash[:notice] I get an error saying that I am not specifying an ID. When I do specify an ID, it tries to render a new template that doesn't exist yet.
I am thinking that it should be a as simple as:
def add_comment
if my_validation?
save the object
redirect_to :action => "show", :id => params[:id]
else
render :action => "show", :id => params[:id]
end
end
This is not my actual code, just something I put together just now as an example.
The best way is to re-render the :new
def create
#obj = TheObject.new(params[:object])
render :action => :new unless #obj.save
end
And in the new.html.erb
<% form_for :obj,
:url => object_url(#obj), :html => {:method => :post} do |f| %>
<%= f.text_field :name %>
<% end %>
That way, the inputs in the form will be pre-filled with what the user entered.
Create a new data object and add the values from the form, before you rerender, think it would work then. If you still get problems, try setting a boolean for editing new vs. existing rows, or create two different views entirely.
I've done it before but I don't quite remember how. Sometimes when I used the very typical use of the MVC pattern, it was allmost "automagical", othertimes (as I had to use an old quirky database) I had to code all the magic myself; sometimes usin the .new? function (or what it was called) on the ActiveRecord object, othertimes I used temporary "magic values" for ID (typically alphabetic strings for invalid id values.
(I appologize if I made some mistakes, it's a while since I coded Rails code...)