Skip to content

Instantly share code, notes, and snippets.

@diamondap
Created February 20, 2014 15:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save diamondap/9115715 to your computer and use it in GitHub Desktop.
Save diamondap/9115715 to your computer and use it in GitHub Desktop.
In Rails 3.2.13 using accepts_nested_attributes_for, a scoped validates_uniqueness_of validator will incorrectly reject unique values.
# In Rails 3.2.13 using accepts_nested_attributes_for, a scoped
# validates_uniqueness_of validator will incorrectly reject unique values.
# Below are two models, Question and AnswerChoice. The validates_uniqueness_of
# validator in AnswerChoice says that AnswerChoice label must be unique within a
# Question. This validator incorrectly rejects unique labels when they are
# reordered. Follow these steps to see the bug.
#
# This occurs when using nested resources and allowing users to edit the
# Question and AnswerChoices in a single form. The form looks like this:
#
# Question: _______________________________
# Answer 1: ___________________________
# Answer 2: ___________________________
# Answer 3: ___________________________
#
# 1. Create a new question.
# 2. Add three answer choices setting the label of the first choice to
# "One", the label of the second choice to "Two" and the label of the
# third choice to "Three".
# 3. Save the Question and all the answer choices.
# 4. Edit the question again, change the labels to "Two", "Three", "One",
# in that order.
# 5. Save the Question.
#
# Expected: No validation error.
# Actual: You get an error saying that the answer choices must be unique.
# Schema info for Question
# Table name: questions
#
# id :integer not null, primary key
# prompt :string(500) not null
# sort_order :integer not null
# created_by :integer default(0), not null
# updated_by :integer
# created_at :datetime not null
# updated_at :datetime not null
class Question < ActiveRecord::Base
has_many :answer_choices
accepts_nested_attributes_for :choices, :allow_destroy => true
validates_presence_of :prompt
validates :choices, length: { minimum: 2, message: "Question must have at least 2 choices." }
attr_accessible :prompt, :answer_choices_attributes
end
# Schema for AnswerChoice
# id :integer not null primary key
# question_id :integer not null
# value :integer
# label :string
# sort_order :integer not null
# is_correct :boolean not null default false
# created_by :integer not null default 0
# updated_by :integer
# created_at :datetime not null
# updated_at :datetime not null
class AnswerChoice < ActiveRecord::Base
belongs_to :question
attr_accessible :value, :label, :sort_order, :is_correct
validates :sort_order, presence: true
validates_length_of :label, :maximum => 400
validates_uniqueness_of :label, :scope => [:question_id], :allow_blank => false, :case_sensitive => false, message: "Answer choice must be unique."
end
# The following validator works without error for this case:
class UniqueChoiceValidator < ActiveModel::Validator
def validate(record)
exists = {}
record.choices.each_with_index do |choice, index|
choice.label ||= '' # conver nils so downcase doesn't raise
if exists[choice.label.downcase]
record.errors[:choices] << "Choices must be unique"
record.choices[index].errors[:label] << "Choices must be unique!"
end
exists[choice.label.downcase] = true
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment