I have this setup:
routes.rb:
resources :orders, only: [] do
# many other member actions
# ...
namespace :confirm, module: :orders do
controller :confirm do
get :cash
end
end
end
orders/confirm_controller.rb:
class Orders::ConfirmController < ApplicationController
load_and_authorize_resource class: Order,
id_param: :order_id,
instance_name: :order
def cash
end
end
ability.rb:
can :cash, Order do |order|
order.buyer == user
end
The idea is: since OrdersController already overloaded with almost 20 actions, I want move new actions to nested controller. The code above is working but I feel that this part:
load_and_authorize_resource class: Order,
id_param: :order_id,
instance_name: :order
can be done easier. And ability that I have is for parent controller and not namespaced to nested controller, so if someone define cash method in OrdersController same ability will be used, what is not good. So is there any better way?
Related
I have a Ruby on Rails application that can generate "roles" for actors in movies; the idea is that if a user looks at a movie detail page, they can click "add role", and the same if they look at an actor detail page.
Once the role is generated, I would like to redirect back to where they came from - the movie detail page or the actor detail page... so in the controller's "create" and "update" method, the redirect_to should either be movie_path(id) or actor_path(id). How do I keep the "origin" persistent, i. e. how do I remember whether the user came from movie detail or from actor detail (and the id, respectively)?
I would setup separate nested routes and just use inheritance, mixins and partials to avoid duplication:
resources :movies do
resources :roles, module: :movies, only: :create
end
resources :actors do
resources :roles, module: :actors, only: :create
end
class RolesController < ApplicationController
before_action :set_parent
def create
#role = #parent.roles.create(role_params)
if #role.save
redirect_to #parent
else
render :new
end
end
private
# guesses the name based on the module nesting
# assumes that you are using Rails 6+
# see https://stackoverflow.com/questions/133357/how-do-you-find-the-namespace-module-name-programmatically-in-ruby-on-rails
def parent_class
module_parent.name.singularize.constantize
end
def set_parent
parent_class.find(param_key)
end
def param_key
parent_class.model_name.param_key + "_id"
end
def role_params
params.require(:role)
.permit(:foo, :bar, :baz)
end
end
module Movies
class RolesController < ::RolesController
end
end
module Actors
class RolesController < ::RolesController
end
end
# roles/_form.html.erb
<%= form_with(model: [parent, role]) do |form| %>
# ...
<% end %>
In my PromoCodesController I have this code:
load_and_authorize_resource :restaurant, find_by: :permalink
load_resource :discount, through: :restaurant
load_resource :promo_code, collection: [:create], through: :discount
It should be good since in #index, it loads the collection #promo_codes and in #create it loads #promo_code.
But it does not load the collection #promo_codes in #create. Where is the problem? In the documentation it says:
:collection argument: Specify which actions are resource collection actions in addition to :index.
Thank you
It's not working because Cancancan's method load_resource (controller_resource_loader.rb) assumes
only one resource variable to be set at a time: either resource_instance or collection_instance.
Your load_resource collection: [:create] can load #promo_codes in #create action via monkey patch to CanCan::ControllerResourceLoader:
# config/initializers/cancan.rb
module CanCan
module ControllerResourceLoader
def load_resource
return if skip?(:load)
# Original condition has been split into two separate conditions
if load_instance?
self.resource_instance ||= load_resource_instance
end
if load_collection?
self.collection_instance ||= load_collection
end
end
end
end
The common way in which this patch works is a create form integrated into index action:
class TicketsController < ActionController::Base
load_and_authorize_resource collection: [:create]
def index
#ticket = Ticket.new
end
def create
if #ticket.valid?
#ticket.create_for_user! params[:message]
redirect_to ticket_path(#ticket)
else
# #tickets are also defined here
render :index
end
end
end
Im working on adding voting to my app (like in Stackoveflow). I have two models Questions and Answers so i want to be able to vote for both of them. I see two ways to manage voting for different type of models:
Add concern for models, controllers and routes.
Add votes_controller that can handle voting for any model that has votable.
I`d like to use second way to solve my problem. But to use controller I will should pass two parameters to controller, like: votable_type: model-name, votable-id: object.id, and my route will look like: vote_up_vote_path, vote_down_vote_path.
Is there a way to use routes like: vote_up_path(answer); vote_down_path(question)?
And by passing object "vote_up_path (answer)" i want to be able to get it in controller
P.S. I`m not able to use gems. Gems provide logic for models, I'm already have this logic.
I found the solution. So at first we need to generate Votes controller.
$rg controller Votes
Than we add routes:
resource :vote, only: [:vote_up, :vote_down, :unvote] do
patch :vote_up, on: :member
patch :vote_down, on: :member
patch :unvote, on: :member
end
And add in votes_helper.rb:
module VotesHelper
def vote_up_path(votable)
{controller: "votes", action: "vote_up",
votable_id: votable.id, votable_type: votable.class}
end
def vote_down_path(votable)
{controller: "votes", action: "vote_down",
votable_id: votable.id, votable_type: votable.class}
end
def unvote_path(votable)
{controller: "votes", action: "unvote",
votable_id: votable.id, votable_type: votable.class}
end
end
Than we should add tests and complete our methods. In controller we can use this method to find our votable:
private
def set_votable
klass = params[:votable_type].to_s.capitalize.constantize
#votable = klass.find(params[:votable_id])
end
Highly recommend this gem to handle upvote/downvote:
Acts as votable
There are two ways to do this; the first is to use superclassed controllers, the second is to use an individual controller with a polymorphic model.
I'll detail both:
Superclassing
You can set a method in a "super" controller, which will be inherited by the answers and questions controllers respectively. This is kind of lame because it means you're sending requests to the answers or questions controllers (which is against the DRY (Don't Repeat Yourself) principle):
#config/routes.rb
resources :answers, :questions do
match :vote, via: [:post, :delete], on: :member
end
#app/controllers/votes_controller.rb
class VotesController < ApplicationController
def vote
if request.post?
# Upvote
elsif request.delete?
# Downvote
end
end
end
#app/controllers/answers_controller.rb
class AnswersController < VotesController
# Your controller as normal
end
... Even better ...
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
def vote
...
end
end
#app/controllers/answers_controller.rb
class AnswersController < ApplicationController
....
end
This will allow you send the following:
<%= link_to "Upvote", answers_vote_path(#answer), method: :post %>
<%= link_to "Downvote", questions_vote_path(#question), method: :delete %>
-
The important factor here is that you need to ensure your vote logic is correct in the backend, which you'll then be able to use the vote method in the VotesController to get it working.
Polymorphic Model
Perhaps the simplest way will be to use a polymorphic model:
#app/models/vote.rb
class Vote < ActiveRecord::Base
belongs_to :voteable, polymorphic: true
end
#app/models/answer.rb
class Answer < ActiveRecord::Base
has_many :votes, as: :voteable
end
#app/models/vote.rb
class Vote < ActiveRecord::Base
has_many :votes, as: :voteable
end
This will allow you to do the following:
#config/routes.rb
resources vote, only: [] do
match ":voteable_type/:voteable_id", action: :vote, via: [:post,:delete], on: :collection #-> url.com/vote/answer/3
end
#app/controllers/votes_controller.rb
class VotesController < ApplicationController
def vote
if request.post?
#vote = Vote.create vote_params
elsif request.delete?
#vote = Vote.destroy vote_params
end
end
private
def vote_params
params.permit(:voteable_id, :voteable_type)
end
end
This would allow you to use the following:
<%= link_to "Vote", votes_path(:answer, #answer.id) %>
Have a nested resource as such
class Dealer < ActiveRecord::Base
has_many :vehicles
end
and
class Vehicle < ActiveRecord::Base
belongs_to :dealer
end
below are my routes.
resources :dealers do
resources :vehicles, :except => [:index]
end
resources :vehicles, :only => [:index]
looking at the wiki at the github page for cancan I did the following:
class VehiclesController < ApplicationController
load_and_authorize_resource :dealer
load_and_authorize_resource :vehicle, :through => :dealer
def index
#vehicles = Vehicle.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: #vehicles }
end
end
end
but now when the admin tries to go to the index page with the abilities:
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.has_role? :admin
can :manage, :all
end
end
I get
Couldn't find Dealer with id=
what do i need to change for admin to still be able to do all the actions and yet have others be checked before they can do any action.
The problem is not that he is not authorized to this action. The problem is that CanCan tries to fetch an instance of dealer to load all its vehicles and you have not provided a :dealer_id within params[:dealer_id]. Cancan assumes you would be loading only dealer's vehicles in this controller because you used an load_and_authorieze :through. This authorization should be used within Dealers::VehiclesController.
As long as you only loading vehicles just use load_and_authorize_resource :vehicle. And because load_and_authorize will set #vehicles for you within the before filter there is also no need to load the vehicles explicitly with Vehicle.all.
load_and_authorize is just a convenient method and it assumes some defaults. Once you will come to a point where you have some more complex use case. It will be time to throw away this method and just use CanCan's authorize!, can? and a properly configured Vehicle .accessible_by (for listing) methods.
When using load_and_authorize_resource :vehicle, through: :dealer it expects to receive a dealer_id in the request in order to authorize the dealer.
Since you use except: :index in your routes dealer_id will not be automatically included in the request.
If you don't want to authorize the dealer in the index action you can do something like this (taken from Can Can wiki)
class CommentsController < ApplicationController
load_and_authorize_resource :post
load_and_authorize_resource :through => :post
skip_authorize_resource :only => :show
skip_authorize_resource :post, :only => :show
end
In my application's admin interface, I am using ActiveScaffold for easy record editing:
class Admin::InspectionsController < ApplicationController
require_role :staff
protect_from_forgery :only => [:create, :update, :destroy]
active_scaffold :inspections do |config|
[:create, :delete].each {|a| config.actions.exclude a}
config.actions.exclude :nested
config.update.columns = [ :name, :activity_status, :inspector, :report, :note, :time_window, :inspection_type ]
end
end
In this case, :activity_status and :inspector are association columns, referring to associated objects. In my scaffold, I want the editor to be able to change which object the foreign key points to, but the above config shows this:
I only want the inspector itself to be updated, not its fields!
Changing the column to :inspector_id only allows the ID itself to be edited directly.
What am I doing wrong?
Workaround I've used is to remove all columns from the subform action on the relevant controllers:
class Admin::UsersController < ApplicationController
active_scaffold :users do |config|
#...
config.subform.columns = []
#...
end
end
I don't know if there's a better, more elegant way though…
Use the form_ui method for the column in the Inspections controller:
[:activity_status, :inspector].each do |c|
config.columns[c].form_ui = :select
end