The magic of class methods for a Ruby on Rails controller and their configurations
Have you used gems that provide class methods for Ruby on Rails controllers that extend these controllers?
Like the ones below:
class TutorialsController < ApplicationController
# This is from cancancan
# It allows to loading and authorizing
load_and_authorize_resource
...
# This is from 'has_scope'.
has_scope :pref_order, only: :index, default: "suggested"
end
Today’s article is about how to implement such a method for our own concern.
My specific case is duplication. I would like to develop a DuplicateResourceConcern that extends the controller and allows users to duplicate a record when they create a new record. This is something we’ve been using at FLLCasts and BuildIn3D for many years, but today I revised the implementation, improved it and decided to share the knowledge in a short article.
The goal is to be able to do:
class TutorialsController < ApplicationController
include DuplicateResourceConcern
enable_duplicate
...
end
With this logic when the user:
- visits /tutorials/new?resource_to_dup=123
- The title field in the form for new tutorial is pre-filled the title of Tutorial 123.
Declare DuplicateResourceConcern and enable_duplicate
First we declare DuplicateResourceConcern
module DuplicateResourceConcern
extend ActiveSupport::Concern
included do
before_action :dup, only: [:new]
end
private
def dup
# call the duplication
end
end
It is a concern with one method that is ‘dup’
We use the concern in the TutorialsController
class TutorialsController < ApplicationController
include DuplicateResourceConcern
def new
end
end
The method ‘dup’ is a private method of the controller. It is called before the :new action. If a param called ‘resource_to_dup=X’ is passed we would like to duplicate the title of Tutorial X.
Simple Tutorial duplication
module DuplicateResourceConcern
extend ActiveSupport::Concern
included do
before_action :dup, only: [:new]
end
private
def dup
original_tutorial = Tutorial.find_by_id(params.permit(:resource_to_dup)[:resource_to_dup])
@tutorial = Tutorial.new(title: original_tutorial.title)
end
end
This duplication will work for the Tutorials controller. But it will not work for ArticlesController, because we are doing “Tutorial.find” and we are assigning a @tutorial variable.
Reusing the DuplicateResourceConcern in a different controller
To reuse the DuplicateResoureConcern we must call Article.find_by_id when we are in an ArticlesController and Tutorial.find_by_id when we are in a TutorialsController.
Here is how we could do this using ‘controller_name’, ‘.classify’ and ‘.constantize’. We can get the name of the records from the controller name.
module DuplicateResourceConcern
extend ActiveSupport::Concern
included do
before_action :dup, only: [:new]
end
private
def dup
# This will return Tutorial for TutorialsController
# and will return Article for ArticlesController
clazz = controller_name.classify.constantize
# If the controller is called 'tutorials' the instance name will be 'tutorial'
instance_name = controller_name.singularize
original_instance = clazz.find_by_id(params.permit(:resource_to_dup)[:resource_to_dup])
duplicated_instance = clazz.new(title: original_instance.title)
# After this call we will be able to access @tutorial in the TutorialsController and @article in the ArticlesController
instance_variable_set("@#{instance_name}", duplicated_instance)
end
end
In this way the DuplicateResourceConcern is not dependent on the type of the object. It could work for Articles and for Tutorials. It is still dependent on the property ‘title’, but I will leave this for now.
How to configure the duplication
The name of the parameter in the url should be ‘resource_to_dup’. What if we want to modify this name on the controller level. For example to have a parameter
‘resource_to_dup‘ for TutorialsController and
‘resource_to_copy‘ for ArticlesController.
What we want to do is pass an argument with the name of the parameter, but we would have to do a little more work.
class TutorialsController < ApplicationController
include DuplicateResourceConcern
enable_duplicate param_name: "my_new_param_name"
def new
end
end
We would use the class_method for the concerns.
module DuplicateResourceConcern
extend ActiveSupport::Concern
included do
before_action :dup, only: [:new]
end
def self.included(base)
base.class_eval do
extend ClassMethods
# Declare a class-level attribute whose value is inheritable by subclasses. Subclasses can change their own value and it will not impact parent class.
# In this way we can extend the class and still have separate configurations
class_attribute :duplicate_resource_configuration, instance_writer: false
self.duplicate_resource_configuration = {
options: {
param_name: "resource_to_dup",
}
}
end
end
class_methods do
# The enable_duplicate method will be called on a class level.
def enable_duplicate(*args) do
options = args.extract_options!
options.symbolize_keys!
# We check that there are only parameters with this names.
options.assert_valid_keys(:param_name)
# Merge the default options with the options passed in the controller
self.duplicate_resource_configuration[:options].merge!(options)
# We mark that we've enabled the duplication
self.duplicate_resource_configuration[:enabled] = true
end
end
private
def dup
# This will return Tutorial for TutorialsController
# and will return Article for ArticlesController
clazz = controller_name.classify.constantize
# If the controller is called 'tutorials' the instance name will be 'tutorial'
instance_name = controller_name.singularize
# Param name. We are no longer dependent on
# the param name being resource_to_dup. It could
# be a different name.
the_param_name = self.duplicate_resource_configuration[:options][:param_name]
original_instance = clazz.find_by_id(params.permit(the_param_name)[the_param_name])
duplicated_instance = clazz.new(title: original_instance.title)
# After this call we will be able to access @tutorial in the TutorialsController and @article in the ArticlesController
instance_variable_set("@#{instance_name}", duplicated_instance)
end
end
Extensibility
This is the basic structure. We can now add more options if we want to and more parameters to the configurations.
What is the system RSpec for duplication.
It is simple.
scenario "/tutorials/new?resource_to_dup can duplicate a resource" do
tutorial.update(title: SecureRandom.hex(10))
visit "/tutorials/new?resource_to_dup=#{tutorial.id}"
expect(page).to have_text "Duplicate from #{tutorial.title}"
click_on "Create Tutorial"
expect(page).to have_text "Tutorial was successfully created."
tutorials = Tutorial.where(title: tutorial.title)
expect(tutorials.count).to eq 2
end
Reply
You must be logged in to post a comment.