Rails nested attributes with existing records - ruby-on-rails

I'm trying to deal with nested attributes with the usual way:
Model:
class InventoryItem < ApplicationRecord
belongs_to :location
accepts_nested_attributes_for :location
end
Form:
<div class="field">
<%= form.fields_for :location do |location| %>
<%= location.label :location_name %>
<%= location.text_field :name %>
<% end %>
</div>
Controller:
def new
#inventory_item = InventoryItem.new
#inventory_item.build_location
end
def inventory_item_params
params.require(:inventory_item).permit(:location_id, location_attributes:[:name])
end
My problem is that I want that if the Location exists the new InventoryItem is associated with it.
I don't now how to rebuild the association between InventoryItem and Location in case of Location with the name exists. The: #inventory_item.build_location in the controller is allways creating a new Location.
Thanks in advance

The accepts_nested_attributes_for :location adds a method to your InventoryItem model: location_attributes=.
You have a special constraint on this method, so you need to override it in inventory_item.rb, something like this:
#inventory_item.rb
def location_attributes=(attrs)
begin
self.location_id = Location.find_by!(name: attrs[:name])
rescue ActiveRecord::RecordNotFound
super
end
end

Related

Repeating form fields and updating to database

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

can't write unknown attribute `company_profile_id`

I'm trying to add a set of users to a company_profile object. The idea is a user will create a company and then add more users to the company in various roles.
The company profile has an address object, and when I pull up the form on the new call I get this error:
"can't write unknown attribute company_profile_id"
company_profile -> new
<%= form_for(setup_companyProfile(#companyProfile), validate: true, html: { multipart: true }) do |f| %>
<%= f.fields_for :address do |address| %>
<%= render :partial => 'shared/address', :locals => {:f => address} %>
<% end %>
<% end %>
user.rb
belongs_to :company_profile
helper.rb
def setup_companyProfile(companyProfile)
if(companyProfile.address.present? == false)
companyProfile.address ||= Address.new
end
companyProfile
end
company_profile.rb
class CompanyProfile < ApplicationRecord
has_many :users
has_one :address
accepts_nested_attributes_for :address
end
company_profile_controller.rb
class CompanyProfileController < ApplicationController
def new
#companyProfile = CompanyProfile.new
end
def edit
#companyProfile = CompanyProfile.find(current_user.company_profile_id)
end
def update
end
def show
end
end
When you use has_one from the company-profile model... Rails expects there to be a belongs_to :company_profile on the Address model... and this belongs_to requires a column called company_profile_id on the addresses table... do you have that? If not - you will need to create a migration that adds it.

has_one and belongs to form

I'm rather new to rails and I'm stuck with this has_one and belongs_to form. I'm trying to create a team that has two speakers (from class 'User') through a form ,in the following manner:
class Team<ActiveRecord::Base
belongs_to :league
belongs_to :seed
has_many :speakers do
def user(level="1")
find_by(level: level).user
end
end
end
my user model looks like this :
class User < ActiveRecord::Base
belongs_to :team
end
user model:
class User
speaker model:
class Speaker < ActiveRecord::Base
belongs_to :team
belongs_to :user
end
my issue is (i think ) primarily in my controllers and form.controller looks like:
class TeamsController<ApplicationController
def new
#seed=Seed.find_by_id(params[:seed_id])
#league=current_admin.league
#team=current_admin.league.teams.build(:seed_id=>#seed,:approved=>false)
#usernames= #mca.connections.connected.each do |x| x.user end
end
def create
#league=current_admin.league
#team = #league.teams.build(team_params)
if #team.save
flash[:notice] = "Team Request Sent!."
redirect_to '/'
else
flash[:error] = "Unable to request team."
redirect_to :back
end
end
form looks like:
<div class="panel-body">
<div class="container">
<%= form_for #team do |f| %>
<%= f.hidden_field :seed_id, :value => #seed.id %>
<%= f.hidden_field :league_id, :value => #league.id %>
<div class="row">
<!-- <div class="col-md-8"> -->
<div class="form-group">
<%= f.collection_select :speaker, #usernames,:user,:fullname, multiple:true %>
</div>
<!-- </div> -->
</div>
<div class="actions">
<%= f.submit "Create" , class:"btn btn-primary" %>
</div>
<% end %>
</div>
</div>
I would really appreciate some help because it keeps throwing the following error:
NoMethodError in TeamsController#create
undefined method `each' for "2":String
The surface issue you have is that you're passing a string when Rails is expecting an object:
User(#69980837338020) expected, got String(#69980808947560)
This means you should be sending #user rather than "username" etc.
The error will likely be on this line:
#team = #league.teams.build team_params
... which means that you're passing :speaker (which Rails needs as an object) when you should be passing the speaker_id foreign key. Yury Lebedev's answer explains how to do this.
There is a deeper issue.
I don't see how each User can only belong to a Team:
class AddFieldsToUser < ActiveRecord::Migration
def change
add_column :users, :speaker_id, :integer
add_column :users, :speaker2_id, :integer
end
end
For this to work, your users can only be a member of one team.
Whilst this might work for a smaller scale product, I personally feel it to be an incorrect schema setup.
If anything, you'd expect the team to have speaker_1 and speaker_2, which would mean those two options being stored in the teams database (not user).
I think this is the cause of your problem (you're trying to set the speaker_1 and speaker_2 params when they don't exist in the teams db).
-
I would recommend the following:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :speaking_engagements, class_name: "Speaker"
has_many :teams, through: :speaking_engagements
end
#app/models/speaker.rb
class Speaker < ActiveRecord::Base
#columns team_id | user_id | level | created_at | updated_at
belongs_to :team
belongs_to :user
end
#app/models/team.rb
class Team < ActiveRecord::Base
has_many :speakers do
def user(level="1")
find_by(level: level).user
end
end
end
This will give you the ability to call:
#team = Team.find params[:id]
#speakers = #team.speakers
#user.speaking_engagements.where(team: #team)
To save it, you'll be able to use the following:
#app/controllers/teams_controller.rb
class TeamsController < ApplicationController
def new
...
#team = current_admin.league.teams.build seed: #seed, approved: false
end
def create
#league = current_admin.league
#team = #league.teams.build team_params
if #team.save
...
end
private
def team_params
params.require(:team).permit(:name, :speakers) #-> not sure about "speakers"
end
end
This should allow you to define the following:
#app/views/teams/new.html.erb
<%= form_for #team do |f| %>
<%= f.collection_select :speakers, #usernames, :id, :name, multiple: true %>
<%= f.submit %>
<% end %>

Virtual attributes in nested forms are not recorded

You'll see in my code I've got a "has_many => belongs_to" models association and a nested form in the new actions's view.
Plus, I've used Using two separate fields for the same parameter in a Rails form handler?
The problem is the "prix" attribute (in the "Projet" model) is recorded in the database, but not the "nom" attribute (in the "Activite" model).
Maybe the problem is around the strong parameters, but I thinks it's all good in my code...
Or maybe in the code I've found on the other Stackoverflow question I've linked.
There is french words in my code : activite is activity, projet is project, nom is name, prix is price, very easy :)
Thank's for your help !
app/model/projet.rb :
class Projet < ActiveRecord::Base
has_many :activites
accepts_nested_attributes_for :activites,
reject_if: lambda {|attributes| attributes['nom'].blank?}
end
app/models/activite.rb :
class Activite < ActiveRecord::Base
belongs_to :projet
def acttext=(value)
#acttext = value if value
prepare_act
end
def actselect=(value)
#actselect = value if value
prepare_act
end
def acttext
#acttext || self.nom
end
def actselect
#actselect || self.nom
end
private
def prepare_act
self.nom = acttext if acttext
self.nom = actselect if actselect
end
end
app/controllers/projets_controller.rb :
class ProjetsController < ApplicationController
def new
#projet = Projet.new
#activites_options = Activite.pluck(:nom).uniq
2.times { #projet.activites.new}
end
def create
#projet = Projet.new(projet_params)
if #projet.save
redirect_to #projet
else
render 'new'
end
end
private
def projet_params
params.require(:projet).permit(:prix, activites_attributes: [:id, :nom, :heures, :minutes, :acttext, :actselect])
end
end
app/views/projets/new.html.erb :
<div>
<%= form_for #projet do |f| %>
<%= f.label :prix %><br>
<%= f.text_area :prix %><br>
<ul>
<%= f.fields_for :activites do |activites_form| %>
<li>
<%= activites_form.label :choisissez_un_type_dactivité %>
<%= activites_form.select :actselect, #activites_options, {include_blank: true} %>
<%= activites_form.label :ou_créez_en_un_nouveau %>
<%= activites_form.text_field :acttext %><br>
<%= activites_form.label :heures %>
<%= activites_form.text_field :heures %>
<%= activites_form.label :minutes %>
<%= activites_form.text_field :minutes %>
</li>
<br>
<% end %>
</ul>
<p><%= f.submit "Créer le projet" %></p>
<% end %>
</div>
In order for virtual attributes to work, you'll also need to define a setter and a getter. This can be done by either defining the getters and setters explicitly yourself or by using attr_accessor.
Example using attr_accessor:
class Activite < ActiveRecord::Base
attr_accessor :nom
....
end
Example defining both setter and getter manually:
class Activite < ActiveRecord::Base
....
def nom=(value)
super
end
def nom
#nom
end
end
For the second issue with your class naming due to them being French, you'd also want specify the class_name option on both has_many and belongs_to to specify your non english class names as follows:
class Projet < ActiveRecord::Base
has_many :activites, class_name: 'Activite'
end
Similarly, for Activite model:
class Activite < ActiveRecord::Base
belongs_to :projet, class_name: 'Projet'
end

Rails: Creating a Multiple Model Form over n association levels

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) %>

Resources