Instantiate instance variable in helper method from controller - ruby-on-rails

I have a helper which instantiates a model and renders a form. This form should be available to any view in the application
# support_form_helper
def support_form
#support_stats = SupportForm::Stat.find(get_stats_id)
#enquiry = SupportForm::Enquiry.new(stats_id: #support_stats.id)
render partial: 'support_form/enquiries/form'
end
And its rendered in the view:
# some_view.html.erb
<%= support_form %>
This is fine until I want to submit the form and validate it in the controller.
# enquiries_controller.rb
def create
#enquiry = SupportForm::Enquiry.new(params[:support_form_enquiry])
topic = #enquiry.topic
#stat = SupportForm::Stat.find(#enquiry.stats_id)
#stat.stats[topic] = #stat.stats[topic].to_i.next
respond_to do |format|
if #enquiry.valid? && #stat.save
format.html { redirect_to(root_path) }
else
format.html { redirect_to(:back) }
end
end
end
This is where I can't render the previous view with the errors attached to the invalid object. The helper gets invoked again and initializes a new #enquiries object, without the errors obviously.
How can I render the form in many views across an application and still return to the view with the object together with errors when it is invalid?
I found an answer which answers my question but its a bad idea:
Render the action that initiated update
def create
#enquiry = SupportForm::Enquiry.new(params[:support_form_enquiry])
topic = #enquiry.topic
#stat = SupportForm::Stat.find(#enquiry.stats_id)
#stat.stats[topic] = #stat.stats[topic].to_i.next
if #enquiry.valid? && #stat.save
redirect_to(root_path)
else
render Rails.application.routes.recognize_path(request.referer).values.join("/")
end
end
The problem is that there will likely be instance variables in the view that submitted the form and I would have to be able to instantiate all the instance variable in the application then.....not possible.
Currently I'm considering putting the errors in the flash hash... not something I want to do. With the original object returned i can repopulate the fields with the users input.

