How do I refactor a conditional statement? - ruby-on-rails

I'm on Rails 4 and Im using devise for user management. I have a simple conditional statement in a show view like this
<% if user_signed_in? %>
<%= link_to "Checkout", user_cart_path(current_user,#cart), method: :patch %>
<% else %>
<%= link_to "Checkout", signin_path %>
<% end %>
I want to clean up my view and add make the conditional a method.
I have tried adding the method
def checkout_link(current_user)
if user_signed_in?
link_to "Checkout", user_cart_path(current_user, self), method: :patch
else
link_to "Checkout", signin_path
end
end
to my cart model, and replaced the conditional in my show cart view to
<%= #cart.checkout_link(current_user) %>
When I load the cart show page, I get an error
undefined method `user_signed_in?' for #<Cart:0x007fd015525228>
What am I doing wrong?
Thanks.

The thing you are doing wrong is concerning your model with the view. This should not be done.
You should place the checkout_link in a view helper method. This will clean up your view, and keep the model only concerned about the data it contains.
Though, in this case, I would just have a single link that leads to the checkout, and if the user is not signed in, have a before_filter redirect them to sign in.

You have two options, you can either add to the view helper or use a decorator.
If you add the method to the view, it would look something like this:
def checkout_link(cart)
if user_signed_in?
link_to "Checkout", user_cart_path(current_user,cart), method: :patch
else
link_to "Checkout", signin_path
end
end
"The decorator wraps the model, and deals only with presentational concerns. In the controller, you decorate the article before handing it off to the view."1 If you would like to use a decorator, check out Ryan Bates' Railscast on them: using an existing gem or creating a decorator from scratch.
1 Quoted from the Draper gem's README. https://github.com/drapergem/draper

You're almost there. You want your checkout_link method to be a "helper method" in a helper file, not in your model file. So, create a file named app/helpers/carts_helper.rb and move your checkout_link method there. Methods in helper files are available to views and controllers.
When I was just learning my way around Rails, I would do:
rails generate scaffold Dummy
specifically to see what files it generated, including helper files. This might be a good place to start.

Related

Why can't find be used in the view as it is used in the controller?

I'm currently going through Michael Hartl's railstutorial.org, and have run into a question that Google/Stackoverflow don't seem to be answering: The tutorial has us display user information in the view by putting <%= #user.name %>, <%= #user.email %> in show.html.erb. In the controller, it has us define a show method: #user = User.find(params[:id]).
I understand why this works, but what I don't understand is why the following code does not produce the same result (I removed the show method from the controller and tried to place all the code in the view). These are my editions to show.html.erb:
<%= #user.find(params[:id]).name %>, <%= #user.find(params[:id]).email %>
It returns "undefined method `find' for nil:NilClass".
I'm sure you understand by now from the other answers and probably the tutorial that you should not have this logic in the view, HOWEVER, to answer your question:
The reason why <%= #user.find(params[:id]).name %> generates the error
undefined method 'find' for nil:NilClass is because #user is an instance variable which you deleted from the show method in the controller. So now, #user is nil and doesn't exist. Also, when you want to query the database you must use the class (model) name which is User instead of #user.
So whiles its bad practice to do this, if you wanted to, you could do <%= User.find(params[:id]).name %> (as long as its a correct id) and that would work. Your params are available in the view as they are in the controller too.
Finally, if you intend to use the show view, you cannot completely delete the show method, you must have at least:
def show
end
in order for it to work.
params is probably not available in the view, and it shouldn't.
Your problem is the main reason why you should do your data preparation in the controller: to catch issues with the data. That's the reason a controller exists.
Your code would make two requests to the database instead of one, if it wouldn't be for rails side query caching.
Prepare your #user in the controller and access it in the view. It will make testing and refactoring your code way easier and greatly improve readability.
Compare
<%= User.find(params[:id]).name %>, <%= User.find(params[:id]).email %>
VS.
<%= #user.name %>, <%= #user.email %>
You really shouldn't have a lot of logic in the views, but I think the code you are looking for is:
<%= User.find(params[:id]).name %>, <%= User.find(params[:id]).email %>

Manage user permission with an instance variable or in a helper

I'm looking for the cleanest way to handle a user permission. It would be used to define if a menu option can be displayed (menu is present in all views) and the access to a page.
So I was wondering which is the cleanest way to do it.
Set an instance variable in each action from the controller validating if the user had access
Add a method in the application helper validating each time it is call if the current user have access
You can define method in your ApplicationController who will check current user permissions. And you can use that method in before_action callback for those actions you need it.
I would recommend to look at cancancan gem (it's community driven support of cancan gem)
Using it it's easy to authorize actions and check abilities to decide show menu item or not.
You can also check out RailsCast about that subject to get understanding of whole idea.
Are you trying to implement an administrator or something similar? I think the cleanest way would be to just make a new column in the users table, which is initialized to false for most, but to true if the user is an admin (or something else). Then you can just make two partials to handle the two cases.
In that case, in your menu view (in your layout or whatnot) you would have this code or something similar:
<% if current_user.admin? %>
<%= render 'admin_page' %>
<% else %>
<%= render 'user_page' %>
<% end %>
Where I assume you define #current_user in your controller, or if you are using Devise, this is handled automatically.
Edit: Yes I endorse the earlier answer, CanCan is a good gem to handle these things also, you should consider using it. In such a case your code would look something like:
<% if can? :update, #user %>
# Edit something
<%= link_to edit_profile_path(#user), class: 'user' do %>
Edit your profile
<% end %>
<% end %>

How to hide parts of the view given a user role on Rails 4

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.

Model Method Not Working in Another's View

I am working on an application that involves "follow/unfollow" functionality. Users can follow Objects and Objects can have many Users following them. It's a has_many :through relationship via a Relationships model/controller.
I have the following snippet in the object#show view:
<% if current_user.following?(#object) %>
<%= render 'unfollow' %>
<% else %>
<%= render 'follow' %>
<% end %>
When testing various functionalities in a request spec, it shows undefined method 'following?' for nil:NilClass and fails all of the object#show specs.
The following? method is in the User model and looks like this:
def following?(object)
relationships.find_by_object_id(object.id)
end
The method following? is in the User model (since they are the only ones doing following and unfollowing). I thought you could use methods between objects in Ruby, but perhaps not. If not, how would I go about refactoring this to be able to use that method?
Thanks in advance for any help!
There is no current_user helper in specs. That's why you get nil.
You should stab test user into current_user variable

link_to problem

I want to display product count in a link_to, the link_to is a part of partial displayed in application.erb.html, the problem is, I have a method in my application controller named products_on_cart which return products count, when I try this code:
<%= link_to "<%= products_on_cart%>", :controller=>"carts", :action=>"index"%>
rails give me an error:
"syntax error, unexpected '>'
...er=>"carts", :action=>"index"%>"
I don't really understand why, can somebody help me?
You can't use <%= .. %> inside of <%= .. %>.
<%= link_to products_on_cart, [:carts] %>
You're nesting ERb tags. Make sure products_on_cart() is available as a helper method, then rewrite your link_to code without nested ERb tags as follows:
<%= link_to products_on_cart(), :controller => "carts", :action => "index" %>
To make products_on_cart() a helper method, either move it to app/helpers/application.rb, or declare it as a helper in your controller:
def products_on_cart()
# method definition goes here
end
helper_method :products_on_cart
If you only need to access products_on_cart from your views and not from your controllers, putting it in app/helpers/application.rb is the preferred way to go. If you need to use it in both controllers and views, use the helper_method approach above instead.

Resources