Reputation: 9316
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
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 uniqueness
to 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