Getting user information using either user.login or user.id - ruby-on-rails

So I have a url like the following
localhost/users/:id/posts
which gives the posts of that particular user. Now this id can be either his login (which is a string) or the id (user.id) which is technically an Integer but params[:id] is always a string. So how do I implement this an action.
#user = params[:id].is_a?(String) ? User.find_by_login(params[:id]) : User.find(params[:id])
The above code miserably fails since params[:id] is always a string. Any thoughts? Thanks.

When I've done this, I've actually had two separate controller actions-- show and show_by_login. I feel like it's less unpredictable that way, and I have more control.
Be sure to enforce uniqueness of your logins, index them, and if show_by_login can't find the record you have to raise ActiveRecord::RecordNotFound yourself.
def show
#user = User.find(params[:id])
respond_to do |format|
format.html
format.xml { render :xml => #user.to_xml }
end
end
def show_by_login
#user = User.find_by_login(params[:login])
unless #user
raise ActiveRecord::RecordNotFound
end
render :action => 'show'
end

You could use a regular expression:
#user = params[:id] =~ /^\d+$/ ? User.find(params[:id]) : User.find_by_login(params[:id])

So long as you don't allow any logins to consist purely of digits, you could write your own finder/named_scope.
class User < ActiveRecord::Base
named_scope :find_by_id_or_login, lambda {|id_or_login|
{ :conditions => ["id = ? OR login = ?", id_or_login, id_or_login] }
}
end
#user = User.find_by_id_or_login(params[:id])

Related

Nested form validation rails 3.2

I have a job and user(devise) form in the same view. When I am trying to submit with errors in the user fields it gives me an exception page with the validation messages. Submitting errors in the job fields works fine!
job_controller.rb
def new
#job = Job.new
if !current_user
#job.user = User.new
end
respond_to do |format|
format.html # new.html.erb
end
end
def create
#types = Type.all
#categories = Category.all
#job = Job.new(params[:job])
#if not logged in creates a user and sign in
if !current_user
#user = User.new(params[:job][:user_attributes])
else
#user = current_user
end
#job.user_id = #user.id
respond_to do |format|
if #job.save
if !current_user
sign_in(:user, #user)
end
format.html { redirect_to #job }
else
format.html { render action: "new" }
end
end
end
job.rb
attr_accessible :user_attributes, :description, :name ....
belongs_to :user
accepts_nested_attributes_for :user
Thanks!
That becuase you are calling, #user.save! which will generate an exception. Also doing it this way won't put the job in the same transaction as User. What you want are nested_attributes:
class Job < ActiveRecord::Base
accepts_nested_attributes_for :user
end
If the user is logged in, don't show that part of the form and filter those params.
See more in the Rails documentation here http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
EDIT:
Simplify your controller code, since you're using nested attributes you no longer need to manually create a user.
#if not logged in creates a user and sign in
if !current_user
#user = User.new(params[:job][:user_attributes]) # this is no longer needed
else
#user = current_user
end
#job.user_id = #user.id # this is redundant
To something more like:
# if logged in, manually assign the user (also you may want to reject any user attributes)
#job.user = current_user if current_user

How to escape this redirect loop?

I'm trying to make all a user's illicit attempts to see other users' show page redirect instead to their own show page.
None of my attempts in the else section of this code do the job though.
class UsersController < ApplicationController
def show
if params[:id] == current_user.id.to_s
liked_bookmark_ids = current_user.likes.pluck(:bookmark_id)
liked_bookmarks = Bookmark.where(id:liked_bookmark_ids)
liked_topic_ids = liked_bookmarks.pluck(:topic_id)
#liked_topics = Topic.where(id:liked_topic_ids).order('topics.name')
else
# redirect_to :controller => 'users', :id => current_user.id # gives a screwy url
# redirect_to user_path, :id => 6 # causes a redirect loop
# redirect_to :back # causes a redirect loop
end
end
end
What's the right way to go about it?
redirect_to user_path(current_user)
I think a better approach would be to use a filter to check if it's a request by a current user or not. You can do something like this:
class UsersController < ApplicationController
before_action :check_current_user, only: :show
def show
liked_bookmark_ids = current_user.likes.pluck(:bookmark_id)
liked_bookmarks = Bookmark.where(id:liked_bookmark_ids)
liked_topic_ids = liked_bookmarks.pluck(:topic_id)
#liked_topics = Topic.where(id:liked_topic_ids).order('topics.name')
end
private
def check_current_user
redirect_to current_user, notice: "Not authorized" if params[:id] != current_user.id.to_s
end
end

return redirect_to in private controller method

Preface: I'm using devise for authentication.
I'm trying to catch unauthorized users from being able to see, edit, or update another user's information. My biggest concern is a user modifying the form in the DOM to another user's ID, filling out the form, and clicking update. I've read specifically on SO that something like below should work, but it doesn't. A post on SO recommended moving the validate_current_user method into the public realm, but that didn't work either.
Is there something obvious I'm doing wrong? Or is there a better approach to what I'm trying to do, either using devise or something else?
My UsersController looks like this:
class UsersController < ApplicationController
before_filter :authenticate_admin!, :only => [:new, :create, :destroy]
before_filter :redirect_guests
def index
redirect_to current_user unless current_user.try(:admin?)
if params[:approved] == "false"
#users = User.find_all_by_approved(false)
else
#users = User.all
end
end
def show
#user = User.find(params[:id])
validate_current_user
#user
end
def new
#user = User.new
end
def edit
#user = User.find(params[:id])
validate_current_user
#user
end
def create
#user = User.new(params[:user])
respond_to do |format|
if #user.save
format.html { redirect_to #user, :notice => 'User was successfully created.' }
else
format.html { render :action => "new" }
end
end
end
def update
#user = User.find(params[:id])
validate_current_user
respond_to do |format|
if #user.update_attributes(params[:user])
format.html { redirect_to #user, :notice => 'User was successfully updated.' }
else
format.html { render :action => "edit" }
end
end
end
private
def redirect_guests
redirect_to new_user_session_path if current_user.nil?
end
def validate_current_user
if current_user && current_user != #user && !current_user.try(:admin?)
return redirect_to(current_user)
end
end
end
The authenticate_admin! method looks like this:
def authenticate_admin!
return redirect_to new_user_session_path if current_user.nil?
unless current_user.try(:admin?)
flash[:error] = "Unauthorized access!"
redirect_to root_path
end
end
EDIT -- What do you mean "it doesn't work?"
To help clarify, I get this error when I try to "hack" another user's account:
Render and/or redirect were called multiple times in this action.
Please note that you may only call render OR redirect, and at most
once per action. Also note that neither redirect nor render terminate
execution of the action, so if you want to exit an action after
redirecting, you need to do something like "redirect_to(...) and
return".
If I put the method code inline in the individual controller actions, they do work. But, I don't want to do that because it isn't DRY.
I should also specify I've tried:
def validate_current_user
if current_user && current_user != #user && !current_user.try(:admin?)
redirect_to(current_user) and return
end
end
If you think about it, return in the private method just exits the method and passes control back to the controller - it doesn't quit the action. If you want to quit the action you have to return again
For example, you could have something like this:
class PostsController < ApplicationController
def show
return if redirect_guest_posts(params[:guest], params[:id])
...
end
private
def redirect_guest_post(author_is_guest, post_id)
redirect_to special_guest_post_path(post_id) if author_is_guest
end
end
If params[:guest] is present and not false, the private method returns something truthy and the #show action quits. If the condition fails then it returns nil, and the action continues.
You are trying and you want to authorize users before every action. I would suggest you to use standard gems like CanCan or declarative_authorization.
Going ahead with this approach you might end up reinventing the wheel.
In case you decide on using cancan, all you have to do is add permissions in the ability.rb file(generated by rails cancan:install)
can [:read,:write,:destroy], :role => "admin"
And in the controller just add load_and_authorize_resource (cancan filter). It will check if the user has permissions for the current action. If the user doesnt have persmissions, then it will throw a 403 forbidden expection, which can be caught in the ApplicationController and handled appropriately.
Try,
before_filter :redirect_guests, :except => [:new, :create, :destroy]
should work.
This is because you are using redirect twice, in authenticate_admin! and redirect_guests for new, create and destroy actions.
"Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action."
That's the reason of the error. In show method, if you are neither the owner of this account nor the admin, you are facing two actions: redirect_to and render
My suggestion is to put all of the redirect logic into before_filter

Is there a more idiomatic way to handle this controller logic in Rails?

def index
#workouts = Workout.all
#user_workouts = current_user.workouts.order("created_at DESC") unless current_user.blank?
if #client.present?
#user_workouts = #client.workouts.order("created_at DESC")
end
respond_to do |format|
format.html # index.html.erb
format.json { render json: #workouts }
end
end
The issue in question here is the instance variable #user_workouts - I am using a nested route to be able to do something like /clients/1/workouts instead of /workouts which will show the current users workouts which if that were nested would be /users/1/workouts.
Any idiomatic way to handle this or is it normal to just let the conditionals creep in?
You could create a class method on Workout accepting a User instance
def self.for_user(user)
where(user_id: user.id).order("created_at DESC")
end
and then simplify your action
def index
#workouts = Workouts.all
#user_workouts = Workout.for_user(#client || current_user)
respond_to ...
If #client exists it will be passed to for_user, otherwise current_user will.
You could DRY it up a little with:
user = #client || current_user
#user_workouts = user.workouts.order("created_at DESC")
Other than that, it looks pretty good as-is.

Rails: how can I optimize this action

The action bellow creates a new comment.
A user has many statuses
A status has many comments
How can optimize this action so that head 401 and return is not repeated many times.
def create
#user = User.where(id: params[:user_id]).first
if #user
if current_user.friend_with?(#user) or current_user == #user
#status = #user.statuses.where(id: params[:status_id]).first
if #status
#comment = #status.comments.build(params[:comment])
#comment.owner = current_user
if #comment.valid?
#comment.save
current_user.create_activity(:comment_status, #comment, #user)
else
head 401 and return
end
else
head 401 and return
end
else
head 401 and return
end
else
head 401 and return
end
end
Thank you.
When do you want to return 401?
when a user has not been found
when a user is not a current user or is not a friend of that user
when a status has not been found
when new comment has not been successfully created
Instead of using so many conditionals, you can use methods that raise exceptions. When you do so, you can rescue from that exceptions with the desired behavior (rendering 401).
So my suggestions for listed conditions are:
use find! instead of where and then first.
raise something, preferably custom exception (NotAFriendError)
same as 1., use find!
use create!, it's an equivalent to new and then save! which will raise ActiveRecord::RecordInvalid exception if it fails on validation.
Here's the result:
def create
begin
#user = User.find!(params[:user_id])
raise unless current_user.friend_with?(#user) || current_user == #user
#status = #user.statuses.find!(params[:status_id])
#comment = #status.comments.
create!(params[:comment].merge(:owner => current_user))
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
head 401
end
# everything went well, no exceptions were raised
current_user.create_activity(:comment_status, #comment, #user)
end
You have a lot of excessive checking and branching in your code, so it can be simplified to this:
def create
success = false
#user = User.find(params[:user_id])
current_user_is_friend = current_user.friend_with?(#user) || current_user == #user
if #user && current_user_is_friend && #status = #user.statuses.find(params[:status_id])
#comment = #status.comments.build(params[:comment])
#comment.owner = current_user
if #comment.save
current_user.create_activity(:comment_status, #comment, #user)
success = true
end
end
render(status: 401, content: '') unless success
end
A few things I did:
Combine a lot of the if conditions, since there was no need for them to be separate.
Change where(id: ...).first to find(...) since they're the same. Note that, if the find fails, it will give a 404. This may make more sense, though (I think it does)
Don't call #comment.valid? right before #comment.save, since save returns false if the object wasn't valid.
Use || instead of or for boolean logic (they're not the same).
Use render(status: ..., content: '') instead of head ... and return.
Use a boolean variable to track the success of the method.
I would advise that you try and pull some of this logic out into models. For example, User#friend_with should probably just return true if it's passed the same User.
def create
#user = User.where(id: params[:user_id]).first
if #user
if current_user.friend_with?(#user) or current_user == #user
#status = #user.statuses.where(id: params[:status_id]).first
if #status
#comment = #status.comments.build(params[:comment])
#comment.owner = current_user
if #comment.valid?
#comment.save
current_user.create_activity(:comment_status, #comment, #user)
everythingOK = true
end
end
end
end
head 401 and return unless everythingOK
end

Resources