When you use redirect_to, rails will kick off a whole new controller & view sequence. Use
render "path/to/template/from/view/folder"`
instead.
A typical create action using this pattern would look like (for a 'post' object in this case):
def create
#post = Post.new(params[:post])
#created = #post.save
respond_to do |format|
if #created
flash[:notice] = 'Post was successfully created.'
format.html { redirect_to post_path(#post) }
format.js
else
format.html { render :action => :new }
format.js
end
end
end
Notice how if it's successfully created we do a full redirect to the "show" page for the post, but if it's not successful we just do a render.
You should probably modify your support_form helper so that it only creates a new #enquiry if it hasn't been created already:
def support_form
#support_stats = SupportForm::Stat.find(get_stats_id)
#enquiry ||= SupportForm::Enquiry.new(stats_id: #support_stats.id)
render partial: 'support_form/enquiries/form'
end
||= is shorthand for "equals itself or". If it hasn't been defined (or is nil or false) then it will fail the first part of the or and pass through to the second, where the object is created.
In your form partial, also, you should make sure you're using form_for, which will submit to the create or update action depending on whether the object has been saved already.

Related

How does rails populate forms?

Let's say I create a scaffold:
rails g scaffold Cat name:string age:integer
and I add a presence validation on the Cat model's age attribute:
validates :age, presence: true
When I attempt to create a cat via the form, and put in the cat's name but purposely leave out the cat's age the controller bounces me back to the form but that cat's name is still present in the name field!
How is this happening?
I would have thought the
#cat = Cat.new
would replace all of the invalid cat's attributes. Maybe if it were #cat ||= Cat.new I could understand that more.
Also, how can I make this behaviour happen in a more complex rails app? I have a simple forum where topics has_many replies. I create my new replies via a form in my topic show view:
topic#show:
#reply = Reply.new
topic/show.html.erb:
<%= form_for [#toplic, #reply] do |f| %>
<%= f.text_field :name placeholder: 'Create a new name...' %><br>
<%= f.text_area :description, placeholder: 'Create a new description...', rows: 5 %><br>
<%= f.submit 'Create Discussion' %>
<% end %>
While everything works perfectly, when I purposely leave out a reply's name, though I am redirected back to the form and an error flash shows, my form is completely empty. All of the attributes have vanished? Why is this?
The key to understanding how this works is to realize that in the case of a form failure, the controller action is not rerun, but rather the template is rendered using the existing state from the action.
In a typical Rails scaffold, your create action will look like this
def create
#cat = Cat.new(cat_params) # instance variable is initialized with the form values
if #cat.save
redirect_to #cat, notice: 'Success!'
else
# in the case of form failure, we will re-render the 'new' template
# this will NOT rerun the entire 'new' action, thus the #cat variable
# will still maintain the values from the form that we gave it above
render 'new'
# note the difference if we had instead done a redirect_to; this would
# cause the CatsController#new action to be re-run which would reinitialize
# the #cat variable according to the code within the 'new' action
# redirect_to new_cat_url
end
end
For your more complex example, you'll want to follow the same procedure, making sure you just re-render the form and don't redirect to another action (which will cause the state to be lost).
# TopicsController
def show
#topic = Topic.find(params[:id])
#reply = Reply.new
end
# RepliesController
def create
#reply = Reply.new(reply_params) # init the var with the form values
if #reply.save
redirect_to #topic, notice: 'Success!'
else
# this is the key - we need to re-render the template of the previous action
# in this case, it would be the TopicsController#show template
render 'topics/show'
# Remember - if we instead do a redirect_to #topic, then we will lose the form
# values which are currently set in the #reply variable.
end
end
In short, make sure you recognize when you are redirecting to a new action versus just re-rendering a template.
One important GOTCHA to be aware of when re-rendering a template is that you must make sure that all the instance variables which exist for the controller action are available when you render the template.
For example,
# TopicsController
def show
#topic = Topic.find(params[:id])
#reply = Reply.new
#foo = Foo.new
end
# RepliesController
before_action :set_topic
def create
#reply = Reply.new(reply_params)
if #reply.save
# ...
else
# we need to remember to set up a #foo variable here otherwise it will be undefined
# when used within the 'show' template
#foo = Foo.new
render 'topics/show'
end
protected
def set_topic
#topic = Topic.find(params[:topic_id])
end
OK, so, you go go /cat/new. Rails' route for this URL runs the method CatsController#new, which renders the new.html.erb template. You put in your data, then hit submit. The action for this form is to POST to /cats, which runs the CatsController#create method. This method does this following:
#cat = Cat.new(cat_params)
It then tries to save the Cat. If it succeeds, it redirects you to the Cat's URL. If not, it re-renders the new.html.erb template. That's where the name comes from — the CatsController#update method creates its Cat from the values you put into the original form.
For a typical scaffold create action:
# POST /products
# POST /products.json
def create
#product = Product.new(params[:product])
respond_to do |format|
if #product.save
format.html { redirect_to #product, notice: 'Product was successfully created.' }
format.json { render json: #product, status: :created, location: #product }
else
format.html { render action: "new" } #will re-submit
format.json { render json: #product.errors, status: :unprocessable_entity }
end
end
end
The magic happens in the render method! which will submit the previous POST request (won't go back to the new action while a redirect will do), this way the submited values are still there. (check this SO question for more details)
That's said, this behavior relies on following the convention, however sometimes you need to give it a hand specially with some inputs (e.g selects, checkboxes, radio) might require extra setup using selected or value options

Rails redirect_to in controller to correct page

I have several models with has_many :attachments.
I'm trying to redirect back to the Note view after the Note is created.
This is the attachments controller code I'm trying. The #note tells me that this attachment is related to that Note.
# GET /attachments/new
# GET /attachments/new.json
def new
#attachment = Attachment.new
#comment = params[:comment_id]
#note = params[:note_id]
respond_to do |format|
format.html # new.html.erb
format.json { render json: #attachment }
end
end
# POST /attachments
# POST /attachments.json
def create
#attachment = Attachment.new(params[:attachment])
respond_to do |format|
if #attachment.save
if #note != nil
format.html { redirect_to note_path(#note), notice: 'Attachment was successfully created.' }
else
format.html { redirect_to attachments_path, notice: 'Attachment was successfully created.' }
end
But, #note is nil by the time the create code happens.
Thanks for the help!
As a rule, you probably won't see "new" and "create" blocks executed in the same context. That's a bit of a mouthful, so lets be a bit more specific: the variables you declare in "new" won't still exist when "create" is called. So, any variables you want to use in "create" must be declared there as well.
One thing you can do (depending on the code) is share a block between different controller methods that initialized these variables for you. For example:
before_filter :initialize_vars, only: [:new, :create]
...
def initialize_vars
#note = params[:note_id]
end
The "before_filter" will execute the "initialize_vars" method before any new request is sent to the "new" or "create" methods.
More generally, this relates to a pretty important Rails concept (and server-side web engineering in general) - that there is very little "state" within the server. The server takes a request, processes it, and forgets about it. Everything that's needs to be remembered must be stored in the server, or somehow communicated by the request the user sends.

Save referenced resource on update_attributes (create nested resource on edit)

I have something like issue tracking system where there are issues and they have some comments.
Now on one page I want to give user an option to edit some stuff of "issue" as well as add a comment. Editing of and issue is a standard stuff like in /edit but also I want to create a comment and validate if it's not blank.
I've figured out that I can build a comment and make a form for it, but how should I check simultaneously that both issue attributes and comment attributes are valid? Because each update should be followed by a new comment, but I don't want to create a new comment if the issue attributes are no valid.
I would approach this by first adding fails_validation? methods to both your Issues and Comments models to check for problems.
Second, you will have to manually load the #issue form data from params[] and validate it BEFORE you save it (can't use update_attributes(params[:issue]).) Create a new Comment and load it via params[]. Then you can test the validation on both models and go back to the edit action if either fails.
If both pass you can save #issue and then #comment as normal.
def update
#issue = Issue.find(params[:id])
# manually transfer form data to the issue model
#issue.title = params[:issue][:title]
#issue.body = params[:issue][:body]
#...
#comment = #issue.comments.new(params[:comment])
# validate both #issue and #comment
if #issue.fails_validation? || #comment.fails_validation?
flash[:error] = "Your edits or your comment did not pass validation."
render :action => "edit",
end
# validation passed, save #issue then #comment
respond_to do |format|
if #issue.save
#comment.save
format.html { redirect_to #issue, notice: 'Issue successfully updated. Comment created' }
format.json { head :ok }
else
format.html { render action: "edit" }
format.json { render json: #issue.errors, status: :unprocessable_entity }
end
end
end
Not the most elegant solution, but it should work.
You can validate the comment model and the issue model in their respective classes.
It is not clear to me whether you are using 'accepts_nested_attributes_for' in Issue for comments. If you are, then the standard IssueController#update will not save the record if issue is invalid and consequently, it will not create the comment records as well.
Here is the standard IssueController#update:
class IssueController < ApplicationController
def update
#issue = Issue.find(params[:id])
if #issue.update_attributes(params[:issue])
redirect_to issues_path, notice: 'issue updated'
else
render action: 'edit'
end
end

New to jQuery/Ajax how can I add a user relation to creating a new review?

I have been trying to get to grips with jQuery and been following a railscast on adding an Ajax add review form, which works fine but I would now like to add into it the ability for a review to belong to a user as well as a venue.
Reviews controller
def create
#review = Review.create!(params[:review])
#review.venue = #venue
if #review.save
flash[:notice] = 'Thank you for reviewing this venue!'
respond_to do |format|
format.html { redirect_to venue_path(#venue) }
format.js
end
else
render :action => :new
end
end
views\reviews\create.js.erb
$("#new_review").before('<div id="flash_notice"><%= escape_javascript(flash.delete(:notice)) %></div>');
$("#reviews_count").html("<%= pluralize(#review.venue.reviews.count, 'Review') %>");
$("#reviews").append("<%= escape_javascript(render(:partial => #review)) %>");
$("#new_review")[0].reset();
I have tried changing the controller to:
def create
#review = #current_user.reviews.create!(params[:review])
#review.venue = #venue
if #review.save
flash[:notice] = 'Thank you for reviewing this venue!'
respond_to do |format|
format.html { redirect_to venue_path(#venue) }
format.js
end
else
render :action => :new
end
end
but it just wont submit, with no errors.
I think I have the models set correctly with belongs_to and has_many, I think this is a controller issue I'll add other code bits if needed.
Development log
NoMethodError (undefined method `reviews' for nil:NilClass):
app/controllers/reviews_controller.rb:14:in `create'
Thanks for any help!
It appears that your error is residing with #current_user. According to your development log, #current_user is nil when you call #current_user.reviews on it. I would say track down where this #current_user instance variable is being set and find out why it is nil. Now, what kind of authentication are you using? Most authentication plugins, especially those used by Ryan Bates of the Railscasts you mentioned, use a local variable, say just current_user, as the means to access the currently signed in user. I know I do in all my code.
So, rewrite the line as
#review = current_user.reviews.create!(params[:review])
and see if that works. If it doesn't, change it back and then track down where this #current_user is being set. Chances are good it is being set in a before_filter :method_name at the beginning of your controller.
Calling create! (with exclamation mark) will throw an exception and thus abort your create action if saving fails. Check your log/development.log for these exceptions.
Use build instead of create and lose the exclamation mark.
def create
#review = #current_user.reviews.build(params[:review])
#review.venue = #venue
if #review.save
flash[:notice] = 'Thank you for reviewing this venue!'
respond_to do |format|
format.html { redirect_to venue_path(#venue) }
format.js
end
else
render :action => :new
end
end

Ruby on Rails when create method fails, render loses local variables

Hey guys I have a simple create method with some validations and whenever the create method fails due to validation errors it re-renders the 'new' action.
The problem is in my new action/view I have a local variable that is established in the action and passed to a partial to render some related information to what the user is creating.
Now when my create action fails and I try to re-render the 'new' action I'm getting the always awesome
undefined method `cover' for nil:NilClass
error.
What is the best way to handle re-establishing my action's local variables on a render instead of redirecting to the action again and the user losing the data they input?
For some clarification. Here is some sample code:
#controller.rb
def new
#summary = User.find(params[:user_id])
#page = Page.new
end
def create
#page = Page.new(params[:page])
if #page.save
redirect_to #page
else
render :action => 'new'
end
end
in my new.html.erb file i have something like this
<%= #summary.cover %>
#page form etc...
When you create the object and attempt to save it, the object still holds the values and the validation errors, so pass it on into the render. Usually it is named the same in your create method as it is in your new method, so the template just works.
if #my_object.save
flash[:notice] = "Successfully created."
redirect_to ....
else
render :action => 'new' #assuming new.html.erb uses #my_object
end

Resources