Rails nested attributes don't create object from JSON string inside hidden form input
Hi I have a table that I fill out through a form and using parameters. This table has an FK for another table. The place where lat, lng and package address are stored. The Location table uses GeoKit.
My form has fields for the package and a field that allows the user to enter a location name. Google maps help the user fill in the details with autocomplete and save the results as json in a hidden field.
I am trying to use strong parameters like
private
def package_params
params.require(:package).permit( :state, :delivery_date, :length, :height, :width, :weight, destination: [:id, :address, :lat, :lng], origin: [:id, :address, :lat, :lng] )
end
I have also tried
private
def package_params
params.require(:package).permit( :state, :delivery_date, :length, :height, :width, :weight, destination_attributes: [:id, :address, :lat, :lng], origin_attributes: [:id, :address, :lat, :lng] )
end
but the origin and destination_ attributes are no longer passed in the parameters batch object.
Package model
class Package < ActiveRecord::Base
belongs_to :user
has_many :bids, dependent: :destroy
belongs_to :origin, :class_name => 'Location', :foreign_key => 'origin'
belongs_to :destination, :class_name => 'Location', :foreign_key => 'destination'
has_many :locations, autosave: true
accepts_nested_attributes_for :origin, :destination
....
end
location model
class Location < ActiveRecord::Base
acts_as_mappable
validates :address, presence: true
validates :lat, presence: true
validates :lng, presence: true
end
Create method
def create
@package = current_user.packages.build(package_params)
if @package.save
......
end
package.save doesn't work. This is the error I am getting.
ActiveRecord :: AssociationTypeMismatch in PackagesController # create Location (# 70350522152300) expected, got string (# 70350507797560)
I can think of a couple of workarounds, but I would like to get this working so that I can learn from it. I tried reading the api rails and googling this for a couple of days, but I couldn't get it to work.
Postal data
Parameters: {
"utf8"=>"✓",
"authenticity_token"=>"ZYkfpQBu6fvX7ZmzRw2bjkU+3i6mH0M7JLeqG4b99WI=",
"origin_input"=>"Kimmage, Dublin, Ireland",
"package"=>{
"origin"=>"{
\"address\":\"Kimmage, Dublin, Ireland\",
\"lat\":53.32064159999999,
\"lng\":-6.298185999999987}",
"destination"=>"{
\"address\":\"Lucan, Ireland\",
\"lat\":53.3572085,
\"lng\":-6.449848800000041}",
"length"=>"22",
"width"=>"222",
"height"=>"22",
"weight"=>"0 -> 5",
"delivery_date"=>"2014-10-31"},
"destination_input"=>"Lucan, Ireland",
"commit"=>"Post"}
I know the origin and destination have not been deserialized, but I do not know why they are not. Do I need to manually deserialize the string, and can I do it in package_para?
The form creating this looks like this
<%= form_for(@package, :html => {:class => "form-horizontal", :role => 'form'}) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="form-group">
<input type="text" name="origin_input" placeholder="From" onFocus="geolocate(); autocompleteLocation(this,package_origin)" class="form-control" />
<%= f.hidden_field :origin, class: "form-control" %>
</div>
<div class="form-group">
<input type="text" name="destination_input" placeholder="Destination" onFocus="geolocate(); autocompleteLocation(this,package_destination)" class="form-control" />
<%= f.hidden_field :destination, class: "form-control" %>
</div>
<div class="form-inline form-group">
<div class="input-group col-md-3">
<%= f.text_field :length, placeholder: "L", class: "form-control" %>
<span class="input-group-addon">cm</span>
</div>
<div class="input-group col-md-3">
<%= f.text_field :width, placeholder: "W", class: "form-control" %>
<span class="input-group-addon">cm</span>
</div>
<div class="input-group col-md-3">
<%= f.text_field :height, placeholder: "H", class: "form-control" %>
<span class="input-group-addon">cm</span>
</div>
</div>
<div class="form-group input-group">
<p>Please select the weight range of your package, Weights are in kg</p>
<% options = options_from_collection_for_select(@weights, 'weight', 'weight') %>
<%= f.select :weight, options, class: "form-control dropdown" %>
</div>
<div class="form-group">
<%= f.date_field :delivery_date, class: "form-control" %>
</div>
<%= f.submit "Post", class: "btn btn-large btn-primary", id: "package_post" %>
<% end %>
<%= render 'shared/places_autocomplete' %>
source to share
Problem
The error you are getting AssociationTypeMismatch
is caused by putting origin:
and destination:
in your strong_parameters. Rails thinks that you are trying to link objects the same way you would @post.comment = @comment
.
Even with proper serialization and deserialization of your parameters, this approach will not work. Rails sees what you are currently trying to use with strong_pairs:
# Not deserialized
@package.origin = '{ \"address\":\"Kimmage, Dulbin, Ireland\", ... }'
# Deserialized. However, this still won't work.
@package.origin = { address: "Kimmage, Dublin, Ireland", ...}
Rails wants an object in both cases. You can check this by logging into the console using a properly deserialized case:
$ rails c
irb(main): p = Package.new
irb(main): p.destination = { address: "Kimmage, Dublin, Ireland" } # => Throws ActiveRecord::AssociationTypeMismatch.
So why isn't it working? Because instead of passing it in as a real object, Rails interprets what you passed as a string or hash. To bind objects via strong_parameters, Rails looks up and uses the method accepts_nested_attributes
(which you tried). However, this won't work for you as described below.
The problem is how you are trying to link your data. Using nested attributes accepts is to bind and store child objects through the parent object. In your case, you are trying to link and save two parent objects (source and destination) through a child object (package) using the accepts_nested_attributes_for method. The rails won't work this way.
First line from the docs (emphasis mine):
Nested attributes allow you to persist the attributes of related records through the parent .
In your code, you are trying to link and save / update it via a child .
Decision
Solution 1
You will need origin_id
it location_id
in your form as well, excluding accepts_nested_attributes
from your model, as you won't need it, then save your package using ids:
params.require(:package).permit(:width, :length, :height, :whatever_else, :origin_id, :location_id)
Then, using AJAX requests before submitting your form, you paste in origin_id
and destination_id
out of those two into the hidden fields. You can use find_or_create_by method to create these locations after extraction if they don't already exist.
Solution 2
- Find or create parent resources
@destination & @origin
in the before_action file in the controller - Link
@origin
and@destination
with@package
You don't have to accept accept_nested_attributes_for anything. You can save the package as usual (make sure you change the package_params).
class PackagesController < ApplicationController
before_action :set_origin, only: [:create]
before_action :set_destination, only: [:create]
def create
@package = current_user.packages.build(package_params)
@package.destination = @destination
@package.origin = @origin
if @package.save
# Do whatever you need
else
# Do whatever you need
end
end
private
# Create the package like you normally would
def package_params
params.require(:package).permit( :state, :delivery_date, :length, :height, :width, :weight)
end
def set_origin
# You can use Location.create if you don't need to find a previously stored origin
@origin = Location.find_or_create_by(
address: params[:package][:origin][:address],
lat: params[:package][:origin][:lat],
lng: params[:package][:origin][:lng],
)
end
def set_destination
# You can use Location.create if you don't need to find a previously stored destination
@destination = Location.find_or_create_by(
address: params[:package][:destination][:address],
lat: params[:package][:destination][:lat],
lng: params[:package][:destination][:lng],
)
end
end
To make sure you have a package with a valid name and destination, check it in your model:
class Package < ActiveRecord::Base
validates :origin, presence: true
validates :destination, presence: true
validates_associated :origin, :destination
end
source to share