Getting Fields_For and Accepts_Nested_Attributes_For to Work With a Belongs_To Relationship

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



Leave a reply



Submit