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

      

+3


source to share


1 answer


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

      

+4


source







All Articles