Rails nested form fields_for doubled in edit - ruby-on-rails

I'm trying to create a form where users can see a list of their friends and add or remove friends from lists (sort of like facebook's groups). For the create list view, I can get the form working properly, but for the edit view I'm having trouble with check_box syntax and fields_for.
routes.rb
resources :friendships
resources :friend_lists
class User < ActiveRecord::Base
has_many :friendships, dependent: :destroy
has_many :friends, through: :friendships
has_many :friend_lists
has_many :flist_memberships, class_name: 'FlistMembership', foreign_key: 'member_id', dependent: :destroy
...
end
class FriendList < ActiveRecord::Base
belongs_to :user
has_many :flist_memberships
has_many :members, through: :flist_memberships
accepts_nested_attributes_for :members, allow_destroy: true
accepts_nested_attributes_for :flist_memberships, allow_destroy: true
end
class FlistMembership < ActiveRecord::Base
belongs_to :friend_list
belongs_to :member, class_name: 'User'
end
class FriendListsController < ApplicationController
def new
#friend_list = current_user.friend_lists.build
#friends = current_user.friends.paginate(page: params[:page])
#friend_list.flist_memberships.build
end
def create
#friend_list = current_user.friend_lists.build(friend_list_params)
respond_to do |format|
format.html {
if #friend_list.save
flash[:success] = "Friends list created!"
redirect_to friendships_path
else
render 'friend_lists/new'
end
}
end
end
def edit
#friend_list = FriendList.find(params[:id])
#friends = current_user.friends
end
def update
#friend_list = FriendList.find(params[:id])
if #friend_list.update_attributes(friend_list_params)
flash[:success] = "Friend list updated!"
redirect_to friend_list_path(#friend_list)
else
render 'edit'
end
end
private
def friend_list_params
params.require(:friend_list).permit(:name, flist_memberships_attributes: [:id, :member_id, :friend_list_id])
end
end
new.html.erb
<%= form_for #friend_list do |f| %>
<%= f.label :name, "Name for this friends list:" %>
<%= f.text_field :name %>
<% #friends.each do |friend| %>
<%= f.fields_for :flist_memberships do |m| %>
<%= m.check_box :member_id, {}, friend.id %>
<%= m.label :member_id, friend.name %>
<% end %>
<% end %>
<%= f.submit "Save", class: "btn btn-primary" %>
<% end %>
edit.html.erb
<%= form_for #friend_list do |f| %>
<%= f.label :name, "Name for this friends list:" %>
<%= f.text_field :name %>
<%= f.fields_for :flist_memberships do |m| %>
<%= m.collection_check_boxes(:member_id, #friends.all, :id, :name) %>
<% end %>
<%= f.submit "Save", class: "btn btn-primary" %>
<% end %>
new.html.erb renders and saves the selected items correctly, but I can't figure out how to write the edit form so that it doesn't iterate through the collection once for each item in the collection (both with collection_check_boxes and the form as written in new.html.erb). Trying
<%= f.fields_for :flist_memberships do |m| %>
<%= m.check_box :id %>
<%= m.label :id, :name %>
<% end %>
in the edit form just passes the string "id" as params rather than pulling the id of each member in the collection, and I'm not sure how to pull the individual item's name/id without iterating over the collection, which goes back to the multiple renders problem. I'm using cocoon elsewhere in the project, but for this I'd rather present the user a list of all options and allow them to check a box for each one, rather than having to manually add each item on the list. If there is a way to do it with cocoon, I'd be happy to hear it.

Related

Tag model on another Rails

I want to tag my product model on my post model.
post.rb
has_many :taggings
has_many :products, through: :taggings
product.rb
has_many :taggings
has_many :posts, through: :taggings
tagging.rb
belongs_to :post
belongs_to :product
On my post view, I have a product list. I want that when the user clicks on a product, it creates a new product/post link via post method.
What link can I use? How to set up controllers and params?
Thanks for the help
If you want to let the user create multiple taggings at once you can just add a select / checkbox to the forms for a post.
<%= form_for(#post) do |f| %>
# ...
<div class="field">
<%= f.label :product_ids %>
<%= f.collection_select :product_ids, Product.all, :name, :id %>
</div>
<% end %>
def post_params
params.require(:post)
.permit(:foo, :bar, product_ids: [])
end
Rails will automatically create the records in the join table.
If you want the user to create the linkings one by one you need to setup a nested route:
Rails.application.routes.draw do
# ...
resources :posts do
resources :taggings, only: :create
end
end
You then need to setup a form for each product on the posts/show.html.erb page:
<ul>
<% #post.products.each do |product| %>
<li>
<%= product.name %>
<%= form_for [#post, product.taggings.new] do |f| %>
<%= f.hidden_field :product_id %>
<%= f.submit 'tag' %>
<% end %>
</li>
<% end %>
</ul>
You can pretty this up later with CSS/JS.
And a controller to handle creating the taggings.
class TaggingsController < ApplicationController
# POST /posts/:post_id/taggings
def create
#post = Post.find(params[:post_id])
#tagging = #post.taggings.new(product: Product.find(params[:tagging][:product_id]))
if #tagging.save
redirect_to #product, success: 'Tagging saved.'
else
redirect_to #product, error: 'Tagging not saved.'
end
end
end

Rails not saving nested attributes

I have the tables Task and Item. I have a form for Item where I record all the possible items that my Tasks may have, which is working fine. Then I have the form for Task where all the Items are displayed alongside a field to put a cost value for each item. This will result in a join between Task and Item: TaskItem (this table contains task_id, item_id and cost).
When I submit the form, it's saving the Task but not the TaskItems associated. I don't see what I'm missing as I searched a lot for this problem and nothing seems to work. Please, see the code below.
Model:
class Task < ApplicationRecord
has_many :task_items
has_many :items, :through => :task_items
accepts_nested_attributes_for :task_items, :allow_destroy => true
end
class Item < ApplicationRecord
has_many :task_items
has_many :tasks, :through => :task_items
end
class TaskItem < ApplicationRecord
belongs_to :task
belongs_to :item
accepts_nested_attributes_for :item, :allow_destroy => true
end
Controller:
def new
#items = Item.all
#task = Task.new
#task.task_items.build
end
def create
#task = Task.new(task_params)
#task.save
redirect_to action: "index"
end
private def task_params
params.require(:task).permit(:id, :title, task_items_attributes: [:id, :item_id, :cost])
end
My view:
<%= form_for :task, url:tasks_path do |f| %>
<p>
<%= f.label :title %><br>
<%= f.text_field(:title, {:class => 'form-control'}) %><br>
</p>
<% #items.each do |item| %>
<% #task_items = TaskItem.new %>
<%= f.fields_for :task_items do |ti| %>
<%= ti.label item.description %>
<%= ti.text_field :cost %>
<%= ti.hidden_field :item_id, value: item.id %>
<% end %>
<% end %>
<p>
<%= f.submit({:class => 'btn btn-primary'}) %>
</p>
You need to add inverse_of option to the has_many method in class Task:
class Task < ApplicationRecord
has_many :task_items, inverse_of: :task
has_many :items, through: :task_items
accepts_nested_attributes_for :task_items, :allow_destroy => true
end
This is due to the when creating a new TaskItem instance, it requires that the Task instance already exists in database to be able to grab the id fo the Task instance. Using this option, it skips the validation.
You can read this post about inverse_of option and its use cases.
fields_for has an option to specify the object which is going to store the information. This combined with building each of the TaskItem from the has_many collection should ensure that all the relationship are set correctly.
View Code:
<%= form_for #task do |f| %>
<p>
<%= f.label :title %><br>
<%= f.text_field(:title, {:class => 'form-control'}) %><br>
</p>
<% #items.each do |item| %>
<% task_item = #task.task_items.build %>
<%= f.fields_for :task_items, task_item do |ti| %>
<%= ti.label item.description %>
<%= ti.text_field :cost %>
<%= ti.hidden_field :item_id, value: item.id %>
<% end %>
<% end %>
<p>
<%= f.submit({:class => 'btn btn-primary'}) %>
</p>
<% end %>
Controller Code:
def index
end
def new
#items = Item.all
#task = Task.new
end
def create
#task = Task.new(task_params)
#task.save
redirect_to action: "index"
end
private
def task_params
params.require(:task).permit(:id, :title, task_items_attributes: [:id, :item_id, :cost])
end

rails4 collection select with has_many through association and nested model forms

I have a rails4 app. At the moment my collection select only works if I select only one option. Below is my working code. I only have product form. Industry model is populated with seeds.rb. IndustryProduct is only use to connect the other 2 models.
I'd like to know what I have to change in the code to be able to choose more.
I saw some working examples with multiple: true option like (https://www.youtube.com/watch?v=ZNrNGTe2Zqk at 10:20) but in this case the UI is kinda ugly + couldn't pull it off with any of the sample codes. Is there an other solution like having more boxes with one option chosen instead of one box with multiple options?
models:
class Product < ActiveRecord::Base
belongs_to :user
has_many :industry_products
has_many :industries, through: :industry_products
has_many :product_features
accepts_nested_attributes_for :industry_products, allow_destroy: true
accepts_nested_attributes_for :product_features
validates_associated :industry_products
validates_associated :product_features
end
class Industry < ActiveRecord::Base
has_many :industry_products
has_many :products, through: :industry_products
accepts_nested_attributes_for :industry_products
end
class IndustryProduct < ActiveRecord::Base
belongs_to :product
belongs_to :industry
end
_form.html.erb
<%= form_for #product do |f| %>
<%= render 'layouts/error_messages', object: f.object %>
......
<%= f.fields_for :industry_products do |p| %>
<%= p.collection_select :industry_id, Industry.all, :id, :name %>
<% end %>
<%= f.fields_for :product_features do |p| %>
<%= p.text_field :feature, placeholder: "add a feature", class: "form-control" %>
<% end %>
<%= f.submit class: "btn btn-primary" %>
<% end %>
products controller
def new
#product = Product.new
#product.industry_products.build
#product.product_features.build
end
def create
#product = current_user.products.new(product_params)
if #product.save
redirect_to #product
else
render action: :new
end
end
......
def product_params
params.require(:product).permit(....., industry_products_attributes: [:id, :industry_id, :_destroy], industries_attributes: [:id, :name], product_features_attributes: [:feature])
end
Firstly, you could fix your first collection select by using it to set the industry_ids for the #product:
<%= form_for #product do |f| %>
<%= f.collection_select :industry_ids, Industry.all, :id, :name %>
<% end %>
This will allow you to set the collection_singular_ids method, which exists for all has_many associations.
You'd have to back it up in the params method:
#app/controllers/products_controller.rb
....
def product_params
params.require(:product).permit(.... industry_ids: [])
end
A lot more succinct than using nested attributes.
To get that "multiple" selection, you'll want to use the following:
<%= f.collection_select :industry_ids, Industry.all, :id, :name, {}, { multiple: true } %>
Tested & working
--
You may also want to look at collection_check_boxes:
<%= f.collection_check_boxes :industry_ids, Industry.all, :id, :name %>

How to add tags to posts using checkbox?

I want to build a many-to-many association through tagging between posts and tags. Users can create posts with tags by checking the existed tags. But I don't know how to create a nested form and save the association.
My form
<%= form_for(#post) do |f| %>
<div class="field">
<%= f.label :text %><br />
<%= f.text_field :text %>
</div>
<div>
<%= hidden_field_tag "post[tag_ids][]", nil %>
<% Tag.all.each do |tag| %>
<%= check_box_tag "post[tag_ids][]", tag.id, #post.tag_ids.include?(tag.id) %>
<%= tag.name %>
<% end %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
posts controller here:
class PostsController < ApplicationController
def index
#posts = Post.all
end
def new
#post = Post.new
end
def create
#post = Post.new(post_params)
if #post.save
redirect_to #post
else
render 'new'
end
end
def show
#post = Post.find(params[:id])
end
private
def post_params
params.require(:post).permit(:text)
end
end
Model
class Post < ActiveRecord::Base
has_many :taggings
has_many :tags, :through => :taggings
end
class Tag < ActiveRecord::Base
has_many :taggings
has_many :posts, :through => :taggings
end
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :post
end
I don't think you actually need to have a nested form in this case. Rails automagically figures out the association, when given the attribute :tag_ids
This is what I did for a multiselect form_helper, which would need minor changes for a check_box helper
f.select :tag_ids, Tag.all.collect {|tag| [tag.name, tag.id]}, {}, :multiple => true

rails: mass-assign error caused by updating child (Post) from parent (Topic) controller (edit view)

I am trying to edit a Topic which has many Posts.
Edit page for a Topic has Topic's name and Post's content that can be edited.
The mass-assignment error occurs in topics_controller.rb, update method, post.update_attributes(params[:post]).
How do I avoid mass-assignment error.
topic.rb
class Topic < ActiveRecord::Base
has_many :posts, :dependent => :destroy
belongs_to :forum
accepts_nested_attributes_for :posts, :allow_destroy => true
attr_accessible :name, :last_post_id, :posts_attributes
end
post.rb
class Post < ActiveRecord::Base
belongs_to :topic
attr_accessible :content
end
topics_controller.rb
def update
#topic = Topic.find(params[:id])
post = #topic.posts.first
if #topic.update_attributes(params[:topic]) && post.update_attributes(params[:post])
topic = Topic.find(#post.topic_id)
flash[:success] = "Success!"
redirect_to topic_posts_path(topic)
else
render 'edit'
end
end
views/topics/edit.html.erb
<%= form_for #topic do |f| %>
<!-- render 'shared/error_messages_topic' -->
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.fields_for #topic.posts.first do |post| %>
<%= render :partial => "posts/form", :locals => {:f => post} %>
<% end %>
<%= f.submit "Edit", class: "btn btn-large btn-primary" %>
<% end %>
views/posts/_form.html.erb
<%= f.label :content %>
<%= f.text_area :content %>
In update method you don't have to update attributes of both the models instead of if #topic.update_attributes(params[:topic]) && post.update_attributes(params[:post]) it should this only if #topic.update_attributes(params[:topic]) it will update the posts automatically.
And change your view from this <%= f.fields_for #topic.posts.first do |post| %> to <%= f.fields_for :posts, #topic.posts.first do |post| %> it will work fine.
For more information read this http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-fields_for

Resources