views:

70

answers:

1

I have a Card model that has many CardSets and a CardSet model that has many Cards through a Membership model:

class Card < ActiveRecord::Base
  has_many :memberships
  has_many :card_sets, :through => :memberships
end

class Membership < ActiveRecord::Base
  belongs_to :card
  belongs_to :card_set

  validates_uniqueness_of :card_id, :scope => :card_set_id
end

class CardSet < ActiveRecord::Base
  has_many :memberships
  has_many :cards, :through => :memberships

  validates_presence_of :cards
end

I also have some sub-classes of the above using Single Table Inheritance:

class FooCard < Card
end

class BarCard < Card
end

and

class Expansion < CardSet
end

class GameSet < CardSet
  validates_size_of :cards, :is => 10
end

All of the above is working as I intend. What I'm trying to figure out is how to validate that a Card can only belong to a single Expansion. I want the following to be invalid:

some_cards = FooCard.all( :limit => 25 )

first_expansion = Expansion.new
second_expansion = Expansion.new

first_expansion.cards = some_cards
second_expansion.cards = some_cards

first_expansion.save    # Valid
second_expansion.save   # **Should be invalid**

However, GameSets should allow this behavior:

other_cards = FooCard.all( :limit => 10 )

first_set = GameSet.new
second_set = GameSet.new

first_set.cards = other_cards    # Valid
second_set.cards = other_cards   # Also valid

I'm guessing that a validates_uniqueness_of call is needed somewhere, but I'm not sure where to put it. Any suggestions?

UPDATE 1

I modified the Expansion class as sugested:

class Expansion < CardSet 
  validate :validates_uniqueness_of_cards

  def validates_uniqueness_of_cards
    membership = Membership.find(
      :first,
      :include => :card_set,
      :conditions => [
        "card_id IN (?) AND card_sets.type = ?",
        self.cards.map(&:id), "Expansion"
      ]
    )
    errors.add_to_base("a Card can only belong to a single Expansion") unless membership.nil?
  end
end

This works! Thanks J.!

Update 2

I spoke a little too soon. The above solution was working great until I went to update an Expansion with a new card. It was incorrectly identifying subsequent #valid? checks as false because it was finding itself in the database. I fixed this by adding a check for #new_record? in the validation method:

class Expansion < CardSet
  validate :validates_uniqueness_of_cards

  def validates_uniqueness_of_cards
    sql_string = "card_id IN (?) AND card_sets.type = ?"
    sql_params = [self.cards.map(&:id), "Expansion"]

    unless new_record?
      sql_string << " AND card_set_id <> ?"
      sql_params << self.id
    end

    membership = Membership.find(
                   :first,
                   :include => :card_set,
                   :conditions => [sql_string, *sql_params]
                 )

    errors.add_to_base("a Card can only belong to a single Expansion") unless membership.nil?
end
+1  A: 

I'm really not sure about that, I'm just trying because I'm not able to test it here... but maybe something like the following works for you. Let me know if it does :]

class Expansion < Set 
   validate :validates_uniqueness_of_cards

   def validates_uniqueness_of_cards
      membership = Membership.find(:first, :include => :set,
         :conditions => ["card_id IN (?) AND set.type = ?",
            self.cards.map(&:id), "Expansion"])
      errors.add_to_base("Error message") unless membership.nil?
   end
end
j.
I'm going to test adding `validates_uniqueness_of :card_ids` to the Expansion class when I get home. Could it be that simple? (on an unrelated note: trying to update a comment on StackOverflow with your iphone is a terrible idea! They really need to implement a mobile view for this site!)
irkenInvader
nope. My idea didn't work. I've got access to Rails now, I'll experiment some with you idea.
irkenInvader
Thanks again, J. I updated my question with my final validation method. I had to add a check so an Expansion would ignore itself in future `#update`s, but other than that you're solution was perfect.
irkenInvader
I completely forgot this `update` case. But I'm glad my answer helped you :]
j.