Search code examples
ruby-on-rails-5nested-attributeshas-many-through

Rails 5 how to edit join model attributes from the parent model's view?


I have a system where admins can create Exams and price them according to the Branch they are selling them in. So for example, Exam One can cost $5 in Branch One, but $10 in Branch Two.

I made a join table named ExamOffering that has the price of the exam, so each Exam can be sold at many Branches at different prices, and each Branch can have many Exams. Like this:

    class Branch < ApplicationRecord
      has_many :exam_offerings
      has_many :exams, through: :exam_offerings
    end

    class Exam < ApplicationRecord
      has_many :exam_offerings
      has_many :branches, through: :exam_offerings
    end

    class ExamOffering < ApplicationRecord
      # this class has a 'price' attribute
      belongs_to :exam
      belongs_to :branch
    end

I need to be able to create a new Exam and select a Branch in the form in order to put in a price, but that attribute is not part of the Exam model, but the ExamOffering join table. I've tried some ways but failed (using accepts_nested_attributes_for :exam_offerings in the Exam model or iterating through all the Branches and creating ExamOfferings for each one in the controller). What's the 'Rails way' of doing this? I think it's a pretty common case, but I haven't found an answer that fits my case. Maybe this has a name and I don't know it.

It could be phrased like this: When I'm creating a new Exam, I want to be able to input a price for each existing Branch.

Thanks.


Solution

  • I ended up using accepts_nested_attributes_for in the Exam model, so I didn't have to use another view to put the price, so it's something like this:

    class Branch < ApplicationRecord
      has_many :exam_offerings
      has_many :exams, through: :exam_offerings
    end
    
    class Exam < ApplicationRecord
      has_many :exam_offerings
      has_many :branches, through: :exam_offerings
      accepts_nested_attributes_for :exam_offerings
    end
    
    class ExamOffering < ApplicationRecord
      # this class has a 'price' attribute
      belongs_to :exam
      belongs_to :branch
    end
    

    The important part was the view. I needed to use fields_for with a new object when creating the exam, but fetch the existing exam_offerings when editing the exam, so I have this code:

    <% if exam.exam_offerings.empty? %>
      <% Branch.all.each do |branch| %>
        <%= form.fields_for :exam_offerings, @exam.exam_offerings.build do |offering| %>
          <div class="field">
            <%= offering.label "Price in #{branch.name}" %>
            <%= offering.text_field :price %>
            <%= offering.hidden_field :branch_id, value: branch.id %>
          </div>
        <% end %>
      <% end %>
    <% else %>
      <%= form.fields_for :exam_offerings do |offering| %>
        <div class="field">
          <%= offering.label "Price in #{offering.object.branch.name}" %>
          <%= offering.text_field :price %>
        </div>
      <% end %>
    <% end %>