Does before_action in class override actions defined in concern? - ruby-on-rails

When using concerns in Rails controllers how do filters added via before_action operate? Specifically, if I have the following code for which actions in FeedsController will the before_action set_record run? All of them once (show, update, destroy, foobar), all of them once and twice before destroy or only before destroy, foobar (I presume authenticate still runs only before destroy)?
module JsonApiController
extend ActiveSupport::Concern
included do
before_action :authenticate, only %i[ destroy ]
before_action :set_record, only: %i[ show update destroy ]
end
def show/update/destroy
end
protected
def set_record
#record = controller_path.classify.constantize.find(params[:id])
end
end
class FeedsController < ApplicationController
include JsonApiController
before_action :set_record, only: %i[destroy, foobar]
def foobar
...
end
end
I'd like FeedsController to be able to add the set_record filter before any actions it wants without having to know what the JsonApiController did. So, ideally, the filter :set_record would execute once before each of show/update/destroy/foobar but I don't think that's what the code below accomplishes. But more than any particular solution I want to know how before_action works with concerns and included do so I minimize code duplication between concerns and classes.
If this was just inheritance then I know that before_actions are inherited. But this discussion suggests that a before_action in an included do in a module will be overwritten by one in the class but when I try to look at the source it suggests that append_callback is the default action so I'm confused.

From the Rails Guides: Action Controller Overview/Filters:
Calling the same filter multiple times with different options will not work, since the last filter definition will overwrite the previous ones.
That said if you want to change or update the configuration of a before_action then that new config will override all existing filters with the same method name.
Therefore, in your example, you will need to use the following new declaration to extend (actually override) the existing declaration from the included module:
before_action :set_record, only: %i[show update destroy foobar]

Related

Method not required for GET? If so, is it common not to write it?

I'm new to Rails, and I was thinking that a method should be defined for every single route.
However, /hello_world works as long as I write as below:
Rails.application.routes.draw do
get "/hello_world", to: "hello#world"
end
class HelloController < ApplicationController
# no world
end
# app/views/hello/world.html.erb
hello world!
Is it an expected behavior? If so, is it common not to write it?
You only need a controller action if you have any processing / data retrieval that you need to do before you display the view.
So, yes, the method itself isn't necessarily needed.
You might need to retrieve a record in a show action like this...
class CustomersController < ApplicationController
def show
#customer = Customer.find(params[:id])
end
end
but inn some cases, you might have a before_action that does whatever's neeeded for several methods, so you (again) don't need to specify the action method.
class CustomersController < ApplicationController
before_action :set_customer only: [:show, :edit, :update]
private
def set_customer
#customer = Customer.find(params[:id])
end
end
So this is a case where you might have needed to define a method for the action, but you've now made it unneccessary.

How do I build a controller filter override helper

