how to save data to two models from one form - ruby-on-rails

I have a scenario where I would like to save data to two models from one form.
Basically I have a player which belongs to many teams. So in the new action of players_controller I'd like to have a multiple select box that contains all the teams. then the user can select 2 or 3 of them..click save and they will be saved.
player belonging to many teams is done by a table called playerizations it contains a player_id and team_id columns
so if I want to get all the teams a player belongs to. I just say player.teams
all this relationship is working fine. I would just like to know how to save to playerizations table when new player is created
What I have (it is basically scaffolding model):
def new
#player = Player.new
#teams = Team.all
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => #player }
end
view:
<% form_for(#player) do |f| %>
<%= f.error_messages %>
...
....
All Teams<br/>
<%= select_tag 'selected_teams[]', options_for_select(#teams.map {|t| [t.team_name, t.id]}), :multiple => true%>
end
Can I get some hints please? I took a look at railscast regarding this but not much help.

You're describing a has-and-belongs-to-many ("HABTM") relationship but you have not defined it according to Rails convention, so Rails isn't sure how it should update your models.
Player model should say:
has_and_belongs_to_many :teams
Team model should say:
has_and_belongs_to_many :players
This has the happy side effect that not only does "player.teams" give a list of a player's associated teams, but also "team.players" gives the list of the players in a given team.
Your join table must be called "players_teams", because the Rails convention is to use the name of the two models in plural form and joined together in ascending alphabetical order. Renaming your "playerizations" table should be sufficient since it sounds like the table columns are correct.
Your select menu code is almost there; you need something like:
select_tag(
:player_team_ids,
options_for_select( #teams.map { | t | [ t.team_name, t.id ] } ),
{
:multiple => true,
:name => 'player[team_ids][]'
}
)
It's the "name" assignment that contains the 'magic' to get your team IDs array assigned. The first parameter to "select_tag" is just the form field's name of "player[team_ids][]" with the square brackets turned into underscores or stripped off if at the end of the string, thus generating a recognisable and unique ID for use in the output HTML.
You can then save your player model or update its attributes with standard calls to save() or update_attributes() - no need for additional code per se however Rails falters on validations. If you are editing an existing player's details, then a call to "update_attributes" will result in the teams association being updated first. Then the player is updated; if its validations fail, the team changes will have been saved anyway. It's quite simple to patch around; wrap your call to update_attributes() in a transaction and roll back if update_attributes returns 'false' indicating failure.
success = Player.transaction do
player.update_attributes( params[ :player ] ) ) or raise ActiveRecord::Rollback
end
The value of 'success' will end up being 'true' for success or 'nil' for failure. This works because the Rollback exception is caught by the transaction block and does not propagate. Setting 'success' to the evaluated result of the block rather than trying to use local variables means that the code is both Ruby 1.8 and Ruby 1.9-friendly.
This is only necessary when updating existing records with HABTM relationships. It is not required when creating new records.
All of the above code is untested and may contain typing errors, so please use with due care and attention.
For more on HABTM:
http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
For more on transactions:
http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html

Related

Rails accepts_nested_attributes_for with belongs_to. Why I can't set id?

I use Rails 5.1.6 and have troubles with accepts_nested_attributes_for.
I have two models
class Material < ApplicationRecord
belongs_to :rubric, optional: true
accepts_nested_attributes_for :rubric
end
class Rubric < ApplicationRecord
has_many :materials, dependent: :nullify
end
I try to set rubric id to new item by rubric_attributes.
describe 'create material' do
it 'should set rubric: :id' do
# prepare
item = FactoryBot.build(:material)
rubric = FactoryBot.create(:rubric)
# action
item.assign_attributes(
rubric_attributes: {
id: rubric.id
}
)
# check
expect(item.valid?).to eq(true)
expect(item.save).to eq(true)
expect(item.rubric_id).to eq(rubric.id)
end
end
But I have an error:
Failure/Error:
item.assign_attributes(
rubric_attributes: {
id: rubric.id
}
)
ActiveRecord::RecordNotFound:
Couldn't find Rubric with ID=1 for Material with ID=1
And I have the same error with updating a material.
Is it a predictable behavior of accepts_nested_attributes_for, and I can't use rubric_attributes for setting existed rubric id?
Docs say:
For each hash that does not have an id key a new record will be instantiated, unless the hash also contains a _destroy key that evaluates to true.
It suggest that if you pass id in nested attributes, it's treated as an existing record that should be updated.
You most likely don't need accepts_nested_attributes_for in the first place.
If you want the user to be able to select records with a select you don't actually need to do anything besides create a select and whitelist the material_id attribute:
<%= form_for(#material) do |f| %>
<div class="field">
<%= f.label :rubic_id %>
<%= f.collection_select :rubic_id, Rubic.all :id, :name %>
</div>
<%= f.submit %>
<% end %>
The select will create an array in the params.
class MaterialsController
# POST /materials
def create
#material = Material.new(material_params)
if #material.save
redirect_to #material
else
render :new
end
end
private
def material_params
params.require(:material)
.permit(:foo, :bar, material_ids: [])
end
end
accepts_nested_attributes_for is really intended for the case where you need to create / edit nested resources in the same request. The only reason you would use it here is:
The user should be able to create the material in the same form.
You have a join table with additional attributes (like quantity for example) which you want the user to be able to set.
You can still do 1. together with the select above, but you can't use accepts_nested_attributes_for to set a simple belongs_to association. Nor would you want to either as its like using a rocket to beat in a nail.
Just leaving this in case somebody else may have a problem as I did, populating nested children records in a Rails backend via an API, but using hash_ids via friendly_id.
Came about this when trying to PATCH Rails records via an API. First setup was to mirror the Rails way of sending the record values in nested form fashion. Meaning, I've purposefully built the params hash I was sending from the frontend over to a Rails backend like in a typical nested form transmission:
{ "children": {
"0": {
"name": "foo",
"category_id": "1",
"hash_id": "HDJPQT"
}
}
accepts_nested_attributes_for needs id to PATCH records. Otherwise it is going to create a new record altogether. Which i did not want in my scenario. I was sending over hash_id and therefore creating new records unintentionally.
Solution
For the time being I am not replicating a nested form value hash anymore to send to the Rails backend anymore. Instead I am simply updating children records separately in a chained fetch query from the Javascript frontend.
Note:
If you want to keep sending a mirrored nested form array of hashes, there could be a way to change the primary key of the database table to hash_id or UUID, depending on your needs. Have not yet tested this solution.

Rails Creation through association & Collection vs. Array

I am confused about the some Association concepts in Active Records.
I have three models User, Bank and Bankaccount. Both the User and the Bank models "has_many" Bankaccounts and the Bankaccount model "belongs_to" both the User and the Bank models. I use the following syntax to create a Bankaccount through its association with User
#bankaccount = #user.bankaccounts.create(bankaccount_params)
What is the appropriate syntax if I want to create a bankaccount object through both the association with User and the association with Bank?
My second question is related to this one. Right now, because I am not sure how to create a bankaccount through both associations, I handle the association with the Bank by putting the parameter manually
bank_id = params[:bank_id]
However, this seems to trigger some issues down the road when I want to iterate through all the bankaccounts and retrieve the name of the associated bank.
In my view I have
<% #bankaccounts.each do |bankaccount| %>
<%= bankaccount.bank %>
I obtained a list of these
#<Bank:0x007f7a66618ef0>
#<Bank:0x007f7a664c9ab8>
If I tried to get the name of the bank
<% #bankaccounts.each do |bankaccount| %>
<%= bankaccount.bank.name %>
I get an undefined method name for nil class. I do get the name of the bank in the console with these simple lines
bankaccount = Bankaccount.find(1)
bankaccount.bank.name
Could you anyone give me more background on those concepts and provide me with the appropriate syntax to loop accross my collection #user.bankaccount and for each bankaccount retrieve the name of the associated bank?
Thanks.
You'll have to choose one association to create a bankaccount through, then set the second separately:
#bankaccount = #user.bankaccounts.new(bankaccount_params)
#bankaccount.bank = somebank
#bankaccount.save
Or
#bankaccount = #bank.bankaccounts.new(bankaccount_params)
#bankaccount.user = someuser
#bankaccount.save
In addition, I don't see why setting the second association manually with a param would inherently cause the other problems you are experiencing. This should be fine (assuming a bank with this id actually exists):
#bankaccount.bank_id = params[:bank_id]
If you choose to assign a foreign key as a parameter, you can roll it into strong parameters and pass it into the bankaccount model with everything else. For example:
def bankaccount_params
params.require(:bankaccount).permit(:bank_id, ...)
end
You last issue regarding arrays vs. collections depends on what you are trying to do. First, if you are particularly interested in the bankaccount's bank name, make it easier to get:
class Bankaccount
belongs_to :bank
...
def bank_name
bank.name
end
end
For those who buy into such things, this also prevents a Law of Demeter violation.
If you are simply trying to list the names of banks for #bankaccounts in a view, try leveraging Rails partials with something like this:
app/views/bankaccounts/index.html.erb
<%= render #bankaccounts %>
app/views/bankaccounts/_bankaccount.html.erb
<%= bankaccount.bank_name %>
More on this here: http://guides.rubyonrails.org/layouts_and_rendering.html#using-partials
If you're looping over #bankaccounts for another reason, the code you provided should work, given that #bankaccounts represents ActiveRecord relations and not a simple array:
<% #bankaccounts.each do |bankaccount| %>
<%= bankaccount.bank_name %>
<% end %>
Since you're getting an undefined method error, your problem probably stems from how you are building #bankaccounts. If you are doing exactly this...
#bankaccounts = #user.bankaccounts
...and you've verified that everything is properly associated in the console, then your problem is likely unrelated to arrays or collections.

How to correctly pass through foreign keys in Rails form_for

I currently have a working form to create a resource (An event booking) which belongs_to two other models, a Consumer (the customer) and a Course. In the Booking creation form, I'm using two hidden fields which pass through consumer_id and course_id.
For this to work in form_for, I've created two virtual attributes in my Booking model
attr_accessor :course_id, :consumer_id
And in the create event of BookingsController, I've grabbed those ID's from mass assignment and then manually assigned the actual Course and Consumer objects from the ID
bookings_controller.rb
def create
#booking = Booking.new(booking_params)
#booking.course = Course.find(#booking.course_id)
#booking.consumer = Consumer.find(#booking.consumer_id)
if #booking.save_with_payment
# Payment was successful, redirect to users account page to view it and past bookings
else
render :new
end
end
private
def booking_params
params.require(:booking).permit(:course_id, :consumer_id, :card_token, :visible, :created_at)
end
Is this best practice? I tried to name the form hidden fields as consumer and course, hoping that Rails would see that the value is an ID and automatically do a .find for me, but that doesn't appear to be the case. I'll be surprised if Rails can't take care of this automatically, I'm just not sure how to accomplish it.
It's simpler than you imagine and you're already most of the way there.
When you create a booking, you need only to set the course_id and consumer_id fields, so make sure you've got hidden fields set up in your form with these names and the right values:
<%= f.hidden_field :course_id, value: my_course_id %>
<%= f.hidden_field :consumer_id, value: my_consumer_id %>
Don't set course or consumer in your controller or in your form. That is, remove the following lines from your controller:
#booking.course = Course.find(#booking.course_id)
#booking.consumer = Consumer.find(#booking.consumer_id)
You already have course_id and consumer_id in your permit list, so when you post the form, the values for those parameters will be set on your new booking, which is all that you should care about.
When you attempt to access #booking.course, ActiveRecord will do a find for you based on the id set in course_id; this is handled by the belongs_to association that you've established in your model.

How to pass object id?

I have a relationship between two models
Category Model
class Category < ActiveRecord::Base
belongs_to :product
end
Product Model
class Product < ActiveRecord::Base
has_many :categories
end
I have category_id in products table but When I create a new product in my products table
category_id is null. I am new to rails can anyone help please?
First off, a thought - most of the time, products have many categories, but each category also contains many products. Perhaps your association should be a many-to-many? On to your actual question.
If I understand correctly, your question is really about how to create categories and products that are related to each other in the database, i.e. how to set the value of product_id when building a new category.
For clarity in case you need it, product_id would only be set for a category. The category, after all, belongs to that product, so it has to hold its owner's ID.
So, let's say you want to build a new category that belongs to an existing product - you can do this:
# in your view, where you link from products/show.html.erb to category/new.html.erb
<%= link_to "Build new category for this product", new_category_url(:id => #product.id) %>
# you must have #product defined, and you also must have
# 'resources :categories' in your routes file
# in your controller action categories/new, set the new category's product id:
def new
#category = Category.new(:product_id => params[:id])
end
# include a hidden field to contain the product_id in your new form
<%= form_for #category do |f| %>
<%= f.hidden_field :product_id %>
... other fields, labels, etc.
<% end %>
# save the record as you normally would (analogous to the code in your comment to #Chowlett).
#category = Category.new(params[:category])
if #category.save
redirect_to :action => "list", :notice => "Category saved successfully."
else
render :action => "new"
end
The above code allows you to build a product, then each category one-by-one. So, we are building your product first, then including a link from the product/show page to your category/new form, passing in the ID of the product you want that category to be part of.
If you want to build a product and some categories at the same time, it is a bit more complicated. For more information on this, take a look at http://railscasts.com/episodes/196-nested-model-form-part-1 (this is the first of a three-part series) and https://github.com/ryanb/nested_form. I don't suggest this course of action unless you are very comfortable with the above basics. I once got mired in this code for a week when I was new to Rails!
First off, you have the _id field in the wrong table. If Category belongs_to :product, then your categories table needs a field product_id.
Look at it this way: each Product can have many Categories - so what single value would you expect to find in a category_id field?
If you still have problems after correcting that, let me know.
Edit: Once you've got your tables set up, you still need to tell Rails what the link should be. You've got a few options. Assuming you've got a Category in hand, your best bet is new_prod = my_cat.create_product(). Alternatively, you can use new_prod = Product.create(:category => my_cat).
Later on, you can associate the models together like this:
my_prod.category = my_cat
my_prod.save

Rails Foreign Key Issue

Sorry if this question seems simple, I am very very new to Rails (just started learning a few days ago), but after consulting Google and "Agile Web Development with Rails" I can't find the answer.
I have an issue with Rails 2.3.8 creating a foreign key on two models. My tables look like this:
cars manufacturer
---- ------------
car_make name
car_model country
car_class logo_url
image_url (and default 'id' created by Rails)
manufacturer_id
(and default 'id' created by Rails)
My 'car_make' and 'name' fields are essentially the same; every Car I create, I want to be able to associate it with an existing Manufacturer. This is the column I am trying to create FK on.
My car.rb has 'belongs_to :manufacturer', and my manufacturer.rb has 'has_many :cars' to establish a one manufacturer to many cars relationship. However, when I create a new car (via scaffolding) the manufacturer_id field is blank.
I went to my cars_controller, found the 'create' method that is being used, and tried to add the second line below:
#car = Car.new(params[:car])
#car.manufacturer_id = car.manufacturer.id # <===
This produces a 'NameError in CarsController#create' error, and I see:
undefined local variable or method 'car' for #<CarsController:0x1034642f0>
Rails doesn't seem to like the line I've added. What am I missing to make this work?
Well, you need to have a manufacturer available before you can attach it to the car.
#car = Car.new( params[:car] )
m = Manufacturer.first # => as you can see you must already have one made
#car.manufacturer = m
#car.save
The reason car is undefined is because, well, you haven't defined it. Which car's manufacturer did you want to assign to #car?
So basically you need to make a manufacturer before you make a car. If the form you're filling out has the data for the manufacturer then make sure to put that under a different key in params, like, say, params[:manufacturer] and do a similar thing as you're doing with the car. Maybe like:
#car = Car.new( params[:car] )
#manufacturer = Manufacturer.find_or_create_by_name_and_country( params[:manufacturer][:name], params[:manufacturer][:country] )
#car.manufacturer = #manufacturer
#car.save
In your view, you want to generate a drop-down list for manufacturers (I would assume), so you should do something like this in the form:
<%= collection_select(:car, :manufacturer_id, Manufacturer.all, :id, :name) %>
Then your create action shouldn't need to explicitly set a manufacturer_id because it should have received that from the form.

Resources