Lee McAlilly
Lee McAlilly

Reputation: 9316

How to validate the uniqueness on a form instance and not the entire database in Rails?

How do you validate a form field in rails so that the value is unique to that instance of the form, but not unique across the entire database?

I have a form in rails where there is a User that can create a Quote. More detail on how I got that working here (models, controller, schema, form).

The form allows a User to share a Quote by an Artist (such as David Bowie) about another Artist (such as Lou Reed). So, a quote might look like this:

Topic: David Bowie 
Content: "He was a master." 
Speaker: Lou Reed

Both Speaker and Topic are associations from my Artist model. The models look like this:

class Quote < ApplicationRecord
  default_scope -> { order(created_at: :desc) }

  belongs_to :user

  belongs_to :speaker, class_name: "Artist"
  belongs_to :topic,   class_name: "Artist"

  validates :speaker, presence: true
  validates :topic,   presence: true
  validates :content, presence: true, length: { maximum: 1200 }
  validates :source,  presence: true, length: { maximum: 60 }
  validates :user_id, presence: true

end

class Artist < ApplicationRecord
  default_scope -> { order(name: :asc) }

  belongs_to :user

  has_many :spoken_quotes, class_name: "Quote", foreign_key: :speaker_id
  has_many :topic_quotes,  class_name: "Quote", foreign_key: :topic_id

  validates :user_id, presence: true
  validates :name,    presence: true, length: { maximum: 60 },
                      uniqueness: { case_sensitive: false }
end

And the form looks like this:

<%= form_for(@quote) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">

    <div class="form-group">
      <label>Topic</label>
      <%= f.collection_select :topic_id, Artist.all, :id, :name %>
    </div>

    <div class="form-group">
      <label>Speaker</label>
      <%= f.collection_select :speaker_id, Artist.all, :id, :name %>
    </div>

    <div class="form-group">
      <%= f.text_area :content, class: "form-control",
      placeholder: "Share a new quote..." %>
    </div>

    <div class="form-group">
      <%= f.url_field :source, class: "form-control",
      placeholder: "http:// Link to your source" %>
    </div>
  </div>
  <%= f.submit "Post", class: "btn btn-primary btn-block" %>
<% end %>

First I tried adding these validations to the Quote model:

validates :speaker, uniqueness: {scope: :topic}
validates :topic,   uniqueness: {scope: :speaker}

That works the first time, but as soon as you try to re-use an Artist as a Speaker or Topic in a second Quote—for instance, to share a quote that Lou Reed said about someone other than David Bowie—it will validate the uniqueness of Lou Reed and won't let me re-use Lou Reed in that second Quote because he was already used as a Speaker in the first Quote. This is not the intended behavior, so next I tried adding this to the Quote model:

validates :speaker, presence: true, uniqueness: true, if: :speaker_and_topic_are_the_same?

def speaker_and_topic_are_the_same?
  :topic_id == :speaker_id
end

The thought behind that approach is that it would only enforce the uniqueness validation when the speaker and topic are the same on the same form.

But that unfortunately doesn't work and allows me to create a Quote where the Speaker and Topic are the same artist.

So, what is the correct way to enforce this type of validation in rails?

Upvotes: 0

Views: 85

Answers (1)

Bartosz Pietraszko
Bartosz Pietraszko

Reputation: 1407

Multiple occurences of an Artist both as a speaker and a topic, are at the core of your model logic; I don't think there is any uniquenessto validate at all - neither scoped or conditional. Validation that you want considers only context of single instance of a Quote; I believe it should be a custom one.

validate :topic_cant_be_a_speaker

def topic_cant_be_a_speaker
  errors.add(:speaker, "Topic can't be a speaker") if speaker == topic
end

Upvotes: 1

Related Questions