In Rails you can add before_filters to your controllers. Assuming a base controller has the following:
before_filter :my_base_filter
Given this, assume that you have a whole host of controllers that inherit from this base controller, and that the norm is that this behavior is correct. I now have a small handful of controllers that do the following:
skip_before_filter :my_base_filter, only: [:method1, :method2]
before_action only: [:method1, :method2] do
my_secondary_filter(param1)
end
Given that this code is in a few of my controllers, and that the methods that are passed in (as well as whether or not it uses only or except) are different from one controller to the next, I would ideally like to have a single before_action or before_filter that I could call that would effectively run the above code. The call, in the inherited controller, would ideally look something like:
replace_filter(param1, {only: [:method1, :method2]})
The second parameter (the hash detailing which methods to apply it to) should be able to accept an empty value and apply to all methods. I have created a helper function that (is written alongside these other filters and), syntactically and logically should do this, but can't seem to properly invoke it using a before_action or before_filter without my_base_filter executing first. Is it possible to do something similar to this, and if so, what is the best way to do so?
application_controller.rb
class ApplicationController < ActionController::Base
include ApplicationHelper
before_action :my_base_filter
...
end
inherited_controller.rb
class InheritedController < ApplicationController
# I want to replace these lines with my new helper function
skip_before_filter :my_base_filter, only: [:method1, :method2]
before_action only: [:method1, :method2] do
my_secondary_filter(param1)
end
...
end
application_helper.rb
class ApplicationHelper
def my_base_filter
# Do shit here that is the normal behavior
end
def my_secondary_filter(param1)
# Do shit here that is specific to certain functions INSTEAD
# of running the normal base filter
end
# I want to be able to simply call this function
# as a before_action or before_filter in order
# to DRY up my code
def replace_filter(param1, methods = {})
# Run validation on parameters (including methods) here
# including raising exceptions if necessary
...
# Then run the following
skip_before_filter :my_base_filter, methods
before_action(methods) do
my_secondary_filter(param1)
end
end
end
So... there's this thing in Rails called a Concern. It's intended to be the way you pull out modules of stuff in Rails - while allowing a whole bunch of neato things that you can use.
There's a number of articles out there on what they are and how to use them. I'll let you go explore.
I can't say for certain that the following will fix your problem, but it's how I'd approach it.
Part of your problem is that as you say - by the time you get to running your "replace_filter" method, the bas_filter method has already run.
What you need is to be able to run replace_filter on the first time that your ApplicationHelper is included into the Controller.
This is where ActiveSupport::Concern's included method comes to your aid.
Give something like this a try:
# give this a meaningful name...
class FilterStuff < ActiveSupport::Concern
included do
puts "I'm in included"
# note: not the replace_filter, but the method that will call it
if defined?(:replace_filter_method)
puts "This controller defines replace filter method which I'm now calling"
replace_filter_method
else
puts "this controller does not define the replace method and will default to base behaviour"
before_action :my_base_filter
end
end
def my_base_filter
puts "I'm in my base filter"
end
def my_secondary_filter(param1)
puts "I'm in secondary filter with param: #{param1}"
end
# making this work is another problem...
def replace_filter(param1, methods = {})
puts "I'm in replace filter with: #{param1} and #{methods.inspect}"
# Run validation on parameters (including methods) here
# including raising exceptions if necessary
...
# Then run the following
skip_before_filter :my_base_filter, methods
before_action(methods) do
my_secondary_filter(param1)
end
end
end
class InheritedController < ApplicationController
include FilterStuff
# actions go here...
private
# define this method only on controllers that need it
def replace_filter_method
puts "I'm in this controllers replace filter method"
replace_filter(param1, only: [:method1, :method2])
end
end
I've added a whole bunch of printf debugging - have a run trhough and it'll tell you what is being called when - and that will help you determine what you need to do to get the proper stuff working.

before_filter works differently in development and production modes

I am working on a generic controller to provide default functionality to the RESTful actions, but I am having trouble when I need to add additional routes. The default behavior I am after renders all actions in either an index view, or a show view. My code is setup like this (lots of unrelated code is trimmed);
module RestController extend ActiveSupport::Concern
included do
before_action: setup_index_view,
only: [:index, :new, :create] << #additional_index_routes
before_action: setup_show_view,
only: [:show, :edit:, update, :destroy] << #additional_show_routes
end
def setup_rest_controller(_additional_index_routes,
_additional_show_routes)
#additional_index_routes = _additional_index_routes
#additional_show_routes = _additional_show_routes
end
def setup_index_view
# default behavior, sets #{collection_name} = {Model.collection}
# code trimmed
end
def setup_show_view
# default behavior, sets #{instance_name} = {Model.find(params[:id]}
# code trimmed
end
end
class Order < ApplicationController
def initialize
setup_rest_controller(
_additional_show_routes: [:quote, :contract, invoice_report])
end
def setup_index_view
#override default behavior to filter andpaginate collection
#orders = Orders.active.search(search_terms).paginate
end
#accept default behavior for setup_views
end
In development, this code is working fine (I admit that surprises me a little), but in production the additional routes are not running the setup_show_method. The only difference in the gem file is that development includes rspec-rails. Does anyone know why this code would behave differently?
EDIT
As soon as I hit post, the 'why' hit me. In development the code is reloaded at each request, and in production it is only loaded once...at first load #additional_show_routes is not set, and therefore no additional routes are added to the before_action call. New question then...how do I get the desired behavior?
Adding a call to before_action in the OrdersController overrides the one in RestController and breaks the default functionality.
Adding a call to before_action in setup_rest_controller throws a NoMethodError.
When you add a concern like this, is there a method like new that I can use instead of setup_rest_controller to set #additional_show_views earlier?
This feels like a hack, but it seems to get the desired result;
class OrdersController < ApplicationController
prepend_before_action Proc.new { send :setup_index_view},
only: [:new_restful_action]
def setup_index_view
#override default behavior to filter andp aginate collection
#orders = Orders.active.search(search_terms).paginate
end
#accept default behavior for setup_views
end
I also drop all references to #additional_{X}_routes in RestController. I don't know that prepend_ is required in the code as written above, but in my actual code there are other before_filters that need to run after #additional_{X}_routes...

