how can I DRY up my controller actions? - ruby-on-rails

Say I have the following controller:
class FooController < ApplicationController
def show
end
def a
foo = Foo.find(params[:id])
foo.a
redirect_to foo_url(foo)
end
def b
foo = Foo.find(params[:id])
foo.b
redirect_to foo_url(foo)
end
def c
foo = Foo.find(params[:id])
foo.c
redirect_to foo_url(foo)
end
end
Is there anyway to get a after_filter to perform the shared redirect code?

Try this:
class FooController < ApplicationController
def show
end
[:a, :b, :c].each do |name|
define_method(name) do
foo = Foo.find(params[:id])
foo.send(:name)
redirect_to foo_url(foo)
end
end
end

An after_filter will not work in this situation.
I would use the following approach.
class FooController < ApplicationController
before_filter :get_foo, :only => [:a, :b, :c]
def show
end
def a
do_and_redirect(:a)
end
def b
do_and_redirect(:b)
end
def c
do_and_redirect(:c)
end
private
def get_foo
#foo = Foo.find(params[:id])
end
def do_and_redirect(method_name)
#foo.send(method_name)
redirect_to foo_url(#foo)
end
end

Here's your code refactored:
class FooController < ApplicationController
before_filter :get_foo, :except => [:show]
def show
end
def a
#foo.a
redirect_to #foo
end
def b
#foo.b
redirect_to #foo
end
def c
#foo.c
redirect_to #foo
end
private
def get_foo
#foo = Foo.find(params[:id])
end
end

I haven't tried it, but http://rails.rubyonrails.org/classes/ActionController/Filters/ClassMethods.html#M000319 says you can use the after_filter.

Leave it where it is. after_filters won't work in this case.

Related

Rails Getting Error uninitialized constant ApplicationController (NameError)

I'm getting this error of ApplicationBaseController where it is throwing up a name error. Is there anything wrong in both of these codes because ruby is throwing errors even though i'm inheriting from the applicationbasecontroller in the application controller
class ShoppingController < ApplicationController
def index
#categories = Category.all()
end
def new
end
def showcollection
#products =Product.where(category_id: params[:id])
#productcount = Product.where(category_id: params[:id]).count
end
def create_product
#product = Product.new(product_params)
if (#product.save())
redirect_to shopping_index_path
else
render 'new'
end
end
private def product_params
params.require(:product).permit(:name,:price, :category_id, :image)
end
end
class ApplicationController < ActionController::Base
end
Also here is my directory structure
enter image description here

How to routing to concern throug controller

To have a cleaner code I want to split my controller in some concerns.
In my routes.rb how to redirect to concern without redefine the methods of concern index show destroy create ...
class SomeController
include SomeConcern
def index
end
end
module SomeConcern
def index
end
end
Sorry for my bad english.
Lets say we have a CarsController and AirplanesController that have the typical create and new actions.
class AirplanesController < ApplicationController
def new
#airplane = Airplane.new
end
def create
#airplane = Airplane.new(create_params)
if #airplane.save
redirect_to #airplane
else
render :new
end
end
# ...
end
class CarsController < ApplicationController
def new
#car = Car.new
end
def create
#car = Car.new(create_params)
if #car.save
redirect_to #car
else
render :new
end
end
# ...
end
To dry this up we can extract the shared code to a module:
module Createable
extend ActiveSupport::Concern
included do
attr_accessor :resource
alias_attribute :self.controller_name.to_sym, :resource
end
def new
#resource = resource_class.new
yield #resource if block_given?
end
def create
#resource = resource_class.new(create_params)
if #resource.save
yield #resource if block_given?
redirect_to #resource
else
render :new
end
end
private
def create_params
raise "not implemented controller!"
end
def resource_class
#resource_class ||= self.controller_name.classify.constantize
end
end
We can then apply it to the controller classes by:
class CarsController < ApplicationController
include Createable
def create_params
params.require(:car)
.permit(:model) # ...
end
end
class AirplanesController < ApplicationController
include Createable
def create_params
params.require(:airplane)
.permit(:model) # ...
end
end
But a very important point here is that you are not routing to the module. The module is providing methods to the controller class.
You have to always map to your controller. Concerns are modules where you can put shared logic (it makes sense only in case you need 2 absolutely similar methods in 2 different controllers).
I think, that such code should work:
class SomeController
include SomeConcern
end
module SomeConcern
def index
end
end
Isn't it?
But concerns mostly used to move out some private helper methods from controller, rather actions as we do in this code piece

Scope for nested resources using pundit resolve method

I am referring to my own question Rails Nested Resources with Pundit Allowing Index and finally came up with a working solution but is there not any much better solution defining scope.where(?) or scope.select(?) in the property_policy? How to get all the properties that only belongs to one specific deal using the pundit resolve method?
What I finally did :
properties_controller.rb
class PropertiesController < ApplicationController
before_action :set_deal, except: [:index, :all]
before_action :set_property, only: [:show, :edit, :update, :destroy]
def all
#properties = Property.all
authorize #properties
end
def index
#deal = Deal.find(params[:deal_id])
#properties = policy_scope(Deal)
end
def set_deal
#deal = Deal.find(params[:deal_id])
# pundit ######
authorize #deal
###############
end
(...)
end
property_policy.rb
class PropertyPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all if user.admin?
end
def all?
user_is_admin?
end
def user_is_admin?
user.try(:admin?)
end
(...)
end
What I'd like better:
properties_controller.rb
def index
#deal = Deal.find(params[:deal_id])
#properties = policy_scope(Property) # => for # #properties = #deal.properties
authorize #deal
end
and in the property_policy.rb something like
def resolve
# scope.where(???) if user.admin? # only an admin user can see the #deal.properties
# or any other solution using scope
end
As a reminder 1 deal has many properties and 1 property belongs to one specific deal. My routes are nested deals/id/properties except for the full list of properties I have simple "/properties". Thanks a lot for helping.
** UPDATE **
I finally went for
properties_controller.rb
def index
#deal = Deal.find(params[:deal_id])
#properties = policy_scope(#deal.properties)
authorize #properties, :index?
end
and in property_policy.rb
class PropertyPolicy < ApplicationPolicy
class Scope < Scope
def resolve
user.admin? ? scope.all : scope.none
end
end
def index?
user_is_admin?
end
def user_is_admin?
user.try(:admin?)
end
end
Not sure if it is the proper way
What you want to do is pass a scope to the policy - not just a class.
#properties = policy_scope(#deal.policies)
class PropertiesPolicy
class Scope < Scope
def resolve
user.admin? ? scope.all : scope.none
end
end
end
Another problem with your controller is that authorize #deal will call DealsPolicy#index? which is not what you want.
To authorize an index action you want to call authorize with the model class (and not an instance):
def index
authorize Property # calls PropertiesPolicy#index?
#deal = Deal.find(params[:deal_id])
#properties = policy_scope(#deal.properties)
end
In that case you don't have to do anything special in your Scope#resolve method really. Just return scope since you can assume at that point that the user is an admin.

Rails: No Pundit policy found in Rails

I have used the Pundit Gem before, but I've never tried doing what I'm trying to do now, and for some reason Pundit is not happy.
What I'm aiming to do, is to have a modal with the 'create' (Foo) form on my 'index'(Foos) page. Thus I need to instantiate an empty Foo object for the modal form to work.
The issue that I'm experiencing, is that Pundit throws an error when I submit the form remotely. The error is:
Pundit::NotDefinedError - unable to find policy of nil
I have tried to understand why this is happening but I've not been able to solve it yet.
Here is my foos_controller.rb#index:
...
def index
#foo = Foo.new
authorize #foo, :new?
#foos = policy_scope(Foo)
end
...
I then have the following 'before_action' filter that runs for my other actions i.e. 'create'
...
before_action :run_authorisation_check, except: [:index]
def run_authorisation_check
authorize #foo
end
...
The policies that I'm using in foo_policy.rb:
....
def index?
user.has_any_role? :super_admin
end
def create?
user.has_any_role? :super_admin
end
def new?
create?
end
def scope
Pundit.policy_scope!(user, record.class)
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
if user.has_any_role? :super_admin
scope.all
end
end
end
....
The error does not present itself until I submit the form. Could anybody familiar with Pundit please help guide me to understand what I'm doing incorrectly?
UPDATE
Full foos_controller.rb
class FoosController < ApplicationController
def index
#foo = Foo.new
authorize #foo, :create?
#foos = policy_scope(Foo)
end
def new
#foo = Foo.new
end
def create
#foo = Foo.new(foo_params)
respond_to do |format|
if #foo.save
flash[:notice] = I18n.t("foo.flash.created")
format.json { render json: #foo, status: :ok }
else
format.json { render json: #foo.errors, status: :unprocessable_entity }
end
end
end
private
before_action :run_authorisation_check, except: [:index]
def foo_params
params.fetch(:foo, {}).permit(:bar)
end
def run_authorisation_check
authorize #foo
end
end
Yeah, you're not setting the value of #foo, that's why you're getting the error unable to find policy of nil.
Most times, you would have something like this in your foos_controller.rb:
before_action :set_foo, only: [:show, :edit, :update, :destroy]
before_action :run_authorisation_check, except: [:index]
...
private
def set_foo
#foo = Foo.find(params[:id])
end
Let me know if that works
I had this issue when working on a Rails 6 API only application with the Pundit gem.
I was running into the error below when I test my Pundit authorization for my controller actions:
Pundit::NotDefinedError - unable to find policy of nil
Here's how I solved:
Say I have a policy called SchoolPolicy:
class SchoolPolicy < ApplicationPolicy
attr_reader :user, :school
def initialize(user, school)
#user = user
#school = school
end
def index?
user.admin?
end
def show?
user.admin?
end
def new
create?
end
def edit
update?
end
def create
user.admin?
end
def update?
user.admin?
end
def destroy?
user.admin?
end
end
Then in my SchoolsController, I will have the following:
class Api::V1::SchoolsController < ApplicationController
before_action :set_school, only: [:show, :update, :destroy]
after_action :verify_authorized, except: :show
# GET /schools
def index
#schools = School.all
authorize #schools
render json: SchoolSerializer.new(#schools).serializable_hash.to_json
end
# GET /schools/1
def show
render json: SchoolSerializer.new(#school).serializable_hash.to_json
end
# POST /schools
def create
#school = School.new(school_params)
authorize #school
if #school.save
render json: SchoolSerializer.new(#school).serializable_hash.to_json, status: :created, location: api_v1_school_url(#school)
else
render json: #school.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /schools/1
def update
authorize #school
if #school.update(school_params)
render json: SchoolSerializer.new(#school).serializable_hash.to_json
else
render json: #school.errors, status: :unprocessable_entity
end
end
# DELETE /schools/1
def destroy
authorize #school
#school.destroy
end
private
# Use callbacks to share common setup or constraints between actions.
def set_school
#school = School.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def school_params
params.require(:school).permit(:name, :alias, :code)
end
end
Note:
I used an after_action callback to call the verify_authorized method to enforce authorization for the controller actions
I did not call the authorize method on the show action because it was skipped for authorization by me out of choice based on my design.
The instance variables called by the authorize method corresponds to the instance variable of the controller actions being called. So for the index action it is #schools and for the create action it is #school and so on.
That's all.
I hope this helps

Pass all class methods to another function in Ruby on Rails

I have made the following validation method:
def if_admin(&block)
if #current_user.administrator?
yield
else
redirect_to '/go_away'
end
end
and i find my classes are increasingly looking like:
class Foo < ApplicationsController
def index
if_admin do
.....
end
end
def show
if_admin do
.....
end
end
def new
if_admin do
.....
end
end
def edit
if_admin do
.....
end
end
.......
end
I want to know if there is anything similar to before_action which would pass the method into the if_admin method, thus DRYing up the code?
Just like you wrote, there is before_action. You can use it like this:
class Foo < ApplicationsController
before_action :if_admin
# ...
private
def if_admin
redirect_to '/go/away' unless #current_user.administrator?
end
end

Resources