I have a couple of objects, foo, bar and user.
I have a form for creating a new foo object, which uses simple_fields_for and accepts_nested_attributes_for to create a new child bar object at the same time.
Now, I want to set the current_user as the author attribute for the new bar, but I can't figure out how best to do this. (still new to Rails.)
I have tried the following in my create method within the foo controller:
def create
#foo = Foo.build(params[:foo])
#foo.bars.find(:first).author = current_user
However when I run this I get an exception.
undefined method `author=' for nil:NilClass
Can anyone offer any advice on how best to go about this?
You likely need to build the Bar object under #foo. ie
def new
#foo = Foo.new
#foo.bars.build
end
def create
#foo = Foo.new(params[:foo])
#foo.bars.first.author = current_user
if #foo.save
redirect_to #foo
else
render action: "new"
end
end
A good resource for nested forms: http://railscasts.com/episodes/196-nested-model-form-part-1?view=asciicast
Provided you set accepts_nested_attributes_for in your model and correctly used fields_for in your form, params[:foo] will contain an element called bar_attributes that will contain the data entered on the form about the bar object. Look through the railcast I linked to for more info.
Related
I'm looking to pass arguments from the controller to the model, but I keep getting wrong number of arguments.
Model:
before_create :embed_info
def embed_info(embed_url)
something = embed_url
## Do some stuff
self.title = "Title" ##This works.
end
Controller:
Create action:
#post = Post.new post_params
#post.embed_info(params[:title])
if #post.save
redirect_to root_url, notice: 'post created'
else
render 'new'
end
You can not pass arguments from controller to model callback like this.
You can use attr_accessor to create a virtual attribute and then set that attribute as part of your create action from controller.
If you're manually invoking the embed_info method, you shouldn't also be invoking it automatically via a before_create callback. Remove the line before_create :embed_info as it's currently serving no purpose except to invoking embed_info with no arguments when you attempt to save your model.
It's possible that you intended to add a validation, which is different than a before_create callback. If your intent was to make sure that embed_info had been called then you should use validations to insure that whatever side effects embed_info has leave the model in a valid state.
but I keep getting wrong number of arguments.
You are getting an arguments error because of this: before_create :embed_info. You can delete that line and then explicitly call the method like you are already doing in the controller.
This is probably not best practice but I think it will get your code working.
Another thing you could do is to move the logic for deriving the title to the controller and then pass the title in with the params.
# controller
def create
#post = Post.new post_params.merge(title: embed_info)
...
def embed_info
something = params[:title]
## Do some stuff
...
You could use attr_accessor to create a virtual attribute of embed_url. Pass it in new action itself. And when you call save, before_save will be called by itself and you can use embed_url there (as its a virtual attribute).
Let me know if you need help if the explanation is not clear.
I have an ActiveRecord model named Document and have implemented CRUD operations around it. I just have a problem with persisting a Document instance between requests when validation fails (be cause I wanna redirect to another page when this happens).
First, I tried storing the instance in the flash session:
# documents_controller.rb
def new
#document = flash[:document] || Document.new
end
def create
document = Document.new(document_params)
if document.save
return redirect_to documents_path
end
flash[:document] = document
redirect_to new_document_path
end
With the code above, I was expecting that the actual Document instance was stored in the flash session, but instead it became a string which looks somewhat like #<Document:0xad32368>. After searching online for a while, I found out that for some reasons you cannot store ActiveRecord objects in sessions.
There are a lot of suggestions about just storing the object's id in the flash session, but I can't do that because as you can see, the object is not yet stored in the database.
Next, I tried reconstructing the Document instance after the redirect, taking advantage of the instance's attributes method (which returns a serializeable hash that can be stored in the session):
# documents_controller.rb
def new
#document = Document.new(flash[:document_hash] || {})
end
def create
...
flash[:document_attributes] = document.attributes
redirect_to new_document_path
end
This almost solved the problem, except for the part in which the validation errors (document.errors) are not preserved. Also, if this is used to persist an instance already stored in the database (in the case of failed validations when updating a Document instance), I'm not sure which between the original attributes and the new attributes will get persisted.
Right now I've already run out ideas to try. Anyone who has a decent solution for this?
EDIT:
You might be wondering why I still have to redirect to another page instead of just rendering the new document view template or the new action in the create method. I did so because there are some things in my views that are dependent on the current controller method. For example, I have a tab which needs to be highlighted when you are on the document creation page (done by checking if action_name == "new" and controller_name == "documents"). If I do:
def create
...
render action: "new"
end
the tab will not get highlighted because action_name will now be create. I also can't just add additional condition to highlight the tab if action_name == "create" because documents can also be created from the the index page (documents_path). Documents can also be updated from the index page (documents_path) or from the detail page (document_path(document)), and if validation fails in the update method, I'd like to redirect to the previous page.
If I really need to fake persisting something between requests (all of the variables that you set are lost between requests), I will ususally put the relevant attributes into hidden fields in the new form.
In your case, this is overkill. In your code, you are redirecting, which causes a new request:
def create
document = Document.new(document_params)
if document.save
return redirect_to documents_path
end
flash[:document] = document
redirect_to new_document_path
end
You can easily render the output of another action, instead of redirecting, by using render action: 'action_to_render'. So in your example, this would probably be:
def create
#document = Document.new(document_params)
if #document.save
render action: 'index'
else
render action: 'new'
end
end
Which can be simplified to:
def create
#document = Document.new(document_params)
action_to_render = #document.save ? 'index' : 'new'
render action_to_render
end
If you need extra logic from the action, you can refactor the logic to a method called from both actions, or simply call the other action from the current one.
It is fine once in a while, but I would caution that having to jerk around with the rendering too much is usually indicative of poor architecture.
Edit:
An additional option, given the newly highlighted constraints, could be to make the new and create methods the same. Remove the new action and routes, and make create answer for GET and PATCH requests. The action might look something like:
def create
#document = Document.new(document_params)
request.patch? && #document.save && redirect_to( documents_path )
end
I actually use something very similar to this for almost all of my controllers, as it tends to DRY things significantly (as you can remove the extra probably identical view, as well)
Another option would be to just use an instance variable to keep track of the active tab in this instance, and make the rest of the code a lot cleaner.
SOLVED
I was able to make a workaround for it using ActiveSupport::Cache::Store (as suggested by #AntiFun). First I created a fake_flash method which acts closely like the flash sessions except that it uses the cache to store the data, and it looks like this:
def fake_flash(key, value)
if value
Rails.cache.write key, value
else
object = Rails.cache.read key
Rails.cache.delete key
object
end
end
And then I just used it like the flash session.
# documents_controller.rb
def new
...
#document = fake_flash[:document] || Document.new
...
end
def create
document = Document.new document_params
...
# if validation fails
fake_flash :document, document
redirect_to new_document_page
end
I'm following the revised version of the nested models Railscast: http://railscasts.com/episodes/196-nested-model-form-revised
I have a similar form, the railscast uses the models survey, question, answer; but I have member, child and caregiver instead.
My code is almost exactly the same as the railscast. I'm using Rails 3.1.3 and Ruby 1.9.2.
My problem is, the children attributes are not reflected on the form. According to this: http://archives.ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes There is no need to do anything on the controller...
The beauty of this solution is that it takes your controllers out of
the mix and makes standard for submissions work perfectly with no
interference at the controller level.
If I don't do this #member.children << Child.new the nested children part of the form does not appear.
def new
#member = Member.new
#member.children << Child.new
respond_to do |format|
format.html
end
end
Am I missing something?
you still need to have children records for the model in order for the form to see it
i would change your controller a bit to use build instead of adding to array
def new
#member = Memeber.new
#member.children.build
end
Newbie on Ruby here, I've successfully build an html form in which users' "project" models can "follow" (or as I've phrased it "pfollow") other "plant" models. The only problem is that the code I've built doesn't work when users try to "pfollow" multiple plants on one form, which forces the user to hit "submit" multiple times for more plants. If the user selects more than one plant from a selection box at a time, my "project" model and my "prelationship" controller choke on the input, which comes as an array rather than the expected single integer (aka, the "pfollower_id" from the plant which is used by the "prelationships" controller for its create action).
How can I teach my app to accept an array of "pfollower_id(s)" and then create multiple plant prelationships?
Here is the error:
undefined method `id' for #<Array:0x26abe70>
app/models/project.rb:35:in `pfollow!'
app/controllers/prelationships_controller.rb:6:in `create'
Request
Parameters:
{"utf8"=>"✓",
"authenticity_token"=>"NKqa1f0M2yPLQDHbRLnxl3SiwBeTus/1q1hpZjD7hgY=",
"prelationship"=>{"pfollower_id"=>["4",
"5"]},
"project_id"=>"90",
"commit"=>"Pfollow"}
My "prelationships" controller:
class PrelationshipsController < ApplicationController
def create
#project = Project.find(params[:project_id])
#plant = Plant.find(params[:prelationship][:pfollower_id])
#project.pfollow!(#plant)
respond_to do |format|
format.html { redirect_to #project }
format.js
end
end
end
And the suspect "pfollow!" method in my model that the trace indicates is also culpable:
def pfollow!(pfollowed)
prelationships.create!(:pfollowed_id => pfollowed.id)
end
The form works fine, so it's just this controller and method that can't handle an array of pfollower_ids.
I'm desperate for help! Any and all direction would be immensely helpful.
Break it down with an each method in your controller :D
params[:prelationship][:pfollower_id].each do |p|
#project.pfollow!( Plant.find(p) )
end
I'm guessing this would work if only one param is returned as well. Not entirely sure.
I have been trying to get my head around render_to but I haven't had much success.
Essentially I have controller methods:
def first
#I want to get the value of VAR1 here
end
def second
VAR1 = ["Hello", "Goodbye"]
render_to ??
end
What I can't figure out is how to accomplish that. Originally I just wanted to render the first.html.erb file but that didn't seem to work either.
Thanks
Edit: I appreciate the answers I have received, however all of them tend to avoid using the render method or redirect_to. Is it basically the case then that a you cannot pass variables from controller to controller? I have to think that there is some way but I can't seem to find it.
It is not a good idea to assign the object to a constant. True this is in a global space, but it is global for everyone so any other user going to this request will get this object. There are a few solutions to this.
I am assuming you have a multi-step form you are going through. In that case you can pass the set attributes as hidden fields.
<%= f.hidden_field :name %>
If there are a lot of fields this can be tedious so you may want to loop through the params[...] hash or column_names method to determine which attributes to pass.
Alternatively you can store attributes in the session.
def first
#item = Item.new(params[:item])
session[:item_attributes] = #item.attributes
end
def second
#item = Item.new(session[:item_attributes])
#item.attributes = params[:item]
end
Thirdly, as Paul Keeble mentioned you can save the model to the database but mark it as incomplete. You may want to use a state machine for this.
Finally, you may want to take a look at the Acts As Wizard plugin.
I usually don't have my controllers calling each other's actions. If you have an identifier that starts with a capital letter, in Ruby that is a constant. If you want to an instance level variable, have it start with #.
#var1 = ["Hello", "Goodbye"]
Can you explain what your goal is?
Have you considered using the flash hash? A lot of people use it solely for error messages and the like, it's explicitly for the sort of transient data passing you might be interested in.
Basically, the flash method returns a hash. Any value you assign to a key in the hash will be available to the next action, but then it's gone. So:
def first
flash[:var] = ["hello", "goodbye"]
redirect_to :action => :second
end
def second
#hello = flash[:var].first
end
way 1
Global variable
(fail during concurrent requests)
way 2
class variable
(fail during concurrent requests)
way 3
Stash the object on the server between requests. The typical way is to save it in the session, since it automatically serializes/deserializes the object for you.
Serialize the object and include it in the form somewhere, and
deserialize it from the parameters in the next request. so you can store attributes in the session.
def first
#item = Item.new(params[:item])
session[:item_attributes] = #item.attributes
end
def second
#item = Item.new(session[:item_attributes])
#item.attributes = params[:item]
end
way 4
The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed to the very next action and then cleared out.
def new
#test_suite_run = TestSuiteRun.new
#tests = Test.find(:all, :conditions => { :test_suite_id => params[:number] })
flash[:someval] = params[:number]
end
def create
#test_suite_run = TestSuiteRun.new(params[:test_suite_run])
#tests = Test.find(:all, :conditions => { :test_suite_id => flash[:someval] })
end
way 5
you can use rails cache.
Rails.cache.write("list",[1,2,3])
Rails.cache.read("list")
But what happens when different sessions have different values?
Unless you ensure the uniqueness of the list name across the session this solution will fail during concurrent requests
way 6
In one action store the value in db table based on the session id and other action can retrieve it from db based on session id.
way 7
class BarsController < UsersController
before_filter :init_foo_list
def method1
render :method2
end
def method2
#foo_list.each do | item|
# do something
end
end
def init_foo_list
#foo_list ||= ['Money', 'Animals', 'Ummagumma']
end
end
way 8
From action sent to view and again from view sent to other actions in controller.