Andrew
Andrew

Reputation: 43153

Rails validation among a three model relationship

I'm working on a three model relationship with one aspect that I'm not sure how to approach. Here's the basic relationship:

class Taxonomy
  has_many :terms
  # attribute: `inclusive`, default => false
end

class Term
  belongs_to :taxonomy
  has_and_belongs_to_many :photos
end

class Photo
  has_and_belongs_to_many :terms
end

This is pretty straightforward stuff except for one thing:

A Taxonomy can be either 'Inclusive' or 'Exclusive'. Exclusive means the terms are mutually exclusive, Inclusive means they're not.

Now, I can handle this on the client-side without a problem, but I am not quite sure how to set up a validation on Photos (or somewhere else) that says basically: "validate that no more than one term from an exclusive taxonomy is associated with this record."

Bonus Question

Also, while it's not essential, I've been thinking it would be nice to setup a join of some kind between Taxonomies and Photos, ie., I'd like an easy way to query for all the photos that have been classified with terms from a given taxonomy.

I think I can do this with something like Photo.where('term_id in ?', Taxonomy.find(1).terms.map(&:id)) but, obviously that's not very elegant. I'm pretty sure has_many :through can't work since terms are habtm to photos, so if anyone knows a more elegant way to setup that relationship I'd love to hear it.

Thanks!


Update, to further explain the taxonomy idea:

If a Taxonomy is exclusive ie. taxonomy.inclusive = false, then there can only be one term from that taxonomy attached to a given photo. Now, a Photo may have more than one taxonomy applied to it. For instance:

Taxonomy: Colors, Inclusive Terms: Red (id 1), Blue (id 2), Green (id 3)

Taxonomy: Region, Exclusive Terms: North America (id 4), South America (id 5)

So let's say we have a photo with a lot of red and blue in it, so it gets those two terms, and it might be in South America, so it gets that term. But, a Photo can't be of both North America and South America.

So in this case if we called: photo.terms.map &:id we would get [1,2,4]

On the client side I can basically do (pseudo code)

form_for photo
- Taxonomy.each do |tax|
  - if tax.inclusive?
    - tax.terms.each do |term|
      - check_box term
  - else
    - tax.terms.each do |term|
      - radio_button term

But on the model side, I would like to say, that a photo can have any of term_id 1,2,3, but only either/or 4 and 5.

Does that make sense?

Upvotes: 1

Views: 173

Answers (1)

Adrien Coquio
Adrien Coquio

Reputation: 4930

For your validation problem, you can add a uniqueness validation on Term model for taxonomy_id field which is only executed if the taxonomy is not inclusive, ie :

class Term
  validates :taxonomy_id, uniqueness: true, unless: Proc.new { |term| term.taxonomy.inclusive }
end

You can also do this with a custom validation method :

class Term
  validate :uniqueness_of_taxonomy_if_exclusive

  def uniqueness_of_taxonomy_if_exclusive
    if !taxonomy.inclusive && Term.find_by_taxonomy_id(taxonomy_id)
      errors.add(:taxonomy_id, "already exists for this exclusive taxonomy")
    end
  end
end

Note that this is useless in this case as the first method give the same result but may be useful if you have more complex validation to do.

For the second part of your question, here is the way to make your query in ActiveRecord, let's assume taxonomy is the taxonomy for which you want the photos :

Photo.includes(:terms => :taxonomy).where('taxonomies.id' => taxonomy.id)

UPDATE after your comment

I believe your validation should take place in the table which make the link between photos and terms. If you have following Rails convention, it's actually the table photos_terms. To add a validation on it, you will have to make it a first class citizen, ie: add an active record model for this table and an id field, let's say you name it PhotoTermLink. Here is the code for the validation you want :

class PhotoTermLink
  belongs_to :photo
  belongs_to :term

  validates :term_id, uniqueness: { scope: :photo_id }, unless: Proc.new { |link| link.term.taxonomy.inclusive }
end

Upvotes: 2

Related Questions