← Back to Upcase

Rails polymorphic associations in a nested form in one html-table


(Andreas Wunder) #1

Hello everyone,

first of all, i am aware that this is a bit of an advanced issue here and i’ve read the “this is not stackoverflow” warnings. :slight_smile: - However, i am desperate enough to give it a try, anyway…

The issue i have might be specific, but i still think it may be quite common as nested forms are everywhere nowadays…

i’ve discussed this with a great guy (tbuehlmann - a very nice and helpfull guy) from the rails irc channel on freenode and. unfortunately he had no time to take a deeper look but he suggested two possible solutions:

  1. change the form so, that it’ll render dependent on the achievementable type…

my poor attempt looked like this (but doesnt work):

<% @achievements.each do |achievement| %>
  <% if achievement.achievementable.is_a?(Domain) %>
    <%= domain.fields_for :achievements do |f| %>
      <%= render 'domains/achievements', f: f %>
    <% end %>
  <% elsif achievement.achievementable.is_a?(ActionField) %>
    <%= domain.fields_for(:action_fields) { |f| f.fields_for :achievements do |achievement|
                                            render 'domains/achievements', f: achievement
                                            end } %>
  <% end %>
<% end %>

i am aware that this wont work as this block gets executed for each action_field achievement, which results in duplicate table rows… however, i am not even sure if this is the right approach or if i should

or

  1. write your own setter method - e.g. write / override the “achievements_attributes=(achievement” method inside the domains_controller.

i’ve googled the heck out of it but all i could come up with is how to set specific attributes of an already “identified” object…
i guess in my case i somehow got to tell rails first, where it could find the object if it isnt related to the domain i am viewing… so, similar to the view approach i probably gotta to have 2 paths, if its a domain achievement, all is fine, if its an action_field achievement, i need to tell it that it needs to look for an actin_field achievement and not for a domain_achievement.
however, i have no idea how to proceed from here… and any suggestion will help!

thank you guys,
Andreas


this is the post from (ironically :wink: ) stackoverflow…:

i am stuck with a rails problem and a 2-level nested form… I am getting the error:

ActiveRecord::RecordNotFound - Couldn’t find Achievement with ID=10 for Domain with ID=1:

which somehow is correct as that achievement is’nt from the domain but from one of the domain’s ActionFields

however, my goal is to have all Achievements / Actions in that view, so those from the Domain as well as those from the ActionFields…

in a view like this:

visual representation of that view: https://i.stack.imgur.com/W6x7M.png

How could i achieve that?

i have defined the following models:

visualized data model: https://i.stack.imgur.com/qP9gS.png

which is in code:

Domain:

class Domain < ApplicationRecord
  belongs_to :editor, class_name: 'User', foreign_key: :editor_id
  belongs_to :deputy, class_name: 'User', foreign_key: :deputy_id
  belongs_to :solution_category

  has_many :action_fields, dependent: :destroy
  has_many :actions, as: :actionable
  has_many :achievements, as: :achievementable

  accepts_nested_attributes_for :action_fields, :achievements, :actions,  allow_destroy: true

  validates :name, :solution_category_id, presence: true
end

ActionField:

class ActionField < ApplicationRecord
  belongs_to :domain
  belongs_to :editor, class_name: 'User', foreign_key: :editor_id
  belongs_to :deputy, class_name: 'User', foreign_key: :deputy_id

  has_many :actions, as: :actionable
  has_many :achievements, as: :achievementable

  accepts_nested_attributes_for :achievements, :actions, allow_destroy: true
end  

Achievement:

class Achievement < ApplicationRecord
  belongs_to :achievementable, polymorphic: true

  validates :name, presence: true
end

then there is my domains_controller (i removed the unneeded methods to make it shorter)

class DomainsController < ApplicationController
  before_action :set_domain, except: [:new, :create, :index, :domain_index]

  def show
    @action_fields = @domain.action_fields
    @actions = Action.where(actionable: [@domain, *@action_fields])
      .where(visible: true)
      .order(domain_rank: :asc, action_field_rank: :asc)

    @achievements = Achievement.where(achievementable: [@domain, *@action_fields])
      .where(visible: true)
      .order(domain_rank: :asc, action_field_rank: :asc)
  end

  def edit
    authorize @domain
    @action_fields = @domain.action_fields
    @actions = Action.where(actionable: [@domain, *@action_fields])
      .order(domain_rank: :asc, action_field_rank: :asc)
    @achievements = Achievement.where(achievementable: [@domain, *@action_fields])
      .order(domain_rank: :asc, action_field_rank: :asc)
  end

  private

  def domain_params
    params.require(:domain).permit(
      :name,
      :editor_id,
      :editor_name,
      :deputy_id,
      :deputy_name,
      :adjust_solution,
      :adjust_skills,
      :adjust_organization,
      :solution_category_id,
      :achievements_attributes => [
        :id, :name, :domain_rank, :visible, :additional_info, :media, :ready, :rollout, :_destroy
      ], 
      :actions_attributes => [
        :id, :domain_rank, :visible, :due_date,  :_destroy
      ],
      :action_fields_attributes => [
        :id, :name, :domain_weight, :domain_id, :_destroy
      ]
    )
  end

  def set_domain
    @domain = Domain.find(params[:id])
  end
end

the edit form (only with achievements to keep it short):

<%= form_for @domain do |domain| %>
  <%= render 'layouts/error_messages', object: @domain %>

  <div class="small-12">
    <h4 class="text-center"><%= t('domains.edit_form.domain_achievements') %></h4>
    <table class="hover draggable" id="domain_achievements">
      <thead>
        <tr>
          <th class="text-center" width="50"></th>
          <th class="text-center"><%= t('action_fields.headers.achievement_headline') %></th>
          <th class="text-center"><%= t('action_fields.headers.additional_info') %></th>
          <th class="text-center" width="100"><%= t('defaults.misc.visible') %></th>
          <th class="text-center" width="50"></th>
          <th></th>
        </tr>
      </thead>
      <tbody class="achievements">
        <%= domain.fields_for :achievements, @achievements do |f| %>
          <tr>
            <td>
              <%= f.hidden_field :_destroy %>
              <i class="fa fa-trash-o remove_row" aria-hidden="true"></i>
            </td>
            <td><%= f.text_field :name %></td>
            <td><%= f.text_field :additional_info %></td>
            <td>
              <div class="small switch text-center">
                <%= f.check_box :visible, id: dom_id(f.object), class: 'switch-input' %>
                <label class="switch-paddle" for="<%= dom_id(f.object) %>">
                  <span class="show-for-sr"><%= t('domains.edit_form.achievement_screenreader') %></span>
                  <span class="switch-active" aria-hidden="true"><%= t('defaults.misc.y') %></span>
                  <span class="switch-inactive" aria-hidden="true"><%= t('defaults.misc.n') %></span>
                </label>
              </div>
            </td>
            <td class="text-center draggable">
              <i class="fa fa-arrows" aria-hidden="true"></i>
              <%= f.hidden_field :domain_rank, class: 'domain_rank' %>
            </td>
          </tr>
        <% end %>
      </tbody>
      <tfoot class="achievements">
        <tr>
          <td colspan="8" class="text-right"><%= link_to_add_fields t('domains.buttons.add_achievement'), domain, :achievements %></td>
        </tr>
      </tfoot>
    </table>
  </div>

  <hr>

<...snip...>

<% end %>