I'm trying to make a specific link in my application only visible to users who have the attribute :department equal to "Sales".
In other words, I've got a model Users, in which a user has a password, username, and department. The session saves the :user_id once the user is logged in.
What I would like to do is when my view is rendered, depending on the :department of the logged in user, either display, or don't display a specific link.
Here's the code I've got in my view, but I'm struggling with how to take the session info and find the department of the user from it.
<% if Users.where(id: session[:user_id])[:department] == "Sales" %>
<%= link_to 'New Request', new_request_path %>
<% else nil %>
<% end %>
I know it's bad to do a query anywhere other than the controller or model, so if you have any advice on how to better structure this logic as well, I would appreciate it.
I think what you want is:
<% user = User.find_by_id(session[:user_id]) %>
<% if user.present? && user[:department] == "Sales" %>
<%= link_to 'New Request', new_request_path %>
<% end %>
Personally, I'd put this into a helper method to clean it up:
In app/helpers/users_helper.rb:
def user_in_sales?
user = User.find_by_id(session[:user_id])
user.present? && user[:department] == "Sales"
end
Then your view:
<% if user_in_sales? %>
<%= link_to 'New Request', new_request_path %>
<% end %>
Personally, I'd strongly look into using something like cancan to handle this situation. I think you may find that you could use cancan as an effective authorization tool other places in your app, especially if you're doing logic like this elsewhere.
First, you're using an object oriented language. It would help you to quit getting hung up on implementation details (e.g. department == "Sales") and instead consider the intent or meaning you're trying to express and code to meet that. For example:
if current_user.works_in?(:jewelry)
link_to 'Request Receipt', new_request_path
end
Your models should expose a public interface that allows other objects in your code (like your controller) to get the information they need (i.e. whether or not a user has an association with a department) without knowledge of or concern for the underlying data storage schema.
class User
def works_in?(department_name)
departments.pluck(:name).include?(department_name.to_s)
end
end
Related
I am very novice at Ruby and Ruby on Rails.
I am trying to update a web application that has signed up volunteers that require approval before they can access full website functionality. I added a boolean field to the volunteers database model called :approved that defaults to false when a volunteer signs up, and requires a logged in administrator to update it to true.
I created an administrator class in my project that can view a list of pending volunteers from the controller:
def view_pending_volunteers
#navbar = true
#volunteers = Volunteer.where(approved: false)
end
In the administrator view I want to use checkboxes associated with volunteer, which when submitted will allow the administrator to update the :approved field in the volunteer table.
I have several issues that are not clear to accomplish this task:
In my administrator model I use has_many :volunteers with a migration that put the volunteer_id as a foreign key in the administrator database table. Is that sufficient to accomplish the task at hand, or do I need to create a join table for :approved? I can't have volunteers belong_to :administrators because they would not be able to sign up on their own, they need to be created by administrators.
I am not sure exactly how to configure the code for checkbox helpers in my administrator view. I am using form_with but I am sure my code is not correct. I would like to pass an array of parameters for each check_box associated with the :approved field for the list of pending volunteers back to the controller.
<p><b style="color:blue;font-size:24px;">
<%= form_with(model: #volunteer, local: true) do |f| %>
<% #volunteers.each do |v| %>
<li>
Volunteer: <%= x.first_name%> <%= x.last_name%> Age: <%= x.age%> Description: <%= x.description%>
<%= f.check_box (#volunteers, v.approved, options = {}, checked_value = "1", unchecked_value = "0")%>
</li>
<% end %>
<%= f.submit "Approve" %>
<% end %>
</b></p>
I am not exactly sure how to handle the array of checkbox values that get returned to the administrator controller in order to update the volunteer database table. Do I need to create a hash in the view (how to do that?) and then loop through the hash to update_attribute(:approved, true) for each checked volunteer? I have created an update_pending_volunteers method in the administrator controller for the POST operation, but am unclear on the code that should be there because I am unsure the checkbox approach.
Thanks in advance for your patience with a newbie. This seems like such a simple thing to do but I am not sure of the proper approach. I have spent quite a bit of time reviewing APIs and videos and stack overflow articles but cannot seem to piece together information that will give me confidence in a particular approach to do this correctly. Again it seems like such a simple thing to accomplish but has become a source of frustration.
See the comment from Rockwell Rice. Don't create a relationship of any kind for this functionality.
Use the URL feature of form_with, not model. You're not acting on one volunteer, you're acting on many. Create a post route (ie. approve_volunteers). In the view you would create the checkboxes like this:
<%= form_with(url: approve_volunteers_path, local: true) do |f| %>
<% #volunteers.each do |v| %>
<div class="field">
<label class="checkbox">
Volunteer: <%= x.first_name%> <%= x.last_name%> Age: <%= x.age%> Description: <%= x.description%>
<input type="checkbox" name="volunteers[]" value="<%= v.id %>">
</label>
</div>
<% end %>
<%= f.submit "Approve" %>
<% end %>
That should send params through the form like {"volunteers" => ["1", "3"]} and leave the rest empty. You might have to play around with those a little. Check your console for the params.
Then in your controller something like:
def approve_volunteers
volunteer_ids = []
volunteers = Volunteer.where(approved: false)
volunteers.each do |v|
if v.id.in?(params[:volunteers])
volunteer_ids << v.id
end
end
Volunteer.where('id IN (?)', volunteer_ids).update(approved: true)
end
See above.
The checkbox code provided by Sam worked perfectly. My url for the view is "update_pending_volunteers_path." I need to improve the formatting a little bit.
The code in the controller that worked to loop through the array of volunteer ids that was passed back into the controller is as below:
def view_pending_volunteers
#navbar = true
#volunteers = Volunteer.where(approved: false)
end
def update_pending_volunteers
#navbar = true
params[:volunteers].each do |value|
Volunteer.find(Integer(value)).update(approved: true)
end
redirect_to (administrator_dashboard_path)
end
The volunteers passed into the view have already been parsed to just those that have not been approved, so the volunteer_ids returned as checked are only from that list.
I've been struggling to get this to work following a tutorial. I've got Users that have profiles, and a Client model that, through a 'many to many' join table, establishes the relationships between the user and the clients.
What I'm trying to do is create a list of check boxes generated from the list of clients in the DB that you can tick on or off, and then when you submit it, the user will have the relationship to those clients through the join table.
It's sort of working with static data as you can see below:
/profiles/show.html.erb
<% #clients.all.each do |client| %>
<li>
<%= check_box_tag "user[client_ids][]", client.id %>
<%= client.client_name %>
</li>
<% end %>
<%= link_to 'Add Clients', '../assign_clients/' + #profile.user.id.to_s , class: 'btn btn-default' %>
Routes
get 'assign_clients/:id', to: 'users#assign_clients'
And finally in my users_controller.erb
def assign_clients
#user = User.find(params[:id])
#user.client_ids = [1,2]
redirect_to :back, alert: 'Assigned Users'
end
Obviously it's just using hard coded values of 1 and 2. What I'm not sure how to do is wrap the checkboxes in the correct form tag/simple_form (which I am using), and then with the 'submit' button, have that do the 'assign_clients' action that passes through the values.
Thank you for any help.
What I'm not sure how to do is wrap the checkboxes in the correct form
tag/simple_form (which I am using), and then with the 'submit' button,
have that do the 'assign_clients' action that passes through the
values.
In order to create a form that will trigger the assign_clients method a route needs to be setup in your routes.rb file like the following:
resources :users do
patch 'assign_clients', to: 'users#assign_clients', as: 'assign_clients'
end
This sets up a route for a user that you can use the http patch method with (ie. UPDATE). The plan is to pass the client_ids to the users controller as params from the form. I gave it a path name so that we can reference it in the form as user_assign_clients_path(:user_id)
Now that we have the route set up...using the default rails form tags you can structure the form along the lines of this:
<%= form_for #user, url: user_assign_clients_path(#user) do |f| %>
<% #clients.each do |client| %>
<li>
<%= check_box_tag "user[client_ids][]", client.id, #user.clients.include?(client) %>
<%= client.client_name %>
</li>
<% end %>
<%= f.submit "Add Clients", class: "btn btn-default" %>
<% end %>
This will create a form allowing you to post the selected clients as an array of ids to the assign_clients method.
Finally, the assign_clients method can then retrieve the client_ids from the params hash (via params[:user][:client_ids] most likely) and update the user instance (retrieved using user_id from params hash also). You will probably have to add client_ids: [] to the end of your strong parameters list for user to whitelist it - but this essentially should behave like a typical update method.
def assign_clients
#user = User.find(params[:user_id])
#user.update(user_params)
redirect_to wherever_path
end
def user_params
params.require(:user).permit(
client_ids: []
)
end
You need to understand several basic concepts, let me explain to you:
on: member routing - in order to solve your issue directly, your route should be something like:
resources :users do
post '/assign_clients/:client_id', on: :member
end
so that other than user_id, the :client_id can be also passed in as a parameter. For the details, you can read about rails guides on routing.
For the checkbox way, you need nested_attributes - http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html. So that you can achieve what u need with a simple update call on users.
You can also implement a customised logic, with the client_ids passed in as parameters. In order to learn how forms & parameters work in rails, you can build a form, submit it, and see how it goes in the rails server log.
If anything is unclear, simply comment below & I'll try to update.
In my Rails app, I have a store with products and users. Both of those have models and controllers.
What I want to achieve is to show on a view template a product to a current_user based on the attribute from a model that he has. For an example if a user has "Female" attribute from user model, and then to show some products related to this attribute. How can I achieve this?
These are my product views where all products are showed:
<% #products.each do |product| %>
<%= render "product_row", product: product, order_item: #order_item %>
<% end %>
_product_row.html.erb
<h4><%= product.name %></small></h4>
<div class="image">
<%= image_tag product.image.url(:original), class: "img-responsive" %></div>
<p>Some description.</p>
Add to Package
It sounds like you need to retrieve objects from your product class based on an attribute from the User class. Sounds like a basic service object or just a method on the user:
class User < ActiveRecord::Base
def products_for_gender
if gender == 'female'
Product.where("do some logic here based on female")
elsif gender == 'male'
Product.where("do some logic here based on male")
else
#do some other logic just in case it's nil
end
end
end
then in your controller you do this
#products = current_user.products_for_gender
in your view you then render a list with those products. This prevents you from putting logic in your view, which is rarely a good idea.
Also there's more abstraction possible, the if statement is not the prettiest, but this will cover your issue i believe. Eventually you could look into using service objects for example, https://blog.engineyard.com/2014/keeping-your-rails-controllers-dry-with-services
How are you saving the users model. If you are using devise then you have current_user helper method available in your views and you can use that to get the curren_user.gender attribute and show the view based on this
I'm trying to hide parts of my views depending on the User role.
So let's say I want only admins to be able to destroy Products. Besides the code in the controller for preventing regular users from destroying records, I would do the following in the view:
<% if current_user.admin? %>
<%= link_to 'Delete', product, method: :delete %>
<% end %>
The previous code works, but it's prone to errors of omission, which may cause regular users to see links to actions they are not allowed to execute.
Also, if I decide later on that a new role (e.g. "moderator") can delete Products, I would have to find the views that display a delete link and add the logic allowing moderators to see it.
And if there are many models that can be deleted only by admin users (e.g. Promotion, User) maitenance of all the ifs would be pretty challenging.
Is there a better way of doing it? Maybe using helpers, or something similar? I'm looking for something maybe like this:
<%= destroy_link 'Delete', product %> # Only admins can see it
<%= edit_link 'Edit', promotion %> # Again, only admins see this link
<%= show_link 'Show', comment %> # Everyone sees this one
I found these two questions that are similar to mine, but none of them answered my question:
Show and hide based on user role in rails
Ruby on Rails (3) hiding parts of the view
I strongly recommend pundit.
It allows you to create "policies" for each model. For your Product model you might have a ProductPolicy that looks something like this
class ProductPolicy < ApplicationPolicy
def delete?
user.admin?
end
end
In your view you can do something like this
<% if policy(#post).delete? %>
<%= link_to 'Delete', product, method: :delete %>
<% end %>
If later on you want to add a moderator role, just modify the policy method
class ProductPolicy < ApplicationPolicy
def delete?
user.admin? || user.moderator?
end
end
So I kind of figured a way to move the IFs out of the view. First, I override the link_to helper in my application_helper.rb:
def link_to(text, path, options={})
super(text, path, options) unless options[:admin] and !current_user.admin?
end
Then on my views I use it as:
<%= link_to 'Edit Product', product, admin: true, ... %>
This prevents regular users from seeing admin links, but for other html tags with content inside, such as divs, tables etc., an if would still be needed.
CanCan is another gem that lets you define "Abilities" per user role.
In views you can use something like if can? :delete, #post to check if the
user may delete that specific post.
Using the CanCan and Role gems, what is still needed is a way to Check The Route and see if "current_user" has permissions to access that Route based on their role(s) - then show/hide based on that.
This saves the user clicking on things and getting told they cannot see it - or us having to write per-item "if" logic specifying what roles can see what list-items (which the customer will change periodically, as roles are changed/refined) around every single link in one's menu (consider a bootstrap menu with 50+ items nested in groups with html formatting, etc), which is insane.
If we must put if-logic around each menu-item, let's use the exact same logic for every item by checking the role/permissions we already defined in the Ability file.
But in our menu-list, we have route-helpers - not "controller/method" info, so how to test the user's ability to hit the controller-action specified for the "path" in each link?
To get the controller and method (action) of a path (my examples use the 'users_path' route-helper) ...
Rails.application.routes.recognize_path(app.users_path)
=> {:controller=>"users", :action=>"index"}
Get just the controller-name
Rails.application.routes.recognize_path(app.users_path)[:controller]
=> "users"
Ability uses the Model for its breakdown, so convert from controller name to it's model (assuming default naming used) ...
Rails.application.routes.recognize_path(app.users_path)[:controller].classify
=> "User"
Get just the action-name
Rails.application.routes.recognize_path(app.users_path)[:action]
=> "index"
And since the "can?" method needs a Symbol for the action, and Constant for the model, for each menu-item we get this:
path_hash = Rails.application.routes.recognize_path(app.users_path)
model = path_hash[:controller].classify.constantize
action = path_hash[:action].to_sym
Then use our existing Abilty system to check if the current_user can access it, we have to pass the action as a symbol and the Model as a constant, so ...
<% if can? action model %>
<%= link_to "Users List", users_path %>
<% end %>
Now we can change who can see this resource and link from the Ability file, without ever messing with the menu, again. But to make this a bit cleaner, I extracted out the lookup for each menu-item with this in the app-controller:
def get_path_parts(path)
path_hash = Rails.application.routes.recognize_path(path)
model_name = path_hash[:controller].classify.constantize
action_name = path_hash[:action].to_sym
return [model_name, action_name]
end
helper_method :get_path_parts
... so I could do this in the view (I took out all the html-formatting from the links for simplicity, here):
<% path_parts = get_path_parts(users_path); if can?(path_parts[1], path_parts[0]) %>
<%= link_to "Users Listing", users_path %>
<% end %>
... and to make this not take all day typing these per-menu-item if-wraps, I used regex find/replace with capture and wildcards to wrap this around every list-item in the menu-item listing in one pass.
It's far from ideal, and I could do a lot more to make it much better, but I don't have spare-time to write the rest of this missing-piece of the Role/CanCan system. I hope this part helps someone out.
I'm trying to build a condition based on wether or not a "user" is a "member". Basically I need a way of checking if the current_user.id matches any of the user_id of any members. The non-working code I have right now is:
<% if current_user = #page.members %>
you can view this content.
<% end %>
I'm looking for something along the lines of: "If current_user.id exists in the "user_id" of any members."
Something like this, based on the field names in your question:
<% if #page.members.map(&:user_id).include? current_user.id %>
You can view this content
<% end %>
Assuming your #page.members variable contains an array, you can use the include? method:
<% if #page.members.include? current_user %>
you can view this content.
<% end %>
If you're using an array of ids, you will of course need to change the test slightly to look for the current user's id:
<% if #page.members.include? current_user.id %>
you can view this content.
<% end %>
#member_ids = #page.members.map{|m| m.id()}
then check for the condition as below
#memeber_ids.include? current_user.id()
Has said before include? should do the thing.
I'm just answering to tell you about a gem called CanCan, that gives you easy access for authorization "helpers".
Why you should use CanCan instead of doing what you are actually doing?
Don't reinventing the weel most of the times it's a goob practice.
You are placing business logic on the view (bad practice).
CanCan most likely has been developed thinking on security, and all the best practices in mind.
You save some developing hours.
Sorry if I repeated myself.