3

Hi I have a table Package that I populate through a form and using the params. This table has a FK to another table Location that holds the lat, lng and address of the package. The Location table uses GeoKit.

My form has fields for the package and a field that allows the user to enter in the name of a location. Google maps helps the user fill in the details with autocomplete and saves the results as json in a hidden field.

I am trying to use strong params as

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 origin & destination _attributes are no longer being passed through in the package object of the params.

The package model is

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

the location model is

class Location < ActiveRecord::Base
    acts_as_mappable

    validates :address, presence: true
    validates :lat, presence: true
    validates :lng, presence: true
end

The create method is

def create
    @package = current_user.packages.build(package_params)
    if @package.save
......
end

package.save is failing. This is the error that I am receiving.

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 I learn from it. I've tried reading the rails api and googling this for a couple of days but I haven't been able to get it too work.

The post data is

  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 aren't been deserialized, but I don't know why they aren't. Do I have to manually deserialize the string and can I do this in package_params ?

The form that creates this is as follows

<%= 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
  • You'll have to parse the JSON that Google returns before using the params. Commented Oct 20, 2014 at 20:47
  • so would I parse the json in another method json_params in private and then replace :destination => [...] to :destination => json_params or can I parse the json in package_params ? Commented Oct 20, 2014 at 21:37
  • I found this blog article and it looks like what I'm trying to do is right. But I can't see what is different Commented Oct 22, 2014 at 22:06

1 Answer 1

4

Problem

The error you are receiving AssociationTypeMismatch is caused by putting origin: and destination: in your strong_params. Rails thinks you are trying to associate objects much like you would do @post.comment = @comment.

Even with proper serialization & deserialization of your params this approach won't work. Rails sees what you are currently trying with strong_params as this:

# 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 test this by going into your console using the 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 an actual object, Rails interprets what you've passed as a string or a hash. In order to associate objects through strong_params, Rails looks for and uses the accepts_nested_attributes method (which you've tried). However, this won't work out for you as explained below.

The problem here is the way you are trying to associate your data. Using accepts nested attributes is to associate and save child objects through a parent object. In your case you are trying to associate and save two parents objects (origin & destination) through a child object (package) using the accepts_nested_attributes_for method. Rails won't work this way.

First line from the docs (emphasis mine):

Nested attributes allow you to save attributes on associated records through the parent.

In your code, you're trying to associate and save/update it through the child.


Solutions

Solution 1

What you would need is the origin_id and location_id in your form, excluding accepts_nested_attributes from your model since you won't need it, and then saving your package using the ID's:

params.require(:package).permit(:width, :length, :height, :whatever_else, :origin_id, :location_id)

Then, using AJAX requests before your form is submitted you insert the origin_id and destination_id of those two locations into hidden fields. You can use a find_or_create_by method to create those locations upon retrieval if they do not exist yet.

Solution 2

  • Find or create your parent resources @destination & @origin in a before_action in your controller
  • Associate the @origin and @destination to the @package

You will not need to accept_nested_attributes_for anything. You can save the package as you would normally (ensure to modify 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 ensure you have a package with a valid origin and destination then validate that in your model:

class Package < ActiveRecord::Base
  validates :origin, presence: true
  validates :destination, presence: true

  validates_associated :origin, :destination
end
Sign up to request clarification or add additional context in comments.

2 Comments

Great thanks ill try this out on monday and see if it works if it does the rep is yours
Thanks that worked, I had to change the code for set_destination and set_origin. I have edited the answer to show the changes

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.