I'm trying to update the form for a Song model that has multiple Genres through a song_genre_relationships model. I'd prefer to update the genre id's for any given song through an array in the form field.
I suspect my controller is off, but I'm still pretty new to Rails and am unsure.
Here are my models, form and controller.
EDITED BASED ON meagar's RESPONSE BELOW.
song.rb
class Song < ActiveRecord::Base
has_many :song_genre_relationships
has_many :genres, :through => :song_genre_relationships
end
song_genre_relationships.rb
class SongGenreRelationship < ActiveRecord::Base
belongs_to :song
belongs_to :genre
end
genre.rb
class Genre < ActiveRecord::Base
has_many :song_genre_relationships
has_many :songs, :through => :song_genre_relationships
end
song_controller.rb
class SongController < ApplicationController
def update
#song = Song.find(params[:id])
relationships = #song.song_genre_relationships
#genres = Genre.all
#genres.each do |genre|
unless relationships.detect { |g| g.genre_id == genre.id }
relationships.build genre_id: genre.id
end
end
if #song.update_attributes(song_params)
flash[:notice] = "Song updated successfully."
redirect_to(:action => 'index')
else
render('edit')
end
end
private
def song_params
params.require(:song).permit(:name, :genre_ids => [])
end
_form.html.erb (song form)
<%= form_for(#song) do |f| %>
<%= f.label :name, 'Title' %>
<%= f.text_field :name %>
<%= f.label :genre_ids, 'Genres' %>
<%= f.text_area :genre_ids %>
<%= f.submit %>
<% end %>
This is wrong:
#genres = params[:song][:genre_ids]
Array(#genres).each do |genre|
unless relationships.detect { |g| g.genre_id == genre.id }
relationships.build genre_id: genre
end
end
params[:song][:genre_ids] is a string. It contains, in your example, "[<whatever id's input to the form>]".
You're wrapping it in an array with Array(#genres). Now, you have an array containing a single item, which is the string. Then you iterate over that single item, placing it into genre inside the loop
Then you test g.genre_id == genre.id. genre is that string, and you're invoking .id on it, hence your error.
It's impossible to tell what you're trying to do, but you definitely cannot invoke .id on a string and expect to get the ID of some database record back.
Related
any help would be most appreciated, I am rather new to Rails.
I have two models a Shopping List and a Product. I'd like to save/update multiple products to a shopping list at a time.
The suggested changes are not updating the models. I've been googling and is "attr_accessor" or find_or_create_by the answer(s)?
Attempt 1 - Existing code
Error
> unknown attribute 'products_attributes' for Product.
Request
Parameters:
{"_method"=>"patch",
"authenticity_token"=>"3BgTQth38d5ykd3EHiuV1hkUqBZaTmedaJai3p9AR1N2bPlHraVANaxxe5lQYaVcWNoydA3Hb3ooMZxx15YnOQ==",
"list"=>
{"products_attributes"=>
{"0"=>{"title"=>"ten", "id"=>"12"},
"1"=>{"title"=>"two", "id"=>"13"},
"2"=>{"title"=>"three", "id"=>"14"},
"3"=>{"title"=>"four", "id"=>"15"},
"4"=>{"title"=>"five", "id"=>"16"},
"5"=>{"title"=>""},
"6"=>{"title"=>""},
"7"=>{"title"=>""},
"8"=>{"title"=>""},
"9"=>{"title"=>""},
"10"=>{"title"=>""}}},
"commit"=>"Save Products",
"id"=>"7"}
Attempt 2 - no errors the page reloads and none of the expected fields are updated. In earnest, I am Googling around and copying and pasting code snippets in the vain hope of unlocking the right combo.
Added to Products mode
class Product < ApplicationRecord
attr_accessor :products_attributes
belongs_to :list, optional: true
end
<%= content_tag(:h1, 'Add Products To This List') %>
<%= form_for(#list) do |f| %>
<%= f.fields_for :products do |pf| %>
<%= pf.text_field :title %><br>
<% end %>
<p>
<%= submit_tag "Save Products" %>
</p>
<% end %>
<%= link_to "Back To List", lists_path %>
list controller
def update
#render plain: params[:list].inspect
#list = List.find(params[:id])
if #list.products.update(params.require(:list).permit(:id, products_attributes: [:id, :title]))
redirect_to list_path(#list)
else
render 'show'
end
list model
class List < ApplicationRecord
has_many :products
accepts_nested_attributes_for :products
end
original do nothing - product model
class Product < ApplicationRecord
belongs_to :list, optional: true
end
If you just want a user to be able to select products and place them on a list you want a many to many association:
class List < ApplicationRecord
has_many :list_items
has_many :products, through: :list_products
end
class ListItem < ApplicationRecord
belongs_to :list
belongs_to :product
end
class Product < ApplicationRecord
has_many :list_items
has_many :lists, through: :list_products
end
This avoids creating vast numbers of duplicates on the products table and is known as normalization.
You can then select existing products by simply using a select:
<%= form_for(#list) do |f| %>
<%= f.label :product_ids %>
<%= f.collection_select(:product_ids, Product.all, :name, :id) %>
# ...
<% end %>
Note that this has nothing to with nested routes or nested attributes. Its just a select that uses the product_ids setter that's created by the association. This form will still submit to /lists or /lists/:id
You can whitelist an array of ids by:
def list_params
params.require(:list)
.permit(:foo, :bar, product_ids: [])
end
To add create/update/delete a bunch of nested records in one form you can use accepts_nested_attributes_for together with fields_for:
class List < ApplicationRecord
has_many :list_items
has_many :products, through: :list_products
accepts_nested_attributes_for :products
end
<%= form_for(#list) do |f| %>
<%= form.fields_for :products do |pf| %>
<%= pf.label :title %><br>
<%= pf.text_field :title %>
<% end %>
# ...
<% end %>
Of course fields_for won't show anything if you don't seed the association with records. That's where that loop that you completely misplaced comes in.
class ListsController < ApplicationController
# ...
def new
#list = List.new
5.times { #list.products.new } # seeds the form
end
def edit
#list = List.find(params[:id])
5.times { #list.products.new } # seeds the form
end
# ...
def update
#list = List.find(params[:id])
if #list.update(list_params)
redirect_to #list
else
render :new
end
end
private
def list_params
params.require(:list)
.permit(
:foo, :bar,
product_ids: [],
products_attrbutes: [ :title ]
)
end
end
Required reading:
Rails Guides: Nested forms
ActiveRecord::NestedAttributes
fields_for
I'm kinda new to ruby on rails, I've been reading documentation on assosiations and I've been having an easy time (and usually a quick google search solves most of my doubts) however recently I'm having problems with a seemingly easy thing to do.
What I'm trying to do is to create an Event, linked to an existing Category.
Event model
class Event < ApplicationRecord
has_many :categorizations
has_many :categories, through: :categorizations
accepts_nested_attributes_for :categorizations
.
.
.
end
Category model
class Category < ApplicationRecord
has_many :categorizations
has_many :events, through: :categorizations
end
Categorization model
class Categorization < ApplicationRecord
belongs_to :event
belongs_to :category
end
Event controller
class EventsController < ApplicationController
def new
#event = Event.new
end
def create
#user = User.find(current_user.id)
#event = #user.events.create(event_params)
if #event.save
redirect_to root_path
else
redirect_to root_path
end
end
private
def event_params
params.require(:event).permit(:name, category_ids:[])
end
Here is the form, which is where I think the problem lies:
<%= form_for #event, :html => {:multipart => true} do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.fields_for :categorizations do |categories_fields|%>
<% categories = [] %>
<% Category.all.each do |category| %>
<% categories << category.name %>
<% end %>
<%= categories_fields.label :category_id, "Category" %>
<%= categories_fields.select ( :category_id, categories) %>
<% end %>
.
.
.
<%= f.submit "Create"%>
<% end %>
I previously populate the Category db with some categories, so what's left to do is to while creating an event, also create a categorization that is linked both to the new event and the chosen Categorization. but the things I've tried don't seem to be working.
Other things seem to be working ok, whenever I try to submit the event all things are populated as expected except the categorization.
As you mentioned that you are new to rails, you'll find this cocoon gem very interesting. You can achieve what you wanted. And the code will cleaner.
I don't have the points to comment, that's why I am giving this as an answer.
I have three model classes related to each other.
class Student < ActiveRecord::Base
has_many :marks
belongs_to :group
accepts_nested_attributes_for :marks,
reject_if: proc { |attributes| attributes['rate'].blank?},
allow_destroy: true
end
This class describes a student that has many marks and I want to create a Student record along with his marks.
class Mark < ActiveRecord::Base
belongs_to :student, dependent: :destroy
belongs_to :subject
end
Marks are related both to the Subject and a Student.
class Subject < ActiveRecord::Base
belongs_to :group
has_many :marks
end
When I try to create the nested fields of marks in loop labeling them with subject names and passing into in it's subject_id via a loop a problem comes up - only the last nested field of marks is saved correctly, whilst other fields are ignored. Here's my form view code:
<%= form_for([#group, #student]) do |f| %>
<%= f.text_field :student_name %>
<%=f.label 'Student`s name'%><br>
<%= f.text_field :student_surname %>
<%=f.label 'Student`s surname'%><br>
<%=f.check_box :is_payer%>
<%=f.label 'Payer'%>
<%= f.fields_for :marks, #student.marks do |ff|%>
<%#group.subjects.each do |subject| %><br>
<%=ff.label subject.subject_full_name%><br>
<%=ff.text_field :rate %>
<%=ff.hidden_field :subject_id, :value => subject.id%><br>
<%end%>
<% end %>
<%= f.submit 'Add student'%>
<% end %>
Here`s my controller code:
class StudentsController<ApplicationController
before_action :authenticate_admin!
def new
#student = Student.new
#student.marks.build
#group = Group.find(params[:group_id])
#group.student_sort
end
def create
#group = Group.find(params[:group_id])
#student = #group.students.new(student_params)
if #student.save
redirect_to new_group_student_path
flash[:notice] = 'Студента успішно додано!'
else
redirect_to new_group_student_path
flash[:alert] = 'При створенні були деякі помилки!'
end
end
private
def student_params
params.require(:student).permit(:student_name, :student_surname, :is_payer, marks_attributes: [:id, :rate, :subject_id, :_destroy])
end
end
How can I fix it?
#student.marks.build
This line will reserve an object Mark.
If you want multi marks, May be you need something like this in new action :
#group.subjects.each do |subject|
#student.marks.build(:subject=> subject)
end
Hope useful for you.
I'm using the rails3-jquery-autocomplete gem my artist field on a releases form and want to make sure any new releases use the id of an existing artist if found via autocomplete or create a new artist if not found. At the moment the autocomplete gem returns artists as expected but created a new entry even if that artist exists.
My models are as follows:
class Artist < ActiveRecord::Base
has_and_belongs_to_many :releases
end
class Release < ActiveRecord::Base
has_and_belongs_to_many :artists
accepts_nested_attributes_for :artists, :reject_if => lambda { |a| a[:name].blank? }
end
And controllers:
class ArtistsController < ApplicationController
def index
#artists = Artist.find(:all, :order => :name, :group => :name)
end
def create
#release = Release.find(params[:release_id])
#artist = #release.artists.create(params[:artist])
redirect_to release_path(#release)
end
def destroy
#release = Release.find(params[:release_id])
#artist = #release.artists.find(params[:id])
#artist.destroy
redirect_to release_path(#release)
end
end
class ReleasesController < ApplicationController
autocomplete :artist, :name
def new
#release = Release.new
#release.artists.build
end
def create
#release = Release.new(params[:release])
#release.user_id = current_user.id
end
end
My routes contains:
resources :releases do
get :autocomplete_artist_name, :on => :collection
end
And finally the artist fields:
<%= f.fields_for :artists do |builder| %>
<%= render 'artist_fields', :f => builder %>
<% end %>
(The rendered fields)
<%= f.label :name, "Artist" %><br />
<%= f.autocomplete_field :name, autocomplete_artist_name_releases_path, :class => "text" %>
I suggest you watch this railscast on autcomplete associations
It clearly does what you need.
I don't want to spoil your viewing pleasure, but it all revolves around find_or_create_by
Why don't you fetch the id of the artist with
:id_element => '#artist_id' on the f.autocomplete_field?
If the artist_id param does not come back, you can create it
Can anyone tell me why the form at the end of this question isn't working like it should?
Save doesn't work
The select-helper doesn't select the
value according to the object #kid
The whole thing is based on Rails 2.2.2 and no, upgrading to Rails 2.3 to solve this problem isn't an option. :-)
I used this recipe to build the multiple model form.
# CLASS GRANDPARENT
class Grandparent < ActiveRecord::Base
has_many :parents
end
# CLASS PARENT
class Parent < ActiveRecord::Base
belongs_to :grandparent, :class_name => "Grandparent", :foreign_key => "grandparent_id"
has_many :kids
end
# CLASS KID
class Kid < ActiveRecord::Base
belongs_to :parent, :class_name => "Parent", :foreign_key => "parent_id"
# Virtual attribute setter for new self.parent.grandparent (Grandparent) attributes
def new_grandparent_attributes=(_gp_attributes)
self.parent.build_grandparent(_gp_attributes)
end
# Virtual attribute setter for existing self.parent.grandparent (Grandparent) attributes
def existing_grandparent_attributes=(_gp_attributes)
unless self.parent.grandparent.new_record?
attributes = _gp_attributes[self.parent.grandparent.id.to_s]
if attributes
self.parent.grandparent.attributes = attributes
else
self.parent.grandparent.delete(grandparent)
end
end
end
end
# CONTROLLER KIDS
class KidsController < ApplicationController
def new
#kid = Kid.new
end
def edit
#kid = Kid.find(params[:id])
end
def create
params[:kid][:new_grandparent_attributes] ||= {}
#kid = Kid.new(params[:kid])
end
def update
params[:kid][:existing_grandparent_attributes] ||= {}
#kid = Kid.find(params[:id])
end
end
# THIS IS THE MULTI-MODEL FORM USED IN THE VIEW
<% form_for(#kid) do |f| %>
<p>
<% new_or_existing = #kid.parent.grandparent.new_record? ? 'new' : 'existing' %>
<% prefix = "kid[#{new_or_existing}_grandparent_attributes][]" %>
<% fields_for prefix, #kid.parent.grandparent do |g_f| -%>
<p>
<%= g_f.label :, 'Grandparent Name' %><br />
<!-- THE FOLLOWING FORM DOESN'T CHANGE ACCORDING TO EXISTING #child -->
<%= #grandparents = Entity.find(:all, :order => :name)
g_f.collection_select(:name ,#grandparents, :id, :name)
%>
</p>
<% end %>
</p>
<p>
<%= f.label :name, "Kid Name" %><br />
<%= f.text_field :name %>
</p>
<%= submit_tag 'Go' %>
<% end %>
Well, correct me if I am wrong but it doesn't appear that you are actually saving the object anywhere. In your create and update actions you are calling new and then not saving it.
To rectify this you could do:
def create
params[:kid][:new_grandparent_attributes] ||= {}
#kid = Kid.new(params[:kid])
if #kid.save
# successful save logic here
else
#failed save logic here
end
end
def update
params[:kid][:existing_grandparent_attributes] ||= {}
#kid = Kid.find(params[:id])
if #kid.update_attributes(params[:kid])
#successful save logic here
else
#failed save logic here
end
end
Then in your select box you are trying to find every record of Entity, not those fields of Entity that are related to #kid. In order to do this you'll have to set up a relationship between kid and grandparent.
# CLASS GRANDPARENT
class Grandparent < ActiveRecord::Base
has_many :parents
has_many :grand_kids, :through => :parents
end
# CLASS PARENT
class Parent < ActiveRecord::Base
belongs_to :grandparent, :class_name => "Grandparent", :foreign_key => "grandparent_id"
has_many :kids
end
# CLASS KID
class Kid < ActiveRecord::Base
belongs_to :parent, :class_name => "Parent", :foreign_key => "parent_id"
belongs_to :grandparent
# ...
This way you can access a kid's grandparents through by #kid.grandparents. Then you can generate the select field:
<%= g_f.collection_select(:name ,#kid.grandparents, :id, :name) %>