I deeply searched the web in order to find a clean and simple way to deal with attributes initialization on the join model of a has_many :through relation
, but I did not find a best solution for my need.
In the exaple I provide below, I need to automatically set the attribute role
of the Training
join model when I create or update a Course
object.
This is my model:
QUALIFICATIONS = ["Theoretical Instructor", "Practical Instructor"]
class Course < ActiveRecord::Base
has_many :trainings, dependent: :destroy
has_many :theoretical_instructors, through: :trainings, source: :trainer, conditions: { "trainings.role" => "Theoretical Instructor" }
accepts_nested_attributes_for :theoretical_instructors
has_many :practical_instructors, through: :trainings, source: :trainer, conditions: { "trainings.role" => "Practical Instructor" }
accepts_nested_attributes_for :practical_instructors
end
class Trainer < ActiveRecord::Base
has_many :trainings, dependent: :destroy
has_many :courses, through: :trainings
end
class Training < ActiveRecord::Base
belongs_to :trainer
belongs_to :course
# Join model has the :role attribute, that I wish I could validate this way:
# validates :role, presence: true, inclusion: { in: QUALIFICATIONS }
end
The rationale behind this model is that I want to save Training
objects in a single table. I don't want to create the TheoreticalInstructor
and the PracticalInstructor
join models (potentially exploding the number of tables) to solve this problem.
This view provides the form to submit a new Course
:
<%= form_for @course do |course_form| %>
<%- # fields for course attributes, as usual... %>
<%= course_form.label :theoretical_instructor_ids %><br />
<%= course_form.select :theoretical_instructor_ids, Trainer.all.map { |x| [[x.name, x.surname].join(" "), x.id] }, { }, { multiple: true } %>
<%= course_form.label :practical_instructor_ids %><br />
<%= course_form.select :practical_instructor_ids, Trainer.all.map { |x| [[x.name, x.surname].join(" "), x.id] }, { }, { multiple: true } %>
<%= course_form.submit %>
<% end%>
The question is: what can I do in order to make @course = Course.new(params[:course])
the only line of code in the Course
controller needed to save this association on submit of the previous form?
Differently from this question I don't want to create new Trainer
objects when I create a new Course
: I want to choose them from those already present in the DB (through a multiselect input field).
What I need is that something like @course.theoretical_instructor_ids = [1, 2]
creates two Training
objects with the role
attribute set to Theoretical Instructor
I'm thinking on an after_initialize
callback on Training
that set role
basing on the relation name (:theoretical_instructors
and :practical_instructors
), but I really don't know how to do it. Any advice? Am I missing some point?
Thank you guys!
EDIT 1 from oli-g
This question deals with a similar problem: the difference is that I don't want to build Trainer
objects when I create a new Course
, but I simply want to associate existing Trainer
objects to a new Course
.
EDIT 2 from oli-g
Basing on this (a 5 years old post) and this blog posts, I've changed the Course
model in this way:
class Course < ActiveRecord::Base
has_many :trainings, dependent: :destroy
has_many :theoretical_instructors, through: :trainings, source: :trainer, conditions: ["trainings.role = ?", "Theoretical Instructor"] do
def <<(theoretical_instructor)
Training.send(:with_scope, create: { role: "Theoretical Instructor" }) { self.concat theoretical_instructor }
end
end
accepts_nested_attributes_for :theoretical_instructors
has_many :practical_instructors, through: :trainings, source: :trainer, conditions: ["trainings.role = ?", "Practical Instructor"] do
def <<(practical_instructor)
Training.send(:with_scope, create: { role: "Practical Instructor" }) { self.concat practical_instructor }
end
end
accepts_nested_attributes_for :practical_instructors
end
This code enables me to do a thing like this
:001 > c = Course.first
=> #<Course id: 1>
:002 > t1 = Trainer.first
=> #<Trainer id: 1, name: "Tom">
:003 > c.theoretical_instructors << t1
=> #<Trainer id: 1, name: "Tom">
:004 > Training.all
=> [#<Training id: 1, role: "Theoretical Instructor", trainer_id: 1, course_id: 1>]
This is an acceptable workaround, even if in my controller I still can't do just @course = Course.new(params[:course])
, but I have to create Training
objects iterating on params[:course][:theoretical_instructor_ids]
and params[:course][:practical_instructor_ids]
.
But I am curious, so the question remains open: what can I do in order to enable @course = Course.new(params[:course])
to build Training
objects along with the Course
?
Now... I think I discovered a bug in Rails:
:005 > c.practical_instructors
=> [] # correct
:006 > c.practical_instructor_ids
=> [] # obviously
:007 > c.reload
=> #<Course id: 1>
:008 > c.practical_instructor_ids
=> [1] # WRONG!!!
:009 > c.practical_instructors
=> [] # now it's correct...
:010 > c.practical_instructor_ids
=> [] # WTF!?
I think I will report this at github issues...
EDIT 3 by oli-g
Bug reported at github