Getting fields_for and accepts_nested_attributes_for to work with a belongs_to relationship
I think your accepts_nested_attributes
is on the wrong side of the relationship. Maybe something like this would work?
class Account < ActiveRecord::Base
belongs_to :owner, :class_name => 'User', :foreign_key => 'owner_id'
has_many :users
end
class User < ActiveRecord::Base
belongs_to :account
has_one :account, :foreign_key => :owner_id
accepts_nested_attributes_for :account
end
For building the account you want to use build_account.
You can see more examples in the docs.
Using accepts_nested_attributes_for with a belongs_to association, and using find_or_create_by_attr behaviour
I ended up using the following solution, and it does seem to work. I created a gist to hold the solution with a matching spec to define the behaviour I was seeking.
http://gist.github.com/437939
Rails 3 Nested Model Form, 2 levels deep using accepts_nested_attributes_for
I think you've got the form variables slightly mixed up. It should be:
= form_for @question, :html => {:multipart => true} do |f|
= f.label :text, "Question Text:"
= f.text_area :text, :rows => 7
%br
%br
=f.fields_for :answer, do |af|
= af.label :body, "Answer Text:"
= af.text_area :body, :rows => 7
%br
%br
= af.fields_for :image do |img_form|
= img_form.label :title, "Image Title:"
= img_form.text_field :title
%br
= img_form.label :file, "Image File:"
= img_form.file_field :file
%br
= img_form.label :caption, "Image Caption:"
= img_form.text_area :caption, :rows => 7
= hidden_field_tag("case_id", value = @case_id)
= f.submit
Notice how form_for ... do |f|
spawns f.fields_for ... do |af|
, which in turns spawns af.fields_for ... do |img_form|
.
The key is the second fields_for. It should be af.fields_for :image do |img_form|
rather than f.fields_for :image do |img_form|
.
Rails - Why does my nested model form not require 'accepts_nested_attributes_for'
You don't need it because your pattern is wrong:
@user_item = @item.user_items.build(user_id: current_user.id, picture: item_params[:item][:picture])
With your above code, you're creating individual user_items
from each @item
. This does not require accepts_nested_attributes_for
, but is cumbersome, against convention and very hacky.
Here's how it should be done:
Models
#app/models/item.rb
class Item < ActiveRecord::Base
has_many :user_items
has_many :users, through: :user_items
accepts_nested_attributes_for :user_items
end
#app/models/user_item.rb
class UserItem < ActiveRecord::Base
belongs_to :user
belongs_to :item
end
Controllers
#app/controllers/items_controller.rb
class ItemsController < ApplicationController
def new
@item = Item.new
@item.user_items.build
end
def create
@item = Item.new item_params
redirect_to @item, notice: "Thank you for your item submission." if @item.save
end
private
def item_params
params.require(:item).permit(:name, :description, :tag_list, user_items_attributes: [:picture])
end
end
Views
#app/views/items/new.html.erb
<%= simple_form_for @item do |item_builder| %>
<%= item_builder.input :name, required: false, error: false, label: "Item name" %>
<%= item_builder.input :description, as: :text, required: false, error: false, label: "Describe item" %>
<%= item_builder.input :tag_list, required: false, label: "Tags" %>
<%= item_builder.simple_fields_for :user_items do |user_item_builder| %>
<%= user_item_builder.input :picture, as: :file, required: false, label: "Picture of you with this item" %>
<% end %>
<%= item_builder.submit 'Submit new item', class: "btn btn-primary pull-right inherit-width" %>
<% end %>
is there a problem with the way I'm doing it
Technically, no.
However, if you aspire to be a professional, or on a similar level, you'll not get very far with the code you wrote.
Using a framework like Rails gives you access to an unprecedented array of pre-baked functionality. The best code is not the code you wrote, but the library code that's been compiled & tested over years of production use.
Whilst your code works, it's not efficient nor extensible.
--
The ultimate key of whether what you've written is "right" is whether the future you would be proud of looking at it again. If not, you're probably best refactoring.
Update
If you want to give Item
a status
, you'll want to look at the enum
module for ActiveRecord models:
#app/models/item.rb
class Item < ActiveRecord::Base
enum status: [:active, :pending, :declined]
end
This is a very interesting method, as it provides a series of class methods (scopes), and instance methods:
@item = Item.find x
@item.active? #-> true
@item.pending? #-> false
@item.declined? #-> false
Item.active #-> collection of "active" items
Item.pending #-> collection of "pending" items
Item.declined #-> collection of "declined" items
To save an Item's status
, you could use the collection_select
as so:
<%= form_for @item do |f| %>
<%= f.collection_select :status, Item.statuses, :first, :first %>
<%= f.submit %>
<% end %>
Update
Your code can be improved massively:
def create
@item= Item.new item_params #-> this line should do ALL the heavy lifting.
if @item.save
redirect_to items_path, notice: "Thank you for your item request!"
else
render :new
end
end
private
def item_params
params.require(:item).permit(:name, :description, :tag_list, user_items_attributes: [:picture]).merge(created_by: current_user.id)
end
The file
field won't be populated again if you have validation issues; it's an OS problem, not Rails (how does the OS know your file is in the same location as it was when you first submitted?).
You need to make your create
code as succinct as possible; explicit declarations for attributes is generally a bad idea. You should put as much of it as possible into the Item
model (enum
etc).
Rails multiple belongs_to assignment
Save and edit discussions along with post
Existing Discussion
To associate the post you're building with an existing discussion, just merge the id into the post params
@post = current_user.posts.build(
params[:post].merge(
:discussion_id => existing_discussion.id
)
You will have to have a hidden input for discussion id in the form for @post
so the association gets saved.
New Discussion
If you want to build a new discussion along with every post and manage its attributes via the form, use accepts_nested_attributes
class Post < ActiveRecord::Base
belongs_to :user
belongs_to :discussion
accepts_nested_attributes_for :discussion
end
You then have to build the discussion in the controller with build_discussion
after you built the post
@post.build_discussion
And in your form, you can include nested fields for discussions
form_for @post do |f|
f.fields_for :discussion do |df|
...etc
This will create a discussion along with the post. For more on nested attributes, watch this excellent railscast
Better Relations
Furthermore, you can use the :through
option of the has_many
association for a more consistent relational setup:
class User < ActiveRecord::Base
has_many :posts
has_many :discussions, :through => :posts, :source => :discussion
end
class Discussion < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
belongs_to :discussion
end
Like this, the relation of the user to the discussion is maintained only in the Post
model, and not in two places.
accepts_nested_attributes_for values cleared when validation fails
Don't specify the @journey
on your simple_fields_for - that's not the instance you're validating and repopulating. Let Rails handle it - it will pick @user.journeys
for you.
Rails / nested attributes / file_field doesn't show up within params when empty
I would suggest adding a check in your controller and returning a flash[:error] message if the file field is missing.
You could also manually add the fields if they don't exist, so that validation is triggered:
m1params = params[:model_1]
m1params[:model_2_attributes] = {} unless m1params.has_key?(:model_2_attributes)
Finally, you could create a fake attribue in your model_2 Model that you could use to ensure the model_2_attributes get's passed in the form:
class Model2
attr_writer :fake
def fake
@fake ||= 'default'
end
end
= form_for @model_1, :html => { :multipart => true } do |f|
- # fields for model 1 …
= f.fields_for :model_2 do |builder|
= builder.hidden_field :fake
= builder.file_field :file
Related Topics
Gem Install Permission Problem
How to Get a Date from Date_Select or Select_Date in Rails
Read Binary File as String in Ruby
How to Implement a Short Url Like the Urls in Twitter
How to Install Rvm on Windows 7
Rvm Warning! Path Is Not Properly Set Up
How to Parse a CSV File, Update a Field, Then Save
What Does a ||= Mean in Ruby Language
Best Ruby on Rails Social Networking Framework
In Ruby, How Does Coerce() Actually Work
What Are the Magic $-Prefixed Variables in Ruby