0

I have a nested form that creates an @address with each successful @purchase.

My schema and model associations are set up so that Purchase holds the foreign keys for everything else:

address.rb

belongs_to :user
has_many :purchases, foreign_key: :address_id
has_many :items, foreign_key: :address_id

purchase.rb

belongs_to :sender, class_name: "User"
belongs_to :recipient, class_name: "User"
belongs_to :address
belongs_to :item
accepts_nested_attributes_for :address

item.rb

belongs_to :user
has_many :purchases, foreign_key: :item_id
belongs_to :address

I've found some answers that suggest whitelisting the nested attribute params in my controller with a .permit(address_attributes: [:name]) in the controller instead of .permit(address: [:name]), but neither of the two create the record.

purchases_controller.rb

def confirmation
 @item = Item.friendly.find(params[:item_id])
 @address = @transaction_wizard.transaction.build_address
end

def confirm
 current_step = params[:current_step]
 @item = Item.friendly.find(params[:item_id])
 @transaction_wizard = wizard_transaction_for_step(current_step)
 @transaction_wizard.transaction.attributes = address_params
 session[:transaction_attributes] = @transaction_wizard.transaction.attributes

 if @transaction_wizard.valid?
  next_step = wizard_transaction_next_step(current_step)
  create and return unless next_step

  redirect_to action: next_step
 else
  redirect_to action: current_step
 end
end

def create
 if @transaction_wizard.transaction.save
  redirect_to root_path, notice: "Bid sent. If you would like to cancel before it's approved, you can do so #{view_context.link_to('here', transactions_sent_unapproved_path)}.".html_safe
 else
  redirect_to item_path(@transaction_wizard.transaction.item), notice: 'There was a problem making this bid.'
 end
end

private
def address_params
 params.require(:transaction_wizard).permit(address_attributes: [:name, :street, :street_2, :city, :state, :zip_code])
end

Whether I use address_attributes or address, the result is the same:

Parameters: {"transaction_wizard"=>
             {"address"=>
              {"name"=>"name", 
               "street"=>"123 st", 
               "street_2"=>"apt 5", 
               "state"=>"AL", 
               "zip_code"=>"48489"}
             }, 
             "current_step"=>"confirmation", 
             "commit"=>"Complete offer", 
             "item_id"=>"friendly-item-id"
            }
Unpermitted parameter: address

This is my form:

<%= form_for @transaction_wizard, as: :transaction_wizard, url: confirm_item_transaction_wizard_path(@item) do |f| %>
 <%= f.fields_for :address do |address| %>
  <%= address.text_field :name %>
  <%= address.text_field :street %>
  <%= address.text_field :street_2 %>
  <%= address.text_field :city %>
  <%= address.text_field :state %>
  <%= address.text_field :zip_code %>
 <% end %>

 <%= f.submit "Submit" } %>
<% end %>

What gives? How can I make my controller accept these params?


Update: Changing address to addresses requires that I change that @address variable in the controller to @address = @transaction_wizard.transaction.build_addresses. This causes an uninitialized constant Transaction::Addresses error.

calyxofheld
  • 1,538
  • 3
  • 24
  • 62
  • 1
    Did you have the `accepts_nested_attributes_for` on your Purchase model? – Dimitrius Lachi Mar 12 '19 at 19:03
  • i do! forgot to include that in my code sample – calyxofheld Mar 12 '19 at 19:05
  • 1
    You should use `_attributes`, may you can try to use the address plural, like `addresses`, `addresses_attributes` – Dimitrius Lachi Mar 12 '19 at 19:07
  • no matter what i do, console output always says `unpermitted parameter: address`. also, `purchase` belongs to everything else, so `address` should remain singular, not plural. i think? also, i tried the `_attributes`. it says it in the controller code – calyxofheld Mar 12 '19 at 19:36
  • 1
    Wait, you have a error in your relations: Address has_many purchases? you cant create a address form a purchase with this relations. You shoud use Purchase has_many address instead, i will detail it in a answer – Dimitrius Lachi Mar 12 '19 at 19:41
  • Is that an error though? I want addresses to store only the `user_id`, so that they can be referenced in other `purchases`. It seems to me that doing this would require that `purchase` hold the foreign keys. – calyxofheld Mar 12 '19 at 19:46
  • 1
    Why you don't relate it with a user model instead? – Dimitrius Lachi Mar 12 '19 at 19:50
  • Thank you for the help! My `address` is associated with the `user` model. It `belongs_to :user`. I could have something confused, but what I want is for the `purchase` model to hold the foreign keys for everything else - `sender`, `recipient`, `item`, and `address` - so that their records can be referenced in other `purchases`. A `sender` makes a `purchase` for an `item` and provides an `address`. The `item` has its own `address`, and was provided by a `recipient`. When the `purchase` is successfully transferred to the `sender`, the `item` `address` is changed to the new one – calyxofheld Mar 12 '19 at 20:03
  • 2
    Not only you have to use `address_attributes` in the strong params method, that's what your form should send too. – Sergio Tulentsev Mar 12 '19 at 21:06

