In my Rails 3 app, I have a number of models with a boolean field disabled. In controllers for these models, I use a custom action disable to toggle the disabled field using Ajax.
Example (For client),
# routes.rb
resources :clients do
member do
get :toggle_disable, to: 'clients#disable', as: :disable
end
end
# clients_controller.rb
def disable
#client = Client.find(params[:id])
#client.update_attribute :disabled, !#client.disabled
render 'clients/update_client', format: :js
end
# update_client.js.erb
$('#client-<%= #client.id %>-details').html("<%= escape_javascript(render 'clients/client', client: #client) %>");
I have this code for at least ten resources in my application.
QUESTION
How do I go about DRYing up this code and add actions for these boolean fields dynamically? I could have gone with creating a parent controller or module but I am not sure how will I take care of the views code.
I should be able to do something like this
#clients_controller.rb
class ClientsController < ApplicationController
add_toggle_action :disable
end
Two main ways to share methods:
inheritance: define your action in ApplicationController
mixins: add your method in a module and include the module in the appropriate controllers
Since you want only some controllers to get the method, I'll head towards mixin.
Your controller action must use a view with a full path, not relative, something like:
render '/shared/clien/update', format: :js
Lastly, you'll have to define all routes.
Related
I'm very new to Rails, and I'm a little overwhelmed where I do simple things like create an API call. I've set up a route at /reports which has this controller:
class ReportsController < ApplicationController
#client = # Api-accessing gem
#all_reports = []
def self.request_report
begin
puts "Step 1:"
step1 = #client.request_report(opts = {"max_count" => 1})
step1_result = step1.parse
puts "Done!"
puts step1_result
rescue Excon::Errors::ServiceUnavailable => e
puts "Didn't work"
logger.warn e.response.message
retry
end
end # End request_report
request_report
end
This correctly calls the external API when I first load the /reports route, but when I refresh the page the code isn't re-run.
Perhaps I'm misunderstanding what controllers are used for? Am I meant to be putting this code somewhere else? Or is there a caching issue?
The only public API of controller are the actions which respond to a HTTP request. In your case get "/reports" => "reports#request_report" is a route which corresponds to the action request_report.
However actions are instance methods, not class methods:
class ReportsController
def request_report # self.request_report would make this a class method!
# #todo get reports from somewhere and
# return some sort of response.
end
# any method call here happens when the class is evaluated.
end
You are declaring the action as a class method and then calling it when the ReportsController class is evaluated. Sorry to say but just about everything about your controller is wrong.
The Rails convention would be to call the action index.
Controllers in Rails should only be instantiated by the router (or your test framework). So they are definatly the wrong place to put resuable bits and bobs. If you ever see someone doing ReportsController.new.foo or ReportsController.foo - fire them on the spot.
So where do you put external API calls?
If its a pretty trivial one-off you can place it in private method in your controller.
Some place API calls on the model layer - however that is debatable since ActiveRecord models already are supercharged to the gills with powers and responsibilities.
One solution that has worked well for me is Service Objects. They are easy to test and have a clear single responsibility.
class RequestReportService
def initalize(client)
#client = client
end
def call(opts = {})
begin
return #client.request_report(opts.merge("max_count" => 1))
rescue Excon::Errors::ServiceUnavailable => e
nil
end
end
end
class ReportsController
def index
#reports = RequestReportService.new(#client).call
end
end
To add to #max's excellent answer, you need to appreciate that Rails is based on a stateless protocol (HTTP)...
each request message can [only] be understood in isolation.
This means that if you want to create a set of controller actions, you have to appreciate that each call is going to create a new instance of your classes etc. This, coupled with the idea of a RESTful set of actions, should give you a basis from which to build your functionality.
--
#config/routes
scope constraints: { subdomain: "api" } do
resources :reports #-> http://api.url.com/reports
end
#app/controllers/reports_controller.rb
class ReportsController < ApplicationController
respond_to :json #-> requires "responders" gem
def index #-> instance method
#reports = Report.all
respond_with #reports #-> all reports
end
def show
#report = Report.find params[:id]
respond_with #report
end
end
I'll leave the service object stuff as I have no experience with it.
--
If you're pulling from an external API, you have several considerations:
Calls ideally need to be asynchronous (unless you use multi-threading)
Calls need to be made in the instance method
Your current pattern calls the API on the class, which is why you can't refresh it:
class ReportsController < ApplicationController
#client = # Api-accessing gem
#client is only invoked (I don't know why it works, as it should be a class variable) with the class.
So if you send a new request (which creates an instance of ReportsController), #client is going to be declared that one time.
To get it working correctly, #client needs to be defined with each instance method:
class ReportsController < ApplicationController
def index
#client = # Api-accessing gem
This way, each time you invoke ReportsController#index, a new API call will be made. Might seem trivial, but the data scope is massive.
Finally, you need to read up about MVC (Model View Controller):
This will show you how controllers are meant to be used in Rails applications etc.
Well I actually never seen anyone code like this in a rails controller. Rails is a mvp framework. Controller are use to negotiate between your model and the views. First of all, if you routed correctly to your controller like
get "/reports" => "request_report#reports"
your controller should have a method like the following
def request_report
#client = Client.find(params[:id])
end
And then the controller will render and display the view in your app/views/reports/request_report.html.erb with access to the #client variable you just search from your database.
I am not sure why you are calling the block request_report at the bottom of the page, it just doesn't make sense in a controller. And you certainly don't really need to write self in front of a controller method.
def self.request_report
your code
end
As for where to put your api controller, usually for an api controller, we can create new folders under controllers, so the structure will be like
app/controllers/api/v1/your_api_controller.rb
Then in your_api_controller.rb you will need to add namespace infront of your controller like this.
class Api::V1::ReportsController < ActionController::Base
end
It is the same with your routes, you will add namespace in your route.rb
namespace :api do
namespace :v1 do
get "/reports" => "request_report#reports"
end
end
How I can have a special layout in Ruby On Rails 4? For example, I want to call the show method from the backend and front end. The problem is that I need to identify when to call each layout, for example, when calling the URL domain.com/admin/people/1 I want to call backend layout, but when I call the URL domain.com/people/1, I want to call the layout of the front end.
Create your layout in the layouts directory, ie at layouts/admin.html.erb
Route to separate controllers:
class AdminPeopleController
def show
#do things
render layout: 'admin'
end
end
class PeopleController
def show
#do things
render #default
end
end
And add in your routes file:
namespace :admin do
resources :people, controller: :admin_people
end
resources :people, controller: :people
I have two models:
Student
Classroom
Both of them have an action that does the same exact thing: it shows a report of daily activity. That is:
/students/1
/classrooms/1
Grabs activity for the model in question and displays it on the page.
In an attempt to dry this up, I created a ReportsController which extracts all the common logic of building a report.
If I leave the routes like this:
/students/1/report
/classrooms/1/report
Then I can have the ReportsController#show action look for params for :student_id or :classroom_id to determine which model type it is dealing with (for purposes of querying the database and rendering the correct view).
But I would prefer the URLs to be cleaner, so I also changed my routes.rb file to pass the show action for these models to the reports#show controller action:
resources :students, :classrooms do
member do
get :show, to: 'reports#show'
end
end
This works, but I can no longer depend on params to identify which model to work with and which view to render.
Question: should I parse request.fullpath for the model? Or is there a better way to make a shared controller understand which model it is working with?
Routing both show methods to the same controller method for code reuse is somewhat like banging a nail in with a dumptruck.
Even if you can find the resource by looking at the request url you would start splitting the ResortsController into a bunch of ifs and switches even before you got off the ground.
One solution is to add the common action in a module:
module Reporting
extend ActiveSupport::Concern
def show
# the Student or Classroom should be available as #resource
render 'reports/show'
end
included do
before_action :find_resource, only: [:show]
end
private
def find_resource
model = self.try(:resource_class) || guess_resource_class
#resource = model.find(params[:id])
end
# This guesses the name of the resource based on the controller name.
def guess_resource_class
self.class.name[0..-11].singularize.constantize
end
end
class StudentController < ApplicationController
include Reporting
end
# Example where resource name cannot be deduced from controller
class PupilController < ApplicationController
include Reporting
private
def resource_class
Student
end
end
self.class.name[0..-11].singularize.constantize is basically how Rails uses convention over configuration to load a User automatically in your UsersController even without any code.
But the most important key to DRY controllers is to keep your controllers skinny. Most functionality can either be moved into the model layer or delegated out to service objects.
I would put the common logic in the Event Model:
#Event Model
class Event < ...
def self.your_event_method
#self here will be either student.events or classroom.events
#depending on which controller called it
end
end
class StudentsController < ...
...
def show
student = Student.find(params[:id])
student.events.your_event_method
end
end
class ClassroomsController < ...
...
def show
classroom = Classroom(params[:id])
classroom.events.your_event_method
end
end
Can you have non-restful methods in a controller which includes the WickedWizard gem?
Controller:
class Books::BookUpdateController < ApplicationController
include Wicked::Wizard
steps :title_step, :ai_archive_step, :ai_override_step #etc
def show
...
end
def update
...
end
def waterfall
...# loads of code to set up instance variables in the view, which I don't want to have to include in the normal show action for all the wizard steps.
end
end
Routes:
resources :book_update do
member do
get 'waterfall'
... and others
end
end
Version 1 and lower of the gem allows non restful actions, but this commit to solve this PR enforces step names. My error on going to this route http://localhost:3000/book_update/3949/waterfall is
Wicked::Wizard::InvalidStepError in Books::BookUpdateController#waterfall
The requested step did not match any steps defined for this controller.
I suppose I should spark up a new controller and tuck the non restful actions into there, but alternatives would be great.
You need to add:
skip_before_filter :setup_wizard, only: :waterfall
in your wicked controller
I have the following method in my ApplicationController so that I can load unique views depending on the user's privileges. It works fine when I call it within the controller, but when I specify a partial I get the partial's source instead of it rendering it.
class ApplicationController < ActionController::Base
include ControllerAuthentication
private
def render(*args)
options = args.extract_options!
render_options = _normalize_render(*args)
location = logged_in? && current_user.is_admin? ? "admin" : "client"
options[:template] = "/#{location}/#{params[:controller]}/#{render_options[:action] || params[:action]}"
if options[:partial]
options[:partial] = "#{location}/#{params[:controller]}/#{options[:partial]}"
end
super(*(args << options))
end
helper_method :render
end
<%= render partial: "form" %> outputs something like this on the page.
["<form ...>...</form>"]
I've been reading through the source of the render method, but I haven't pinpointed what is causing this. What do I need to change so I can render the partial correctly.
The render method from AbstractController::Rendering has a different behaviour from the one defined in ActionView::Helpers. The fact you're getting an array is normal since the rack stack expects and Enumerable for the body.
With helper_method :render your overriding the implementation of the render method defined in ActionView::Helpers.
I think it would be better to namespace your controllers and extract the common functionality (if any) in a module or a controller that will be used as a base controller for the specific implementation for each role.
for example you could have something like:
namespace :admin do
resources :posts
end
namespace :client do
resources :posts
end
the controllers for this will be located #
app/controllers/admin/posts_controller.rb
app/controllers/client/posts_controller.rb
and the views
app/views/admin/posts/...
app/views/client/posts/...
So basically you'll gonna achieve the same effect but in a more railish manner, and this solves also the view problem.