views:

259

answers:

2

Artists have many Events. Events have many Artists. The join between these two models is called Performances.

Currently the Event form creates the Performance but creates a new artist for each Artist added to the Event form.

I would like the Event form to:

  1. Validate that an Artist can only be added to an Event once
  2. If an Artist with the same name already exists in the Artists table, create the association in the join table (Performances), but do not create another Artist
  3. If an Artist with the same name does not already exist, create it and the Performance

I've tried adding 'validates_uniqueness_of :name' to artist.rb but this prevents the event from being saved. The join (performance) should be created if it does not already exist and the artist should be created if it does not already exist, but the existence of the artist should not prevent the join/association from being created.

event.rb

validates_presence_of :name, :location
has_many :performances, :dependent => :destroy
has_many :artists, :through => :performances
accepts_nested_attributes_for :artists, :reject_if => proc {|a| a['name'].blank?},     :allow_destroy => true

artist.rb

has_many :performances
has_many :events, :through => :performances

perfomance.rb

belongs_to :artist
belongs_to :event

events_controller.rb

def create
  @event = Event.new(params[:event])

  respond_to do |format|
    if @event.save
      flash[:notice] = 'Event was successfully created.'
      format.html { redirect_to(admin_events_url) }
      format.xml  { render :xml => @event, :status => :created, :location => @event }
    else
      format.html { render :action => "new" }
      format.xml  { render :xml => @event.errors, :status => :unprocessable_entity }
    end
  end
end

_form.html.erb

<% form_for([:admin,@event]) do |f| %>
<p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
</p>
<p>
    <%= f.label :location %><br/>
    <%= f.text_field :location %>
</p>
<p>
    <%= f.label :date %><br />
    <%= f.date_select :date %>
</p>
<p>
    <%= f.label :description %><br />
    <%= f.text_area :description %>
</p>
<% f.fields_for :artists do |builder| %>
    <%= render 'artist_fields', :f => builder %>
<% end %>
<p><%= link_to_add_fields "Add Artist", f, :artists %></p>
<p>
    <%= f.submit 'Submit' %> <%= link_to 'Cancel', admin_events_path %>
</p>
<% end %>

artist_fields.html.erb

<p class="fields">   
<%= f.label :name, "Artist"%><br/>
<%= f.text_field :name %>
<%= link_to_remove_fields "remove", f %>
</p>
A: 

You have the proc to reject artist attributes if the name is blank: you could also reject it if the artist already exists on the model, but that doesn't solve the problem of duplicate Artists. Essentially you want to do a find_or_create_by_name when adding them to your event.

I think in your case it would be better to define your own artist_attributes= method instead of relying on accepts_nested. This way you can do your lookup for each artist name and add it only as needed:

def artist_attributes=(params)
  if existing_artist = Artist.find_by_name(params[:name])
    self.artists << existing_artist unless self.artists.include? existing_artist
  else
    self.build_artist(params)
  end
end
Andrew Vit
I've been researching what you've suggested but can't figure out how to start.
shalako
So I need to add a custom method to the Events model, and incorporate find_or_create_by_name somehow. Will I need to remove accepts_nested_attributes_for? Do I need to use virtual attributes, as Ryan Bates suggests in Railscasts #102? Before_save, after_save? I haven't found a good example of how these things fit together.
shalako
I added an example of the artist_attributes= method above. I think you'll still need accepts_nested if you want to take advantage of `f.fields_for :artist`
Andrew Vit
A: 

You should really take a look at these Railscasts:

  1. Don't create various artists. Just use the existing one (or create if needed):

    http://railscasts.com/episodes/167-more-on-virtual-attributes

  2. You can also check these nested form railscasts (part one linked here):

    http://railscasts.com/episodes/196-nested-model-form-part-1

  3. For the validation, you can just have a method do the just-once-in-the-event validation for you. Something like (at Event.rb):

    validate :artists_appear_just_once
    
    
    private
    def artists_appear_just_once
      self.artists.size == self.artists.uniq.size
    end
    

Alternatively you can make the artsits appear just once by default using the uniq! method before saving. Just call a before_save hook and process the artists array...

 before_save :make_artists_unique

 private
 def make_artists_unique
   artists.uniq!
 end

Hope I got what you need right :P

Hock