2 Answers2

1

You should use address_attributes. The problems is your relations:

To use the accepts_nested_attributes_for you must use has_many or has_one, and you using belongs_to.

In your case, relations are inverted, see:

Purchase belongs_to Address

Address has_many purchases

You can't create a address because according your relations, address is a parent model (of purchase) see this link.

Nested Attributes is a feature that allows you to save attributes of a record through its associated parent.

Dimitrius Lachi
  • 1,277
  • 2
  • 9
  • 21
  • 1
    You can use `accepts_nested_attributes_for` on a `belongs_to` association - you just need to set `optional: true` as the presence validation will get in the way. – max Mar 12 '19 at 20:23
  • 1
    The OP actually has the correct setup if you want to be able to link a single address to multiple purchases yet keep track of where a specific purchase was delivered. Inversing the foreign keys or placing it on the users table does not do the job. – max Mar 12 '19 at 20:53
1

You can setup nested attributes through a belongs_to association but the association must be set as optional (unless you are using Rails 4 or earlier):

class Address < ApplicationRecord
  has_many :purcases
end

class Purchase < ApplicationRecord
  belongs_to :address, optional: true
  accepts_nested_attributes_for :address
end

require 'rails_helper'

RSpec.describe Purchase, type: :model do
  it "accepts nested attributes" do
    purchase = Purchase.create!(address_attributes: { name:  'foo', street: 'bar' })
    address = purchase.address
    expect(address.persisted?).to be_truthy
    expect(address.name).to eq 'foo'
    expect(address.street).to eq 'bar'
  end
end

This is because nested attributes is built to work with has_one associations so the "parent" record is validated before the nested record so that the foreign key (the parent id) can be set on the nested record.

You should be whitelisting address_attributes. Nested attributes creates a setter based on the first argument:

accepts_nested_attributes_for :foo
accepts_nested_attributes_for :bars

This will accept foo_attributes= and bars_attributes=. This of course should follow the plurization of your assocation (singular for has_one/belongs_to, plural for has_many).

The address= setter on your model has nothing to with nested attributes - its created by the belongs_to :address association and takes an Address instance. In fact if you whitelisted that param you will get an ActiveRecord::AssociationTypeMismatch exception.

irb(main):004:0> Purchase.new.address = { street: 'foo' }
ActiveRecord::AssociationTypeMismatch: Address(#70264542836680) expected, got {:street=>"foo"} which is an instance of Hash(#70264511691080)
    from (irb):4

You also need to setup the form as:

<%= f.fields_for :address do |address| %>
  # ...
<% end %>

When using fields_for just pass the name of the association. You should not need to pass an instance and using as: :address gives the wrong parameter.

max
  • 96,212
  • 14
  • 104
  • 165
  • Thank you for this! A couple things: `:address` *does* need to be present. And - I'm still getting the `Unpermitted parameter: address` in console, even with `optional: true` and whitelisted `address_attributes`, and your form setup. – calyxofheld Mar 13 '19 at 01:12
  • 1
    No - it has nothing to do with your associations. Rather you just have to make sure that your form is sending the right parameters and that you are whitelisting the correct parameters. I would start by reading the [Rails API docs on nested attributes](https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html) and the [guides on whitelisting](https://edgeguides.rubyonrails.org/action_controller_overview.html#strong-parameters) instead of just relying on wild guesses. – max Mar 13 '19 at 01:17
  • 1
    The reason you are getting `Unpermitted parameter: address` is that `f.fields_for @address, as: :address` creates the wrong parameters. – max Mar 13 '19 at 01:22
  • Thanks for continuing to offer help. I am using your suggestion for the form, tho: `<%= f.fields_for :address do |address| %>`. This whole issue is occurring in last step of a wizard (which uses `ActiveModel::Model`), and I'm wondering if that has something to do with it. – calyxofheld Mar 13 '19 at 01:28
  • 1
    Yeah, if you look at that params hash you can see that the root key is `transaction_wizard`. – max Mar 13 '19 at 01:30
  • 1
    Also you are not even passing the params whitelist to anything in your confirmation method. – max Mar 13 '19 at 01:31
  • I've updated my code with the `#confirm` and `#create` actions. The address params are being passed into `@transaction_wizard.transaction.attributes` and then into a session variable until everything is valid and can be created. Sorry for leaving that out before! I thought it was maybe not relevant. – calyxofheld Mar 13 '19 at 01:38
  • 1
    There still are way to many question marks for me to be able to troubleshoot this line by line, and its kind of out of scope of your original question. You might want to setup a simple example of nested attributes so that you actually understand how it works before trying to apply it to a very complex case – max Mar 13 '19 at 01:43
  • 1
    Nested attributes is one of those things that trip up almost everybody when learning due to the complexity and that you have to get the model, view and controller right. – max Mar 13 '19 at 01:47