Can I set values for accepts_nested_attributes_for children easily?

72 views Asked by At

As a trivial example, let's say I have:

class Garage
  has_many :things
  accepts_nested_attributes_for :things
end

class Thing
  # has a name attribute
  belongs_to :garage
  belongs_to :user
end

class User
...
end

I have a GaragesController that accepts a POST for a new garage and all the things in it.

def create
  @garage = Garage.create(safe_garage_params)
end

def safe_garage_params
  params.require(:garage).permit(...)
end

I have to set the user for each/all of the Things that are created. Obviously I can crawl the safe_garage_params hash and set user for each of the Thing hashes in the things_attributes array. But that seems pretty klutzy. Is there a better/cleaner way?

And, of course, in my actual program the child array can go a few tiers deep - which makes the crawling uglier.

2

There are 2 answers

3
Alex On

Maybe like this:

class Garage
  has_many :things
  accepts_nested_attributes_for :things
  
  attribute :things_user

  def things_attributes=(attributes)
    # you'll probably have to get as klutzy as it needs to be here
    attributes.each_value do |h|
      h[:user] ||= things_user
    end
    super
    # or maybe call super first and then set the user on the Thing models
  end
end
>> Garage.create_with(things_user: User.first).new({things_attributes: {"1": {name: "name"}}}).things
=> [#<Thing:0x00007fd7a48b4ce0 id: nil, name: "name", user_id: 1>]

With CurrentAttributes:

class Current < ActiveSupport::CurrentAttributes
  attribute :user
end

class Thing
  belongs_to :garage
  belongs_to :user, default: -> { Current.user }
end
Current.set(user: User.first) do 
  Garage.create(garage_params)
end

https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html

1
kwerle On

In rails 6, this works. But it's pretty violent and I'd like something a little less drastic.

ApplicationRecord
  class << self
    def with_default_scope(some_scope, &block)
      default_scope { some_scope }
      yield
    ensure
      default_scopes.pop
    end
  end
end

with

    new_things = Thing.where(user: some_user)
    garage = Thing.with_default_scope(new_things) do
      Garage.create!(name: "Bob's", things_attributes: [{}, {}])
    end
    expect(garage.things.count).to eq(2)
    expect(garage.things.first.user).to eq(some_user)
    expect(Thing.new.user).to be_blank

In rails 7 it would need some updating because DefaultScope and all_queries become a thing.