Currently my navigation partial is as given below.
- if (can? :manage, Invoice) and (can? :manage, InvoiceItem)
%li{class: is_active?(["invoices", "invoice_items"])}
= link_to invoices_path do
%i.icon-file-text
Invoices
I see that the can method that define ability can accept an array. Is there a way to make the can? helper method more concise?
UPDATE: I have written a small application helper to sort this out. Is this the best way?
def user_can?(actions, resources)
actions.each do |action|
resources.each do |resource|
return false if cannot? action, resource
end
end
true
end
And in the partial:
- if user_can? [:manage], [Invoice, InvoiceItem]
%li{class: is_active?(["invoices", "invoice_items"])}
= link_to invoices_path do
%i.icon-file-text
Invoices
can? only evaluates one action and one resource at a time as you noted. Your helper is fine, assuming that you don't need to check different combinations of actions and resources, which you typically don't do in a single conditional statement in a view.
You probably also want to consider why you really need to check for :manage access to InvoiceItem when deciding whether to display a list of Invoices.
Related
I currently have a very simple form for search written in HAML:
%form.search{ method: 'get', action: '/users/search' }
...
What would be the correct rails conventions for rendering a different search route based on the model that the controller sets in an instance variable when rendering this view?
I found this blog post, but this code <%= form_tag(recipes_path, :method => "get" is not generic enough for me. I would like to set this value, recipes_path, based on the model that the controller is collaborating with when it renders this view. The search form could be used across multiple controllers with their own search action. My app can search on different pages for different models.
I can definitely come up with a way to do it, but I would like to know the 'right' way or I suppose the 'rails' way of dynamically setting the form action to a different controller action based on the data that the form will be searching against.
I don't know what the 'right' or 'rails' way of doing this is. (But, it sure isn't hand-crafting a form with %form.)
In my apps, I tend to only have one form partial that looks something like this:
app/views/widgets/form
- #presenter = local_assigns[:presenter] if local_assigns[:presenter]
= form_tag #presenter.form_path, remote: true, id: #presenter.form_id, class: #presenter.form_classes, data: #presenter.form_data, method: #presenter.form_method do
= #presenter.form_inner
In my presenter_base.rb file, I have something like this:
class PresenterBase
def render_partial
render(partial: "#{file_name}", locals: {presenter: self})
end
def render_form
render_partial 'widgets/form'
end
end
So, to render the form in a FooPresenter, I might do something like:
class FooPresenter < PresenterBase
def present
render_form
end
def form_path
some_form_path(and: :maybe, some: :query_params)
end
def form_id
'my-cool-form'
end
def form_classes
'some cool classes'
end
def form_data
{some: :form, data: :here}
end
def form_method
:post
end
def form_inner
...
end
end
Naturally, there's more to it than just that (like, how I get a plain old ruby object to render). But, that should give you a sense of one way of doing it.
A simple way if there are no complications and you follow the conventions, can be something like this
%form.search{ method: 'get', action: "/#{controller_name}/search" }
so if you are in users_controller, it will print "users", if you are in static_pages_controller, it will show "static_pages" and so on.
I'm trying to create a generic breadcrumbs method in my application controller to assign the breadcrumbs based on the current controller. If I wanted the breadcrumbs for the index of 'Thing', I would need in the view:
<%= breadcrumb :things, things %>
And for edit or show:
<%= breadcrumb :thing, thing %>
Where things is a method in the things controller that returns all things, and thing is a method returning the relevant thing.Both are exposed, and I have in my application layout:
<%= breadcrumb crumb, crumb_resource %>
And in my application controller:
def crumb
return controller_name.singularize.to_sym if edit_or_show_action
controller_name.to_sym
end
def crumb_resource
resource = controller_name
resource = controller_name.singularize if edit_or_show_action
end
def edit_or_show_action
action_name == 'edit' || 'show'
end
This obviously returns a string for crumb_resource, rather than the call to the controller method. From what I can find I believe it has something to do with send, however
controller.send(resource)
obviously doesn't work. How can I convert the string that is returned into a controller method call?
If you're using Gretel, then I think what you might be looking for is this:
def crumb_resource
resource = controller_name
resource = controller_name.singularize if edit_or_show_action
self.instance_variable_get("##{resource}")
end
This is assuming you have stored the relevant resource into #resource_name during the edit/show/index action.
I accepted the answer given as I'm assuming it works for people using instance variables to access models in their view, however in the end this worked for me:
breadcrumb crumb, eval(crumb_resource)
where eval evaluates the string, basically reverse interpolation which sounds pretty cool.
I have a next method in the model.
def self.next(comment, key = :id)
self.where("#{key} > ?", comment.send(key)).first
end
In my view I can say for example: (does not work)
= link_to "next", #comment.next(#comment)
What's the correct way to call this method?
routes.rb:
Rails.application.routes.draw do
resources :articles do
resources :comments do
end
end
end
You've defined next as a class method (vs an instance method), so you need:
= link_to "next", Comment.next(#comment)
If you want to be able to call #comment.next, define the instance method as:
def next(key = :id)
Comment.where("#{key} > ?", self.send(key)).first
end
It is not good style that the model knows this, you should put it in the controller. You should try a gem called kaminari, this gem lets you paginate over the elements, so in your comments controller you could have something like:
def show
#comment = Comment.order(id: :asc).page(params[:page]).per(1)
end
Then in your view, by just adding this kaminari helper:
<%= paginate #comment %>
You get the pagination bar below and everything works fine (gem's magic).
If you don't like this you could try to add that next method in the controller or find both next and current elements and link to the next element.
In my opinion the model is just a class that knows how to save and get information from the database and maybe some calculations with it's information, so all that logic related to the view should be elsewhere.
I have an application with Post model with associated PostsController and Admin::PostsController under the admin namespace in routes. Controllers share the same index action with controller concern, similar to this approach. I'm using a shared view partial under the shared/posts/_posts_list to list all posts on site and also in the admin dashboard. All this is working as I expected.
I'm asking what is the best approach to add for instance: edit post button only for admin user, that view doesn't get bloated with conditionals like <% if current_user.admin? %> to display this edit button.
If you don't want to put conditionals in your view go for two
separate views (one for regular users, one for admins).
Use conditionals in view and
include necessary elements depending on is_admin?. This is the shortest way to go if the only conditional is is_admin? check, imho. If there are more conditionals (roles, etc.) you are right, this will bloat views. You can check redmine (it is a big, open source rails project) and see that it uses conditionals in views too. Check its base layout here.
You can use helpers to generate elements depending on is_admin?. Lets say you need to show menu elements in your view. You can generate menu elements in your helper. And you can call the helper from your view. Such as:
view.html.erb
<ul>
<%= main_menu_items("main_menu", current_user) %>
</ul>
some_helper.rb
def main_menu_items(menu_name, user = current_user)
allowed_items(menu_name, user).map { |menu_item| content_tag(:li, menu_item) }.join
end
def allowed_items(menu_name, user)
menus = {
main_menu: ["Menu Item 1", "Menu Item 2"],
main_menu_admin: ["Menu Admin Item 1"]
}
menu_to_return = menus[menu_name.to_sym]
menu_to_return += menus[(menu_name + "_admin").to_sym] if user.admin?
end
This is just some quick code. If you want to use it in production, you must make some heavy changes (refactoring, getting items from models, nil checks, etc).
I'd say you've got two approaches:
The one you mentioned. Your view shouldn't get too bloated, and this is the one I'd go with.
You could use the current_user.admin? Check to put an "is_admin" CSS class on your page body. Then, you can add "admin_only" classes to your buttons etc, and use CSS to hide the relevant buttons and stuff when no "is_admin" class exists.
Either approach is fine, but 1 is a bit simpler and immediately more obvious to anyone reading the code, so I'd start with that. Kinda depends on how many admin-only features your UI will have though... If there'll be heaps, option 2 might be worth it.
You need an authorization library(https://github.com/CanCanCommunity/cancancan)
Gem install
gem 'cancancan', '~> 1.10'
rails g cancan:ability
config
class Ability #model/ability.rb manage roles
include CanCan::Ability
def initialize(user) # user is devise current_user
if user.blank?
else
if user.role_id == 1 # role_id 1 is admin
can :manage, Post #Post is you model
end
end
end
view
<% if can? :update, #post %>
<%= link_to "Edit", edit_post_path(#post) %>
<% end %>
controller
class ArticlesController < ApplicationController
load_and_authorize_resource
def index
#posts = Post.accessible_by(current_ability).order("id ASC") #posts is filtered by current_user's roles
end
def show
# #post is already loaded and authorized
end
end
What is your solution to the problem if you have a model that is both not-nested and nested, such as products:
a "Product" can belong_to say an "Event", and a Product can also just be independent.
This means I can have routes like this:
map.resources :products # /products
map.resources :events do |event|
event.resources :products # /events/1/products
end
How do you handle that in your views properly?
Note: this is for an admin panel. I want to be able to have a "Create Event" page, with a side panel for creating tickets (Product), forms, and checking who's rsvp'd. So you'd click on the "Event Tickets" side panel button, and it'd take you to /events/my-new-event/tickets. But there's also a root "Products" tab for the admin panel, which could list tickets and other random products. The 'tickets' and 'products' views look 90% the same, but the tickets will have some info about the event it belongs to.
It seems like I'd have to have views like this:
products/index.haml
products/show.haml
events/products/index.haml
events/products/show.haml
But that doesn't seem DRY. Or I could have conditionals checking to see if the product had an Event (#product.event.nil?), but then the views would be hard to understand.
How do you deal with these situations?
Thanks so much.
I recommend you to make separate admin controller with it's own views to administrate everything you want. And your customer's logic stayed in products contoller.
I don't have good and clean solution for this problem. Usualy if views doesn't differ to much, I use single view and add some code like #product.event.nil?. You can always add some variable, or helper that will make this method shorter, on example has_event? - then your view will look cleaner. And use it in code like this:
<% if has_event? %>
some html
<% end %>
or for single line:
<%= link_to 'Something special', foo_path if has_event? %>
On the other side, you can create few partials that are the same for both views, put them in some folder, on example /shared/products/... and render them from your views like this:
<%= render :partial => '/shared/products/info' %>
and so on.
But if they don't differ too much, I really would use if version.
The views will be handled by the ProductsController. You can alter the logic in your controller depending on the nesting of the resource.
# app/controller/products_controller.rb
# ...some code...
def index
#event = Event.find_by_id(params[:event_id]) if params[:event_id]
#products = #event ? #event.products : Product.all
end
The view will be handled by the usual product view
# app/views/products/index.html.haml
- unless #products.blank?
- #products.each do |product|
%p= product.some_attribute