Say I'm making a Q&A site like StackOverflow. I have two resources: Question and Answer. I'm using default Rails RESTful resource routes, so each resource has its own controller and methods for creating it.
In the /questions/show view, I want to allow the user to submit an answer for the particular question. The form will POST to /answers, which will get routed as a request to the AnswersController with a call to the create method.
If the answer was created, I can simply redirect back to the original question. However, I'm running into trouble dealing with validation failures on the answer object. I need to render the /question/show view and show the validation errors for the answer object. It's not clear to me how to best do this.
Here are example snippets of what the two controllers might look like.
class AnswersController < ApplicationController
def create
#answer = Answer.new(params[:answer])
if #answer.save
redirect_to #answer.question
else
# What should go here??
end
end
end
class QuestionsController < ApplicationController
def show
#question = Question.find(params[:id])
#answer = Answer.new(:question_id => #question.id)
end
end
What should go in the else clause of the AnswersController's create method? A redirect seems wrong, since the error is really caused by the same request. Calling something like render :template => 'questions/show' seems wrong too, since I have to initialize any instance variables that the template depends on.
This style of having separate actions for calling GET to view the form for creating an object and calling POST to actually create the object seems to work well within a single controller.
How can it be done across controllers?
Try this on for size. It redirects, but passes back the dodgy answer object full of errors.
class AnswersController < ApplicationController
def create
#answer = Answer.new(params[:answer])
# stash the dodgy answer if it failed to save
session[:answer] = #answer unless #answer.save
redirect_to #answer.question
end
end
class QuestionsController < ApplicationController
def show
#question = Question.find(params[:id])
# if we have one stashed in the session - grab it from there
# because it probably contains errors
#answer = session[:answer] || Answer.new(:question_id => #question.id)
end
end
Some details need adding (eg clearing it from the session when done) etc
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
Say for example I have two models, posts and category. Now say I want to make it so the from the category show page you can create a new post using the form_for method. To do this, you will obviously need access to the #category variable and a new instance of a post (#post). Is this acceptable code in the controller?
#app/controllers/categories_controller.rb
def show
#category = Category.find(params[:id])
#post = Post.new
end
Or is it bad practice to have two instance variables defined in the one controller action - and if it is, what would be the best practice for a case like this?
I usually do something like:
#app/controllers/categories_controller.rb
helper_method :category
helper_method :post
def show
end
private
def category
#_category ||= params[:id] ? Category.find(params[:id]) : Category.new(params[:category])
end
def post
#_post ||= Post.new(params[:post])
end
Then, in your views, just refer to post or category (not #post or #_post). The nice thing is you can remove the same logic from your new, delete, etc methods...
Actions related to posts should be in the PostsController as much as possible.
Let's say the user is looking at all posts under the category "rails": /categories/rails
There's a button on that page to create a new post under the "rails" category, href: /posts/new?category=rails
This takes you to PostsController#new where you instantiate a new Post, validate the category param and build a view. This view could either be a new page, or a modal popping up.
I have a Question model that has_many Answers (a model), each of which belongs_to a Question.
For each Question's show page, e.g., /questions/1, or /questions/2, I would like the user to input an answer.
The form is working, but the problem is that it seems the question variable is not carrying through to the answers controller that the form is posting to... thoughts?
class QuestionsController < ApplicationController
def show
#question = Question.find(params[:id])
#answer = #question.answers.build
end
class AnswersController < ApplicationController
def create
#answer = #question.answers.build
redirect_to 'questions/:id'
end
I'm getting an error: undefined method `answers' for nil:NilClass at the #answer = #question.answers.build in the AnswersController create method.
Is it time to use a global variable?
By the way, short version is that I'm trying to do a Quora/ Stack Exchange clone. Is there a tutorial that's already done this because man that would be amazing...
And so it shouldn't. This is definitely not the place for a global variable either.
When you're creating an answer for a question, it should post to something like /questions/2/answers, which should point to the AnswersController#create action.
In that action, you can load up the correct question record using params[:question_id] (which comes from the URL) and then create the answer for that question.
(you want create and not build in your controller, and also your redirect is incorrect)
I have a text_area form to create a quick Idea object. This form appears on many parts of the site so I move it as a shared/_idea_form.html.haml file.
I'm currently defining the new object in every controller action where the form is present with #ideas = Idea.new to make it work.
Is there a more convenient/global way define the #ideas = Idea.new outside each action? I.e. as a before_filter in the application controller. I'm not sure if that would be the right approach to this.
you can put it directly in view
<%= form_for #idea ||= Idea.new do |f| %>
#your text area
<%end%>
If you have it on most of the actions yes, that should be a good way. If was me in your place I think I would brake the rules and would do that in the partial... Sometimes rules dosen't make sense and this time is one of it. You just want a form on every page, and so you need to create always a new Idea for the form.. Or do that on the partial or just create the form without helpers.
Just one opinion :)
There are tons of options: using the decent_exposure gem (try it, it's cool!), using before_filters for setting the value of the #idea, manually creating new Idea in your form, defining some helper which will provide your form with a prepared Idea.
in app/controllers/application_controller.rb:
class ApplicationController < ActionController::Base
before_filter :create_new_idea
def create_new_idea
#idea = Idea.new
end
end
That will set #idea to a new object on every request. There must be some cases where you don't want #idea set to a new instance or even set at all. In that case there are a number of options, here's one:
class ApplicationController < ActionController::Base
before_filter :create_new_idea, lambda {|controller| controller.set_new_idea?}
def create_new_idea
#idea = Idea.new
end
def set_new_idea?
# this should be false in some case
end
end
I need to call the create action in controller A, from controller B.
The reason is that I need to redirect differently when I'm calling from controller B.
Can it be done in Rails?
To use one controller from another, do this:
def action_that_calls_one_from_another_controller
controller_you_want = ControllerYouWant.new
controller_you_want.request = request
controller_you_want.response = response
controller_you_want.action_you_want
end
You can use a redirect to that action :
redirect_to your_controller_action_url
More on : Rails Guide
To just render the new action :
redirect_to your_controller_action_url and return
The logic you present is not MVC, then not Rails, compatible.
A controller renders a view or redirect
A method executes code
From these considerations, I advise you to create methods in your controller and call them from your action.
Example:
def index
get_variable
end
private
def get_variable
#var = Var.all
end
That said you can do exactly the same through different controllers and summon a method from controller A while you are in controller B.
Vocabulary is extremely important that's why I insist much.
You can use url_for to get the URL for a controller and action and then use redirect_to to go to that URL.
redirect_to url_for(:controller => :controller_name, :action => :action_name)
This is bad practice to call another controller action.
You should
duplicate this action in your controller B, or
wrap it as a model method, that will be shared to all controllers, or
you can extend this action in controller A.
My opinion:
First approach is not DRY but it is still better than calling for another action.
Second approach is good and flexible.
Third approach is what I used to do often. So I'll show little example.
def create
#my_obj = MyModel.new(params[:my_model])
if #my_obj.save
redirect_to params[:redirect_to] || some_default_path
end
end
So you can send to this action redirect_to param, which can be any path you want.
Perhaps the logic could be extracted into a helper? helpers are available to all classes and don't transfer control. You could check within it, perhaps for controller name, to see how it was called.
Composition to the rescue!
Given the reason, rather than invoking actions across controllers one should design controllers to seperate shared and custom parts of the code. This will help to avoid both - code duplication and breaking MVC pattern.
Although that can be done in a number of ways, using concerns (composition) is a good practice.
# controllers/a_controller.rb
class AController < ApplicationController
include Createable
private def redirect_url
'one/url'
end
end
# controllers/b_controller.rb
class BController < ApplicationController
include Createable
private def redirect_url
'another/url'
end
end
# controllers/concerns/createable.rb
module Createable
def create
do_usefull_things
redirect_to redirect_url
end
end
Hope that helps.
You can call another action inside a action as follows:
redirect_to action: 'action_name'
class MyController < ApplicationController
def action1
redirect_to action: 'action2'
end
def action2
end
end
Separate these functions from controllers and put them into model file. Then include the model file in your controller.