views:

161

answers:

3

I'm having a difficult time understanding how to get Rails to show an explicit error message for a child resource that is failing validation when I render an XML template. Hypothetically, I have the following classes:

class School < ActiveRecord::Base
    has_many :students
    validates_associated :students

    def self.add_student(bad_email)
      s = Student.new(bad_email)
      students << s
    end
end

class Student < ActiveRecord::Base
    belongs_to :school
    validates_format_of :email,
                  :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i,
                  :message => "You must supply a valid email"
end

Now, in the controller, let's say we want to build a trivial API to allow us to add a new School with a student in it (again, I said, it's a terrible example, but plays its role for the purpose of the question)

class SchoolsController < ApplicationController
    def create
      @school = School.new
      @school.add_student(params[:bad_email])
      respond_to do |format|
          if @school.save
          # some code
          else
            format.xml  { render :xml => @school.errors, :status => :unprocessable_entity }
          end
      end
    end
end

Now the validation is working just fine, things die because the email doesn't match the regex that's set in the validates_format_of method in the Student class. However the output I get is the following:

<?xml version="1.0" encoding="UTF-8"?>
<errors>
  <error>Students is invalid</error>
</errors>

I want the more meaningful error message that I set above with validates_format_of to show up. Meaning, I want it to say:

 <error>You must supply a valid email</error>

What am I doing wrong for that not to show up?

A: 

You should use following in the rhtml.

<%= error_messages_for :school, :student %>

To skip "Students is invalid" message use following in the student.rb

  def after_validation
    # Skip errors that won't be useful to the end user
    filtered_errors = self.errors.reject{ |err| %w{ student}.include?(err.first) }
    self.errors.clear
    filtered_errors.each { |err| self.errors.add(*err) }
  end

EDITED

Sorry after_validation must be in a school.rb
Salil
What's the best way to do this in an XML template? Right now I don't have an XML view, I just let render handle it?
randombits
A: 

I see a problem in the posted code. add_student is a class method of class School, so self will point to the class object School instead of an instance object of class School. The line students << s will not add the record s to the record school because of this.

I don't know if this is causing your error message problem, but I think this will keep the code from working properly.

Fred
It gets linked and stored into the database as it should. I'm intentionally failing validation by passing a bad email as the problem originally suggests. If I pass a proper email that passes the regex, then I don't get an error. The point here is to *intentionally* fail, and get the appropriate message into the view.
randombits
I understand you are deliberately failing the example. I don't think you understand my point. Why have you made the add_student method a class method? In Ruby, self points to the class object in class methods, so the message add_student, while being sent to the instance object, executes with self pointing to the class object rather than the instance object. Perhaps ActiveRecord manages to do the right thing despite this, but unless I'm missing something, the method should not be a class method. I would be grateful if you could tell me what I'm missing.
Fred
OK, I understand now. The add_student method cannot be a class method; that was a typo in your example. If you send a class method message to an instance object, you get NoMethodError. Rails doesn't recover from calling a class method on an instance object, at least in this case. The code you gave produces a NoMethodError in SchoolsController#create when I tried it out.Ruby instance objects do not see class methods because class methods are singleton methods for the class object. That's what I didn't understand, and now I do. Learn a thing a day, drive ignorance away!
Fred
+3  A: 

Add a validation block in the School model to merge the errors:

class School < ActiveRecord::Base
  has_many :students

  validate do |school|
    school.students.each do |student|
      next if student.valid?
      student.errors.full_messages.each do |msg|
        # you can customize the error message here:
        errors.add_to_base("Student Error: #{msg}")
      end
    end
  end

end

Now @school.errors will contain the correct errors:

format.xml  { render :xml => @school.errors, :status => :unprocessable_entity }

Note:

You don't need a separate method for adding a new student to school, use the following syntax:

school.students.build(:email => email)
KandadaBoggu