3

I'm using Rails' accepts_nested_attributes_for method with great success, but how can I have it not create new records if a record already exists?

I have found this article, accepts_nested_attributes_for with find_or_create? but it doesn't seem to work in my case.

I have a many-to-many relationship between packages and licenses. There should only be one instance of a license in the table, for instance: I shouldn't have two licenses named, "Awesome"

The desired functionality is that when I create a package, passing in a license name as an attribute, create a new license if the name doesn't already exist or create an association between the existing license (with the provided name) and the package.

Here are what my models look like:

class Package < ActiveRecord::Base
  has_and_belongs_to_many :licenses, :autosave => true
  accepts_nested_attributes_for :licenses
end

class License < ActiveRecord::Base
  attr_accessible :name
  has_and_belongs_to_many :packages
  validates :name, :presence => true
end
Community
  • 1
  • 1
Brett Hardin
  • 4,012
  • 2
  • 19
  • 22

2 Answers2

0

If 'name' really is the only attribute of License that would be used to create it if it didn't already exist, then you could do this via a callback on the Package class and a virtual attribute:

class Package < ActiveRecord::Base
   has_and_belongs_to_many :licenses, :autosave => true
   before_create :set_license_from_name
   attr_accessor :license_name       

   protected

      def set_license_from_name
         self.licenses << Licence.find_or_create_by_name(self.license_name)
      end

end

Then you could could just pass in licence_name as an attribute on the new Package object, eg using a text_box:

<%= form_for @package do |f| %>
  <%= f.text_field :license_name %>
<% end %>

Probably not the best UI in the world though. Instead, you may want people to be able to select an existing licence from a dropdown box, or type in the name of a new one. There is where accepts_nested_attributes_for is useful:

class Package < ActiveRecord:Base
   has_and_many :licenses
   accepts_nested_attributes_for :licenses, :reject_if => :all_blank
end

class PackagesController < ApplicationController

   def new
      @package = Package.new
      @package.licenses.build
      @licenses = License.all
   end
end

And in the view:

<%= form_for @package do |f| %>

  License:
  <%= f.collection_select :license_id, @licenses, :id, :name, :include_blank => true %>

  or create a new one:
  <%= f.fields_for :licenses do |l| %>
     <%= l.text_field :name %>
  <% end %>
<% end %>

You'll have to do a little bit more work in the create action of the controller to get this to work, but it shouldn't be too hard.

Frankie Roberto
  • 1,349
  • 9
  • 9
  • I am using the << method for adding the licenses via a script. In order to test the model, I have the following rspec script `describe "licenses" do it "should only create a single instance of a license" do @package.licenses.create!(:name => "TEST") count = License.all.count @package.licenses.create!(:name => "TEST") License.all.count.should == count end end` Even after implementing the above, the script still seems to fail. – Brett Hardin Jun 24 '11 at 02:04
  • Roberto Any help on this? It still doesn't seem to work. Is there any way to the same logic at the License level and not the package level? – Brett Hardin Jun 29 '11 at 04:01
0

I had a similar issue and solved it with callback hooks. In my situation, I have a Status model and a Person model. I wanted to let users tag multiple people (Person) in their Status update by just typing in a names and it creating a new Person (these do no map to Users, btw, it's just for private use) if a matching one does not exist.

class Person < ActiveRecord::Base
  has_and_belongs_to_many :statuses
end

Class Status < ActiveRecord::Base
  has_and_belongs_to_many :people
  accepts_nested_attributes_for :people
  before_save :get_people

  def get_people
    self.people.map! do |person|
      Person.find_or_create_by_name(person.name)
    end
  end
end
Zac
  • 185
  • 2
  • 17
  • in my case `def get_people` `self.people.map! do |person|` `Person.find_or_create_by_name(person.name)` `end` `end` is not working but `def get_people` `self.people = self.people.collect do |person|` `Person.find_or_create_by_name(person.name)` `end` `end` does! – Muntasim Jun 06 '13 at 07:45