How do I refactor this case statement to work? - ruby-on-rails

So what I would like to do is to do redirects based on the role of the current_user.
This is what I have:
path = case current_user.roles.where(:name => "vendor")
when :vendor
dashboard_path
when :guest
home_path
else
home_path
end
redirect_to path
I am using cancan and the only way to figure out the role of a user, that I know of, is to either do current_user.has_role? :admin or current_user.roles.where(:name => role_name).
Given those constraints (or tell me another way to figure out the role of a user) how do I get this case statement to work?
Edit 1
Assume that I am checking for multiple roles, not just the 2 I have here - could be 4 or 5.
Edit 2
To be clear, this is my current setup.
I am using Devise, CanCan & Rolify. Rolify allows a user to have multiple roles, but my application won't have that use case. A user will just have one role. They can either be a vendor, buyer, guest, superadmin.
If they are a vendor, they can only see the dashboard_path that belongs to them. They can't see any other vendor storefront that belongs to anyone else. They also should not be able to see products from other vendors. So, once they login, their root_path should be dashboard_path not home_path which is what every other role's root_path will be.
If they are a guest, they can see everything except the prices - I already have this logic working. I achieved this like this:
if user.has_role? :guest
can :read, [Product, Vendor, Banner]
cannot :view_prices, Product
end
Then in my view, I just did something like this:
<% if can? :view_prices, Product %>
<div class="price pull-right">
<%= number_to_currency(#product.price) %> ea
</div>
<% else %>
<span class="no-price pull-right"><%= link_to "Log In To See Price", login_path %></span>
<% end %>
So, basically...my real goal is to try and change the root_path depending on the role the user has. I am basically trying to implement the answer on this question.

FINAL ANSWER (for earlier anwsers, see below)
If your users can only have one role, I'd say your current implementation is not exactly appropriate. However, if you really need to keep this implementation, you can do something like this :
class User < ActiveRecord::Base
# this will return the name of the first (so the only one)
# role that your user has, or nil.
def role_name
roles.first.try( :name )
end
end
so now your case statement would work :
path = case current_user.role_name
when 'vendor' ; dashboard_path
when 'guest' ; home_path
when 'foo' ; bar_path
else home_path
end
I still encourage you to wrap your case statement in a helper for reusability and easier maintainance.
EARLIER ANSWER
I'm not sure i understand your question, but i think you don't need a case statement here :
redirect_to (current_user.has_role? :vendor ? dashboard_path : home_path)
another way is to push part of the responsability to the user class (or a presenter):
class User < ActiveRecord::Base
def home_page
# here some logic to return a symbol like :home or :dashboard,
# depending on your roles implementation. Your User could even use
# a state machine to do this, or have multiple decorators.
end
end
and then with a helper
def home_path_for( user )
case user.home_page
when :home ; home_path
when :dashboard ; dashboard_path
when :foo ; bar_path
else home_path
end
end
FIRST EDIT
if your user can have multiple roles at a time, i'd say a case statement is not appropriate. case is a branching statement that is appropriate when you only have one and only one input and one and only one outcome out of a set of possible outcomes.
So you have to reduce your list of roles to an intermediate state, for instance :
DASHBOARD_ROLES = [:vendor, :admin]
CLIENT_ROLES = [:client, :prospect]
BASIC_ROLES = [:logged_visitor]
if (DASHBOARD_ROLES & user_roles).any?
:admin_dashboard
else if (CLIENT_ROLES & user_roles).any?
:client_dashboard
# additionnal, different logic is possible
else if (BASIC_ROLES & user_roles).any? && user_logged_in? && user_roles.first != :prospect
:logged_dashboard
else
:home
end
this is a completely different kind of logic.

First of all, you probably need to address the case when user has multiple roles, if that is possible.
Assuming a user has one role (though we can add more conditions if need be) Can you consider a hash?
Something like -
path = {:vendor => dashboard_path, :guest => home_path} [current_user.active_role] || default_path
I have made some assumptions -
current_user.active_role could be the current role of the user based on which you can redirect the answer.
default_path is self explanatory.

Related

Modify attribute value based on user role

I have a column 'created_by' in a table. Based on the role of logged in user I have to show either the id of the user or the role of the user. Suppose the id logged in user is a customer and the record was created by the user who has support role, then it should show 'Support', and if the support logs in then show the id of the person who added(even if its a different support person's id). I can figure out the current user's role. How can I achieve this without defining a separate apis based on role? Is it possible to have same api to get results from db based on query but transform the column based on role.
My initial thought is to create a view helper.
I'll give you an idea for what it could look like. Since you didn't share the name of the model in your question, I'm picking an arbitrary model (Watermelons) that has a created_by relationship to Users. I realize that's a silly choice. I'm an enigma...
app/helpers/watermelons_helper.rb
module WatermelonsHelper
def created_by_based_on_role(watermelon)
if current_user.role == "Support" || watermelon.created_by.role == "Generic User"
watermelon.created_by.name
else
"The Watermelons.com Support Team"
end
end
end
app/controllers/watermelons_controller.rb
class WatermelonsController < Application Controller
def show
#watermelon = Watermelon.find(params[:watermelon_id])
end
end
app/views/watermelons/show.html.erb
...
<p>This watermelon was created by <%= created_by_based_on_role(#watermelon) %></p>
...
The reason why I'd make this a helper method and not a method in the watermelon.rb model is that the method is dependent on the current_user, which the model can't explicitly know each time (assuming you're using Devise or a hand-rolled, Hartl-style current_user helper method as well).
Edit: Per feedback, let's make it a model method...
app/models/watermelons.rb
class Watermelon < ActiveRecord::Base
...
def created_by_based_on_role(viewing_user)
if viewing_user.role == "Support" || self.created_by.role == "Generic User"
self.created_by.name
else
"The Watermelons.com Support Team"
end
end
...
end
Note that I'm implementing this as an instance method, which means you will need to call "watermelon.created_by_based_on_role(current_user)" in the controller or view.

what is the "rails way" for enabling an admin to create privileges for an existing user?

I'm writing a ruby on rails website for the first time. I have a User model and a Manager model. The user has_one Manager and a Manager belongs_to a User. The Manager model contains more info and flags regarding privileges. I want to allow an admin while viewing a User (show) to be able to make him a manager.
This is what I wrote (probably wrong):
In the view: <%= link_to 'Make Manager', new_manager_path(:id => #user.id) %>
In the controller:
def new
#user = User.find(params[:id])
#manager = #user.build_manager
end
resulting in a managers/new?id=X Url.
I would separate roles and permissions from the User class. Here's why:
Managers are users too. They share the same characteristics of Users: Email address, first name, last name, password, etc...
What if a manager also has a higher level manager? You'll have create a ManagerManager class, and that's terrible. You might end up with a ManagerManagerManager.
You could use inheritance, but that would still be wrong. Managers are users except for their title and permissions, so extract these domains into their own classes. Then use an authorisation library to isolate permissions.
You can use Pundit or CanCan. I prefer Pundit because it's better maintained, and separates permissions into their own classes.
Once you have done that, allowing a manager to change a normal user to a manager becomes trivial and easy to test:
class UserPolicy
attr_reader :user, :other_user
def initialize(user, other_user)
#user = user
#other_user = other_user
end
def make_manager?
user.manager?
end
end
In your user class you can have something like:
def manager?
title == 'manager?'
# or
# roles.include?('manager')
# Or whatever way you choose to implement this
end
Now you can always rely on this policy, wherever you are in the application, to make a decision whether the current user can change another user's role. So, in your view, you can do something like this:
- if policy(#user).make_manager?
= link_to "Make Manager", make_manager_path(#user)
Then, in the controller you would fetch the current user, and the user being acted upon, use the same policy to otherwise the action, and run the necessary updates. Something like:
def make_manager
user = User.find(params[:id])
authorize #user, :make_manager?
user.update(role: 'manager')
# or better, extract the method to the user class
# user.make_manager!
end
So you can now see the advantage of taking this approach.

I'm attempting to determine my user's role from within a view helper and am concerned that I'm doing it wrong

I have helpers that are used to determine whether a given user is an administrator or a vendor. From within the helpers, I query the database for their respective role objects (administrator, vendor, etc.) for comparison against roles associated with the user but have a feeling that this is an ass-backward way to go about determine a user's role.
Am I doing something wrong here? What could I do better? I should probably mention that I'm using/learning Pundit, so maybe it contains a better means by which to accomplish this.
Here's my code:
users_helper.rb
1 module UsersHelper
2 def admin?
3 # Determine whether the user has administrator status within a given event
4 #admin_role = Role.find(1)
5 return true if #user.roles.include? #admin_role
6 end
7
8 def vendor?
9 # Determine whether the user is an approved vendor within a given event
10 #vendor_role = Role.find(2)
11 return true if #user.roles.include? #vendor_role
12 end
13 end
Here's how I use the helpers from within my template:
show.html.erb
1 <% provide(:title, #user.username) %>
2
3 <% if admin? %>
4 <p>Admin</p>
5 <% elsif vendor? %>
6 <p>Vendor</p>
7 <% else %>
8 Something else.
9 <% end %>
The associations of User model with roles imply that your User model may have more then one role such as admin or vendor at the same time.
I would suggest to refactor User model to use has_one association with role, so that users will have only one role.
Then the helper in user_helper.rb might be something like:
def is_a?(user)
roles = { Roles.find(1) => "Admin", Roles.find(2) => "Vendor"}
roles.fetch(user.role) do
"Something else."
end
end
This will return string with correct string definition of role. You can make comparison with that or use it in view as follows:
<p><%= is_a?(#role) %></p>
Which does same thing as your code above in one line.
This might be better over at Code Review.
1) Your role implementation is a bit clunky (what with the magic numbers) - do you do anything with the Role class that warrants its being an ActiveRecord class?
If not, you could use the role_model gem (as mentioned by Sontya), which saves the user roles as a bitmask in the users table.
In case you need a more sophisticated role class, you may want to have a look at rolify, which is similar in concept to your current solution.
Either way, user roles in Rails applications are a solved problem, you do not need to write your own solution (of course, don't let that discourage you, building your own solution is at the very least a good learning exercise).
2) Checking for roles in a view is a pretty bad idea, especially if you're already using pundit.
If you ever add a new role, you need to go through all the views and change the if conditions to support the new new role.
This is a problem that explodes when both your number of views and your number of roles grow.
Instead, use the pundit policies in your view, and check for the users role(s) in the corresponding policy object.
Example:
class CompanyPolicy < ApplicationPolicy
def delete?
user.is?(:admin)
end
end
in the view:
<% if policy(#company).delete? %>
delete link here
<% end %>
Don't forget to authorize in the controller as well!
That way, if you add a new role, you simply have to change a few policy objects and you're good to go.
First off all, include? method returns boolean, so:
return true if #user.roles.include? #admin_role
is equal to:
#user.roles.include? #admin_role
Unless you need nil instead of false.
Second, your method names and the code itself should be so simple and self-descriptive, to not require comments. Unless you need automated documentation, when writing public gem or something.
Regarding you question, IMHO you should do this in your User model:
def admin?
roles.where(id: 1).any?
end
Then you can do #user.admin? in the view.

CanCan, nested resources and using methods

In my new project, I have a resource Bet which other users can only read if they are the owners of the bet or friends of him. The main problem comes when I want to define abilities for the index action. In an index action, the block does not get executed, so I guess it's not an option.
Let's illustrate it. If I wanted only the owner to be able to index the bets, this would be enough:
can :read, Bet, :user => { :id => user.id }
But I need the acceptable ids to be a range, one defined by all the friends of the user. Something like:
if (bet.user == user) || (bet.user.friends.include? user)
can :read, Bet
end
But this is not correct CanCan syntax.
I guess that a lot of people has had problems with CanCan and nested resources, but I still haven't seen any answer to this.
In your Bet model, create a method:
def is_accessible_by? (user)
owner = self.user
owner == user || owner.friends.include?(user)
end
Now in ability.rb, set your CanCan permission:
can :read, Bet { |bet| bet.is_accessible_by?(user) }
EDIT
As you point out, since index actions don't have an instance of the object, the block above won't get executed.
However, it sounds like what you are trying to do - list the bets owned by the user or their friends - should not be handled using CanCan or permissions. I would create a function in my User model:
def bet_listings
friend_bets = friends.inject([]){ |bets, friend| bets<<friend.bets; bets }
self.bets + friend_bets
end
Then in your index action:
#bets = user.bet_listings
Just now I have found a solution, but I don't like it much. It's about creating a custom action and defining abilities for it. For example...
In the controller:
def index
authorize! :index_bets, #user
end
In ability.rb:
can :index_bets, User do |friend|
user == friend || user.friends.include?(friend)
end
It works, but I don't feel great about using it. Isn't out there anything more elegant?

Rails plugin for Group of users

My Rails application have a User model and a Group model, where User belongs to a Group. Thanks to this, a user can be a admin, a manager, a subscriber, etc.
Until recently, when for example a new admin need to be create on the app, the process is just to create a new normal account, and then an admin sets the new normal account's group_id attribute as the group id of the admin... using some condition in my User controller. But it's not very clean, I think. Because for security, I need to add this kind of code in (for example) User#update:
class UsersController < ApplicationController
# ...
def update
#user = User.find(params[:id])
# I need to add some lines here, just as on the bottom of the post.
# I think it's ugly... in my controller. But I can not put this
# control in the model, because of current_user is not accessible
# into User model, I think.
if #user.update_attributes(params[:user])
flash[:notice] = "yea"
redirect_to root_path
else
render :action => 'edit'
end
end
# ...
end
Is there a clean way to do it, with a Rails plugin? Or without...
By more clean, I think it could be better if those lines from User#update:
if current_user.try(:group).try(:level).to_i > #user.try(:group).try(:level).to_i
if Group.exists?(params[:user][:group_id].to_i)
if Group.find(params[:user][:group_id].to_i).level < current_user.group.level
#user.group.id = params[:user][:group_id]
end
end
end
...was removed from the controller and the application was able to set the group only if a the current user's group's level is better then the edited user. But maybe I'm wrong, maybe my code is yet perfect :)
Note: in my User model, there is this code:
class User < ActiveRecord::Base
belongs_to :group
attr_readonly :group_id
before_create :first_user
private
def first_user
self.group_id = Group.all.max {|a,b| a.level <=> b.level }.id unless User.exists?
end
end
Do you think it's a good way? Or do you process differently?
Thank you.
i prefer the controller methods to be lean and small, and to put actual model logic inside your model (where it belongs).
In your controller i would write something along the lines of
def update
#user = User.find(params[:id]
if #user.can_be_updated_by? current_user
#user.set_group params[:user][:group_id], current_user.group.level
end
# remove group_id from hash
params[:user].remove_key(:group_id)
if #user.update_attributes(params[:user])
... as before
end
and in your model you would have
def can_be_updated_by? (other_user)
other_user.try(:group).try(:level).to_i > self.try(:group).try(:level).to_i
end
def set_group(group_id, allowed_level)
group = Group.find(group_id.to_i)
self.group = group if group.present? && group.level < allowed_level
end
Does that help?
Well if you have a User/Groups (or User/Roles) model there is no other way to go than that you have underlined.
If it is a one-to-many association you can choose to store the user group as a string and if it is a many-to-many association you can go for a bitmask but nonetheless either through business logic or admin choice you need to set the User/Group relation.
You can have several choices on how to set this relationship in a view.
To expand your model's capability I advice you to use CanCan, a very good authorization gem which makes it super easy to allow fine grain access to each resource in your rails app.

Resources