How to make a class-based controller in Rails?

I am trying to create a generic controller in Rails, to approach a similar functionality than the one offered in class-based views in Django. It would mean that just by making a subclass of this controller you would acquired all the methods generated by default with scaffold.
The code generated by scaffold in Rails is equal except for the name of the resource. Given an small example and based on the code generated by scaffold:
class ResourceController < ApplicationController
before_action :set_#{resource_under_scored}, only: [:show, :edit, :update, :destroy]
def index
##{resource_under_score_pluralized} = #{resource_model}.all
end
...
Given that definition I could write the following controller for resource big shoe:
class BigShoesController < ResourceController
end
Which would imply in the following code auto generated at runtime:
class BigShoesController < ApplicationController
before_action :set_big_shoe, only: [:show, :edit, :update, :destroy]
def index
#big_shoes = BigShoe.all
end
I still need to learn a lot of metaprogramming to achieve this but I though that there are more people who has wanted to acquired the same result. But I have not found the proper question.
How would you accomplish this ? I am looking for a way to implement the class, to see how it would be made to generated code based on variables. It is preferred this answer than a gem which will make the work.
It seems that you want InheritedResources: https://github.com/josevalim/inherited_resources.
It implements all the basic CRUD stuff. Example:
class ProjectsController < InheritedResources::Base
end
This controller has index, show, create, update and other methods, implemented in a standard manner. There are some possibilities for customization, see the readme file.

Rails Variable across all controller actions

This should be a very simple rails question. I have a variable like the following.
#administration = Administration.first
I want this variable to be accessible through every controller action across all my controllers so for example if I have a Product controller and inside of it I have the usual CRUD actions, I want the #administration variable as defined above to be put into all the CRUD actions. (It would not be needed in destroy or create or update). I have many controllers throughout my project and I was wondering if there is an easier way than adding it manually through all of the actions that I want it in.
I tried a global variable
$administration = Administration.first
but I run into an issue where it is not updated when I update the actual content of the Administration.first table. Also, I would like to avoid global variables.
Any help would be much appreciated. Thanks! :)
You could add a before_filter to your ApplicationController that sets the administration variable before any action is called and you can limit it to only the actions you require.
class ApplicationController < ActionController::Base
...
before_filter :set_admin
def set_admin
#administration = Administration.first
end
..
http://api.rubyonrails.org/v2.3.8/classes/ActionController/Filters/ClassMethods.html
Just extending Christos post...
If you don't want #administration to be accessible to destroy, create and update controller actions then add :except => :action to before_filter like this:
before_filter :set_admin, :except => [:create, :update, :destroy]
On Rails 4 and 5 before_filter it's deprecated. You can use this instead:
before_action :set_admin, except: [:create, :update, :destroy]

Resources