Has_many - edit the association

I have a has_and_belongs_to_many relationship, let’s say

class Student < ActiveRecord::Base
has_and_belongs_to_many :courses
end

class Course < ActiveRecord::Base
has_and_belongs_to_many :students
end

Now I have a form for a student, with a checkbox for applicable courses.

How do I validate the courses checked against some conditions, so that
if validation fails, the new associations are not saved?

I try, for example, in the student controller’s update method

@student.attributes = params[:student]

gather the course assignment from other parameters…

updated_courses = …

@student.courses = updated_courses.collect { |u| @student.courses.build
( u.attributes ) }
@successful = @student.save

But the courses for the student are then actually wiped out.

What is an easy way?

Thanks a lot,

Stephan

Stephan,

I have a has_and_belongs_to_many relationship, let’s say
class Student < ActiveRecord::Base
has_and_belongs_to_many :courses
end
class Course < ActiveRecord::Base
has_and_belongs_to_many :students
end

CRUDify/complexify your models, and your life/code will be simpler:
add 1 model - Enrolment - and use has_many :through instead of hbtm

Something like (untested) :

class Student
has_many :enrolments , :dependent => :destroy
has_many :courses , :through => :enrolments
end
class Course
has_many :enrolments , :dependent => :destroy
has_many :studends , :through => :enrolments
end
class Enrolment
belongs_to :course
belongs_to :studen
end

You would then create enrolments explicitely

Enrolment.create(:student => a_student, :course => a_course)

(you’d place the validations rules in the enrolment model).

For more about this CRUDification, see slides 24 and following of
“World of resources”, by DHH

http://media.rubyonrails.org/presentations/worldofresources.pdf

Alain R.

blog.ravet.com/

CRUDify/complexify your models, and your life/code will be simpler:
add 1 model - Enrolment - and use has_many :through instead of hbtm

Something like (untested) :

class Student
has_many :enrolments , :dependent => :destroy
has_many :courses , :through => :enrolments
end
class Course
has_many :enrolments , :dependent => :destroy
has_many :studends , :through => :enrolments
end
class Enrolment
belongs_to :course
belongs_to :studen
end

You would then create enrolments explicitely

Enrolment.create(:student => a_student, :course => a_course)

Thanks - sounds good. What would the controller’s update method look
like then?

More specific, how would the snippet I mentioned earlier look with this
new Enrolment class:

@student.attributes = params[:student]

gather the course assignment from other parameters…

updated_courses = …

@student.courses = updated_courses.collect { |u| @student.courses.build
( u.attributes ) }
@successful = @student.save

Enrolment.create will manipulate the database, while I want to first
perform validations (must have at least one 4-credit course, for
example), before the database-saves.

Stephan

Alain R.

blog.ravet.com/

Stephan

Thanks - sounds good. What would the controller’s update method
look like then?

By creating a new model - Enrolment - you simplify your controller by
having the model do all the - model - validation.

Enrolment.create will manipulate the database, while I want to first
perform validations (must have at least one 4-credit course, for
example), before the database-saves.

Something like (untested) :

class Enrolment < ActiveRecord::Base
belongs_to :course
belongs_to :student

protected
def validate
errors.add(“student”, “does not have enough credits”) unless
student.credits >= 4
end
end

API validations :
http://api.rubyonrails.com/classes/ActiveRecord/Validations.html#M000931

Alain R.

http://blog.ravet.com

Alain R. wrote:

Stephan

Thanks - sounds good. What would the controller’s update method
look like then?

By creating a new model - Enrolment - you simplify your controller by
having the model do all the - model - validation.

Enrolment.create will manipulate the database, while I want to first
perform validations (must have at least one 4-credit course, for
example), before the database-saves.

Something like (untested) :

class Enrolment < ActiveRecord::Base
belongs_to :course
belongs_to :student

protected
def validate
errors.add(“student”, “does not have enough credits”) unless
student.credits >= 4
end
end

Thanks a lot.

This looks to me like it might not allow adding the first course to the
student set of courses, if it doesn’t satisfy the “student.credits>=4”
condition.

The first “Enrolment.create” might fail, even if the parameters
submitted from the form contain a 4-credits course (unless the
controller does some sorting, which doesn’t look proper either).

Also, the Enrolment.create would ensue a database save independently of
other form-value validations of the student’s fields…

Am I missing something? I looked through the validations section of
ActiveRecord , but couldn’t find anything applicable.

Thanks again.

Stephan

API validations :
Peak Obsession

Alain R.

http://blog.ravet.com

Alain R. wrote:

Stephan

My code was just an example; adapt for your needs.

The first “Enrolment.create” might fail, even if the parameters
submitted from the form contain a 4-credits course (unless the
controller does some sorting, which doesn’t look proper either).
I don’t understand :

class Enrolment < ActiveRecord::Base

def validate
errors.add(“student”, “does not have enough credits”) unless
student.credits >= 4
end
end

will only fail if student.credits < 4

In the controller you’d have something like

def register
student = Student.find(params[:student_id])
course = Course .find(params[:course_id])

if Enrolment.create(student, course)
   ..
else
   flash[:notice] = "student cannot be enrolled for this course"
   ..
end

end

Alain R.

http://blog.ravet.com

In the form there are checkboxes for a whole bunch of courses.

So there would be a whole sequence of

Enrolment.create(student, course1) # course1 was checked off.
Enrolment.create(student, course2) # course2 was checked off.
Enrolment.create(student, course3) # course3 was checked off.

But validation depends on the whole set (does one of them have 4
credits).

Plus, if the last Enrolment.create fails, one would like the first one
not to commit.

I’m getting the impression ActiveRecord doesn’t quite go as far as what
I’m looking for.

I don’t quite understand why one can perform

student.courses = [ … an array of courses … ]

and this gets saved right away, without a chance to apply other changes
to the student record for one-step-validation.

Stephan

Stephan

My code was just an example; adapt for your needs.

The first “Enrolment.create” might fail, even if the parameters
submitted from the form contain a 4-credits course (unless the
controller does some sorting, which doesn’t look proper either).

I don’t understand :

class Enrolment < ActiveRecord::Base

def validate
errors.add(“student”, “does not have enough credits”) unless
student.credits >= 4
end
end

will only fail if student.credits < 4

In the controller you’d have something like

def register
student = Student.find(params[:student_id])
course = Course .find(params[:course_id])

if Enrolment.create(student, course)
   ..
else
   flash[:notice] = "student cannot be enrolled for this course"
   ..
end

end

Alain R.

http://blog.ravet.com

Stephan,

In the form there are checkboxes for a whole bunch of courses.
So there would be a whole sequence of

Enrolment.create(student, course1) # course1 was checked off.
Enrolment.create(student, course2) # course2 was checked off.
Enrolment.create(student, course3) # course3 was checked off.

But validation depends on the whole set (does one of them have 4
credits).

I guess you have two options :

  • check the form values in the controller before creating any new
    record (the validation logic could be moved to the model, in a class
    method.),
    or
  • add a more complex (multiple) creator in the model.

Alain R.

http://blog.ravet.com