In order to drastically reduce code repetition, I want to write up a concern with a generic way to add a special around_action to a controller. It is basically supposed to catch any exception, render the right template and add the exception as a notice. However, it must be applicable to different actions, and show different templates depending on the action. My goal is basically to be able to do this:
protect_from_exception_with 'index', only: [ :update ]
In order to achieve this, I tried to write up my concern like this (Using Rails 4.1):
module CatchException
extend ActiveSupport::Concern
module ClassMethods
def protect_from_exception_with(failure_template, params)
around_action -> { catch_exception_with(failure_template) }, params
end
end
private
def log_error(e)
# Many things happen here
end
def catch_exception_with(failure_template)
yield
rescue => e
log_error(e)
render failure_template
end
end
However, this leads to an error:
LocalJumpError: no block given (yield)
I have trying to find examples for around_action or around_filter with a parameter, but could only find them for before_action.
I hope what I'm trying to achieve is at all possible, otherwise I'd need to write a new method in every controller for every action I need to achieve this.
There are some clues:
around_action receives a callback and a block as params, if we send a function as the 1st param, that function mustn't have any parameter!
We can send a block instead (like you did) but we must pass the current given block to that block as well, your code misses the passing block, that is why the exception raised.
In protect_from_exception_with, I can call block_given?, it returns true, but I don't know how to get the block out!
This works:
module CatchException
extend ActiveSupport::Concern
module ClassMethods
def protect_from_exception_with(failure_template, params)
around_action -> { catch_exception_with(failure_template) }, params
end
end
private
def log_error(e)
# Many things happen here
end
def catch_exception_with(failure_template)
self.send(params[:action])
rescue => e
log_error(e)
render failure_template
end
end
Thankfully, we still have the params in catch_exception_with, make it easy, call the action back to the controller!
Related
I have a requirement to need to validate presence of some params in certain situations. Here is the example of that :
In my user controller, for update action, I am required to validate the presence of these params. Same deal for car controller, update action as well, you could see recurring theme here. Params are additional_info.
My base controller provides additional_info_params which pulls the right data from the request.
Here is what I tried so far. I created a AR controller concern and included it in the controller, here is some code:
module ClassMethods
def require_additional_info_for(*methods)
binding.pry
return unless methods.include?(action_name)
if additional_info_params.empty?
head 400
end
end
end
My idea was to be able to define methods that require these params on the top of controller file, just like before_action from rails or skip_authorization_check from cancan. Like so:
MyController < BaseController
include Concerns::AdditionalInformation
require_additional_info_for :update
def update
...
end
end
This code above however does not work as I intended, mainly because this fires on the request class without much knowledge about the request (where I need to derive action name from via action_name).
So how can I do something like this?
Yes, you can, but i suggest you to use the before_action callback!
In a 'abstract' controller, register your method like this:
class SameController < ApplicationController
...
protected
def require_additional_params
render status: :unprocessable_entity if additional_info_params.empty?
end
end
After this, all the controllers who will use this methods, must extends SameController, and runs before_action passing the above method for the wanted actions, for example:
class UserController < SameController
before_action :require_additional_params, only: [:action1, :action2]
end
Note: You can put the require_additional_params in a module and include in your controller, or just put it in the ApplicationController
You might also look at making these regular strong params in the respective controller. It looks something like this:
def update_params
params.require(:car).permit(:engine, :wheels, :rims).tap do |car_params|
car_params.require(:engine)
end
end
This would expect a top-level :car key params (which it strips), and require an :engine param, but allow the other 2 (:wheels and :rims). If :engine isn't present, it will raise a ActionController::ParameterMissing (just like if :cars was missing)
This is straight from the action controller strong params docs (last example at bottom)
I'll sometimes throw these into separate private methods on the respective controller, so there would also possibly be a create_params method with different requirements. I prefer this method over using a custom method as a before_action.
Let's imagine I have a class
class Test < ActiveRecord::Base
include AuthenticatorHelper
def test
authenticate_or_fail!
puts "If I fail, this should be unreachable"
end
end
and
module AuthenticationHelper
def authenticate_or_fail!
#user = User.find(params[:token])
unless #user
render :json => {code: 401, :err => 'Unauthorized'} and return
end
end
end
What I want to do is either authenticate or reply with a json msg. However, it will obviously ignore my return statement due to nesting and it will always print my message
If I fail, this should be unreachable
Regarding the question
You could extract the call into a before_filter/before_action (based on the rails version).
class Test < ActiveRecord::Base
include AuthenticatorHelper
before_action :authenticate_or_fail!
def test
puts "If I fail, this should be unreachable"
end
end
Please see the documentation for further details.
Because your helper method renders in case of a failure, rails will prevent the test method to be called. You will not need the and return part then, which would only have returned from the method anyway and as such was a NoOp.
Apart from the question but also noteworthy:
I don't want to point out errors for the sake of it. I just want to prevent the OP from running into a series of bugs later on.
User.find(params[:token])
Will raise an exception if no record is found. Because of that, the unless #user part will not be evaluated in case of an invalid token. You could use
User.find_by(id: params[:token])
instead.
Your class which looks like it acts as a controller is named Test and inherits from ActiveRecord::Base. The first is unusual as TestsController would be more along the lines of rails and the seconds looks plain wrong. A controller has to inherit from ApplicationController (which itself inherits from ActionController::Base)
According to the JSON API specification, we should use a filter query parmeter to filter our records in a controller. What the filter parameter actually is isn't really specified, but since it should be able to contain multiple criteria for searching, the obvious thing to do would be to use a hash.
The problem is, it seems like I'm repeating myself quite often in controller actions for different types of records.
Here's what things look like for just a filter that includes a list of ids (to get multiple specific records).
def index
if params[:filter] and params[:filter][:id]
ids = params[:filter][:id].split(",").map(&:to_i)
videos = Video.find(ids)
else
videos = Video.all
end
render json: videos
end
For nested property checks, I guess I could use fetch or andand but it still doesn't look dry enough and I'm still doing the same thing across different controllers.
Is there a way I could make this look better and not repeat myself that much?
Rather than using concerns to just include the same code in multiple places, this seems like a good use for a service object.
class CollectionFilter
def initialize(filters={})
#filters = filters
end
def results
model_class.find(ids)
end
def ids
return [] unless #filters[:id]
#filters[:id].split(",").map(&:to_i)
end
def model_class
raise NotImplementedError
end
end
You could write a generic CollectionFilter as above, then subclass to add functionality for specific use cases.
class VideoFilter < CollectionFilter
def results
super.where(name: name)
end
def name
#filters[:name]
end
def model_class
Video
end
end
You would use this in your controller as below;
def index
videos = VideoFilter.new(params[:filter]).results
render json: videos
end
Here is my take on this, somewhat adapted from Justin Weiss' method:
# app/models/concerns/filterable.rb
module Filterable
extend ActiveSupport::Concern
class_methods do
def filter(params)
return self.all unless params.key? :filter
params[:filter].inject(self) do |query, (attribute, value)|
query.where(attribute.to_sym => value) if value.present?
end
end
end
end
# app/models/user.rb
class User < ActiveRecord::Base
include Filterable
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
# GET /users
# GET /users?filter[attribute]=value
def index
#users = User.filter(filter_params)
end
private
# Define which attributes can this model be filtered by
def filter_params
params.permit(filter: :username)
end
end
You would then filter the results by issuing a GET /users?filter[username]=joe. This works with no filters (returns User.all) or filters that have no value (they are simply skipped) also.
The filter is there to comply with JSON-API. By having a model concern you keep your code DRY and only include it in whatever models you want to filter. I've also used strong params to enforce some kind protection against "the scary internet".
Of course you can customize this concern and make it support arrays as values for filters.
you can use Rails Concerns to drying up ...
##================add common in app/models/concerns/common.rb
module Common
extend ActiveSupport::Concern
# included do
##add common scopes /validations
# end
##NOTE:add any instance method outside this module
module ClassMethods
def Find_using_filters (params)
Rails.logger.info "calling class method in concern=======#{params}=="
##Do whatever you want with params now
#you can even use switch case in case there are multiple models
end
end
end
##======================include the concern in model
include Common
##=======================in your controller,call it directly
Image.Find_using_filters params
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.
My question is about controller methods (possibly included from an outside class) that work with instance variables. I frequently use a before_filter in controllers to set up certain variables, e.g.:
class DocumentController < ApplicationController
before_filter :fetch_document
def action
#document.do_something
end
private
def fetch_document
#document = Document.find(params[:id])
end
end
I've been working on a project in which a few controllers will share some functionality, say, document editing. My first thought was to extract the relevant methods, and get them from application_controller.rb or a separate module. But then I noticed I was writing code that looks like this:
def fetch_document
#document = Document.find(params[:id])
end
def do_something_to_document
#document.do_something
end
This sets off warning bells: do_something_to_document is essentially assuming the existence of #document, rather than taking it as an argument. Is this, in your sage opinions, a bad coding practice? Or am I being paranoid?
Assuming it is an issue, I see two general approaches to deal with it:
Check for the instance var and bail unless it's set:
def do_something_to_document
raise "no doc!" unless #document
[...]
end
Call the action with the instance var as an argument:
def do_something_to_document(document)
[...]
end
2 looks better, because it hides the context of the calling object. But do_something_to_doc will only be called by controllers that have already set up #document, and taking #document as a method argument incurs the overhead of object creation. (Right?) 1 seems hackish, but should cover all of the cases.
I'm inclined to go with 1 (assuming I'm right about the performance issue), even though seeing a list of methods referencing mysterious instance vars gives me hives. Thoughts? Let me know if I can be more clear. (And of course, if this is answered somewhere I didn't see it, just point me in the right direction...)
Thanks,
-Erik
If you really need document in different controllers, I'd do something like this:
class ApplicationController < ActionController::Base
private
def document
#document ||= Document.find(params[:document_id])
end
end
class FooController < ApplicationController
before_filter :ensure_document, :only => [:foo]
def foo
document.do_something
end
private
# TODO: not sure if controller_name/action_name still exists
def ensure_document
raise "#{controller_name}##{action_name} needs a document" unless document
end
end
As #variable are session/instance variable you will get a Nil exception in do_something_to_document method.
The first code is fine, because before_filter will always load your #document.
I suggest you to write something like that
def fetch_document(doc_id)
#document ||= Document.find(doc_id)
end
def do_something_to_document
my_doc = fetch_document(params[:id])
end
where do_something_to_document is in the controller (if not, dont use params[:id], even if you know you can access this global, use another explicit parameter). The ||= thing, will asssure that you call the base only once by request.