ActiveRecord, nested_attributes dla relacji many_to_many i dynamiczne tworzenie formularza

Witam.

Mam problem z utworzeniem dobrego sposobu na dynamicznie tworzony formularz gdzie wraz z obiektem tworzę jego relacje many_to_many. Mianowicie jeśli dane nie przejdą walidacji formularz jest wyświetlany bez danych dla relacji.

Zapoznałem się z http://railscasts.com/episodes/196-nested-model-form-part-1?autoplay=true i drugą cześcią. Ale z tego co wyczytałem jest to przestarzałe. Doszedłem więc do rozwiązana z link_to: remote: true ale kod przy takim rozwiązaniu jest po prostu brzydki (swoją drogą, że najpierw chce zmusić go do poprawnego działania a potem zająłbym sie poprawianiem aby to wyglądało). Czyli tak. najpierw metoda get_data_sets_params musi zwracać mi poprawnie czego nie robi, a następnie metoda create w przypadku gdy dane nie przejdą walidacji ma mi poprawnie wyświetlic formularz. Dodatkowo przydałyby się porada jak poradzić sobie z usuwaniem relacji (wnioskuję, że pomysł z polem _destroy jest do bani bo powoduje powtarzanie sie ID dla pól.)

Moje modele

class DataSet < ActiveRecord::Base
  self.table_name = :data_sets
  has_many :data_set_synch_agents, inverse_of: :data_set, dependent: :destroy
  has_many :data_set_users, inverse_of: :data_set, dependent: :destroy
  has_many :users, inverse_of: :data_sets, through: :data_set_users

  has_many :synch_agents, inverse_of: :data_sets, through: :data_set_synch_agents
  validates_presence_of :configuration_id
  validates_length_of :configuration_id, in: 3..100
  validates_numericality_of :reports_synch_interval_min, greater_than_or_equal_to: 0, only_integer: true, allow_nil: true
  validates_numericality_of :max_packages_on_server, greater_than: 0, only_integer: true, allow_nil: true

  accepts_nested_attributes_for :data_set_synch_agents, :reject_if => lambda { |a| a[:synch_agent_id].blank? }, :allow_destroy => true
  accepts_nested_attributes_for :data_set_users, :reject_if => lambda { |a| a[:user_id].blank? }, :allow_destroy => true
...
end

class DataSetSynchAgent < ActiveRecord::Base
  self.table_name = :data_set_synch_agent
  self.primary_keys = :data_set_id, :synch_agent_id, :max_idle_on_data_set
  belongs_to :synch_agent
  belongs_to :data_set, inverse_of: :data_set_synch_agents, foreign_key: :data_set_id
  validates_numericality_of :max_idle_on_data_set, greater_than_or_equal_to: 0, only_integer: true
  validates_numericality_of :synch_agent_id, greater_than: 0, only_integer: true
end

class SynchAgent < ActiveRecord::Base
  belongs_to :exchange_server, inverse_of: :synch_agents
  has_many :data_set_synch_agents, inverse_of: :synch_agent, dependent: :restrict_with_error
  has_many :synchronization_reports, dependent: :nullify

  has_many :data_sets, inverse_of: :synch_agents, through: :data_set_synch_agents, dependent: :restrict_with_error
...
end

class DataSetUser < ActiveRecord::Base
  belongs_to :data_set, inverse_of: :data_set_users
  belongs_to :user, inverse_of: :data_set_users

  validates_inclusion_of :send_alerts, in: [true, false], default: true
end

class User < ActiveRecord::Base
  include User::Roles

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  has_many :data_set_users, inverse_of: :user, dependent: :delete_all
  has_many :data_sets, inverse_of: :users, through: :data_set_users
  validates_length_of :name, in: 3..32
end

Mój formularz, ktory chcę rozszerzać

.row
  = bootstrap_nested_form_for @data_set do |f|
    = f.text_field :configuration_id
    = f.number_field :max_packages_on_server
    = f.number_field :reports_synch_interval_min

    .row
      #users.col-xs-12
        = link_to t('general.add'), data_set_add_field_path(name: :users), class: 'btn btn-link', remote: true
        .row
          = f.fields_for :data_set_users do |ff|
            .col-xs-8
              = ff.select :user_id, User::FormParameters.get_users_select_list_values
            .col-xs-2
              = ff.check_box :send_alerts
            .col-xs-2
              = f.hidden_field(:_destroy)
              = link_to data_set_remove_field_path, class: 'btn btn-link', remote: true, alt: t('general.remove'), class: "btn btn-lg" do
                = fa_icon :remove

      #synch_agents.col-xs-12
        = link_to t('general.add'), data_set_add_field_path(name: :synch_agents), class: 'btn btn-link', remote: true
        .row
          = f.fields_for :data_set_synch_agents do |ff|
            .col-xs-8
              = ff.select :synch_agent_id, SynchAgent::FormParameters.get_synch_agents_select_list_values, { include_blank: true }
            .col-xs-2
              = ff.number_field :max_idle_on_data_set
            .col-xs-2
              = ff.hidden_field :_destroy
              = link_to data_set_remove_field_path, class: 'btn btn-link', remote: true, alt: t('general.remove'), class: "btn btn-lg" do
                = fa_icon :remove


    = f.submit t("general.save"), class: "btn btn-primary i4b-btn-block-xs"
    = link_to t("general.cancel"), data_sets_path( data_set: params[:params], sort: params[:sort], page: params[:page] ), class: "btn btn-default i4b-btn-block-xs"

Kontroler, w którym a się wszystko odbywać

class DataSetsController < ApplicationController
  expose(:search_data_set) { DataSet.new_search(params) }
  expose(:data_sets) { DataSet::Search.search(params) }

  def new
    @data_set = DataSet.new
    @data_set.data_set_users.build
    @data_set.data_set_synch_agents.build
  end

  def create
    respond_to do  |format|
      @data_set = DataSet.new(get_data_set_params)

      byebug
      if @data_set.save
        format.html {
          redirect_to data_sets_path,
          notice: t(
            "general.models.create_object_message",
            name: DataSet.model_name.human.downcase
          )
        }
      else
        format.html { render action: :new }
      end
    end
  end

  def destroy
    data_set = DataSet.find(params.require(:id))
    data_set.destroy
    attrs = { flash: {} }

    if data_set.errors.any?
      attrs[:flash][:error] = data_set.errors.full_messages().join(". ") + "."
    else
      attrs[:notice] = I18n.t(
        "general.models.destroy_success_message",
        removed_object_name: DataSet.model_name.human
      )
    end

    respond_to do |format|
      format.html {
        redirect_to(
          data_sets_path,
          attrs
        )
      }
    end
  end

  def remove_field; end
  def add_field
    @select_id = "data_set_data_set"
    @value = Time.now().strftime("%Y_%m_%d_%H_%M_%S")
    @name = "data_set"
    if params[:name] == "synch_agents"
      @label = SynchAgent.human_attribute_name('send_alert')
      @select_values = SynchAgent::FormParameters.get_synch_agents_select_list_values
      @include_blank = true
      @synch_agent = true
      @select_id += "_synch_agents_attributes_#{@value}_synch_agent_id"
      @name += "[data_set_synch_agents_attributes][#{@value}][synch_agent_id]"
    else
      @label = DataSet.human_attribute_name('user_id')
      @select_values = User::FormParameters.get_users_select_list_values
      @include_blank = false
      @synch_agent = false
      @select_id += "_users_attributes_#{@value}_user_id"
      @name += "[data_set_users_attributes][#{@value}][user_id]"
    end
    respond_to do |format|
      format.js {}
    end
  end

  private
    def get_data_set_params
      params.require(:data_set)
      .permit(
        :configuration_id,
        :max_packages_on_server,
        :reports_synch_interval_min,
        data_set_users_attributes: [:user_id, :send_alerts, :_destroy],
        data_set_synch_agents_attributes: [:synch_agent_id, :max_idle_on_data_set, :_destroy]
      )
    end
end

Widoki związane z metodą add_field

add_field.js.erb

$('#<%=j params[:name] %>').append('<%=j render partial: "data_sets/partials/add_field", locals: { _label: @label, select_values: @select_values , include_blank: @include_blank, synch_agent: @synch_agent, _value: @value, _select_id: @select_id, _name: @name } %>')

_add_field_html.haml

.row
  .fields
    .col-xs-8
      .form-group
        = label_tag :label, _label, for: _select_id
        = select_tag _select_id, options_for_select(select_values), include_blank: include_blank, class: 'form-control', name: _name
    .col-xs-2
      - if synch_agent
        = label_tag DataSetSynchAgent.human_attribute_name('max_idle_on_data_set')
        = number_field_tag :max_idle_on_data_set, 0, class: 'form-control', id: "data_set_data_set_synch_agents_attributes_#{_value}_max_idle_on_data_set", name: "data_set[data_set_synch_agents_attributes][#{_value}][max_idle_on_data_set]"
      - else
        .checkbox
          %label{ for: "data_set_data_set_users_attributes_#{_value}_send_alerts" }
            %input{ type: "hidden", value: _value, name: "data_set[data_set_users_attributes][#{_value}][send_alerts]" }
            %input{ type: "checkbox", value: "1", checked: "checked", id: "data_set_data_set_users_attributes_#{_value}_send_alerts", name: "data_set[data_set_users_attributes][#{_value}][send_alerts]" }
            = DataSetUser.human_attribute_name('send_alerts')
    .col-xs-2
      = hidden_field_tag("data_set[_destroy]")
      = link_to data_set_remove_field_path, class: 'btn btn-link', remote: true, alt: t('general.remove'), class: "btn btn-lg" do
        = fa_icon :remove

Zawartość params, które jest poprawne ale nie przechodzi przez metodę get_data_set_params

{"utf8"=>"✓", "authenticity_token"=>"Jji9Y/xLRMgGcPzjfLnbPPeH51uP9yqna3ZC/1r9+GsVsSV5hsslxR0sh4RkFpU0wBBdW38GJf1L77l9FkP3Og==", "data_set"=>{"configuration_id"=>"", "max_packages_on_server"=>"", "reports_synch_interval_min"=>"", "data_set_users_attributes"=>{"0"=>{"user_id"=>"1", "send_alerts"=>"1"}, "2015_09_09_11_43_36"=>{"user_id"=>"2", "send_alerts"=>"1"}}, "_destroy"=>"", "data_set_synch_agents_attributes"=>{"0"=>{"synch_agent_id"=>"1", "max_idle_on_data_set"=>"0", "_destroy"=>"false"}, "2015_09_09_11_43_39"=>{"synch_agent_id"=>"2", "max_idle_on_data_set"=>"0"}}}, "commit"=>"Save", "controller"=>"data_sets", "action"=>"create"}

Będę wdzięczny za wszelką pomoc.

Pozdrawiam.

Ok znalazłem rozwiązanie.

Poprawiony formularz z polami do usuwania

.row
  .fields
    .col-xs-8
      .form-group
        = label_tag :label, _label, for: _select_id
        = select_tag _select_id, options_for_select(select_values), include_blank: include_blank, class: 'form-control', name: _name
    .col-xs-2
      - if synch_agent
        = label_tag DataSetSynchAgent.human_attribute_name('max_idle_on_data_set')
        = number_field_tag :max_idle_on_data_set, 0, class: 'form-control', id: "data_set_data_set_synch_agents_attributes_#{_value}_max_idle_on_data_set", name: "data_set[data_set_synch_agents_attributes][#{_value}][max_idle_on_data_set]"
      - else
        .checkbox
          %label{ for: "data_set_data_set_users_attributes_#{_value}_send_alerts" }
            %input{ type: "hidden", value: _value, name: "data_set[data_set_users_attributes][#{_value}][send_alerts]" }
            %input{ type: "checkbox", value: "1", checked: "checked", id: "data_set_data_set_users_attributes_#{_value}_send_alerts", name: "data_set[data_set_users_attributes][#{_value}][send_alerts]" }
            = DataSetUser.human_attribute_name('send_alerts')
    .col-xs-2
      - if synch_agent
        = hidden_field_tag("data_set[data_set_synch_agents_attributes][#{_value}][_destroy]")
      - else
        = hidden_field_tag("data_set[data_set_users_attributes][#{_value}][_destroy]")
      = link_to data_set_remove_field_path, class: 'btn btn-link', remote: true, alt: t('general.remove'), class: "btn btn-lg" do
        = fa_icon :remove

i poprawka w metodzie zwracajacej parametry formularza.

def get_data_set_params
  _permit = [
    :configuration_id,
    :max_packages_on_server,
    :reports_synch_interval_min,
    { data_set_users_attributes: [
      params[:data_set][:data_set_users_attributes].keys.map do |c|
        { "#{c}" => [:user_id, :send_alerts, :_destroy] }
      end
    ] },
    { data_set_synch_agents_attributes: [ 
      params[:data_set][:data_set_synch_agents_attributes].keys.map do |c|
        { "#{c}" => [:synch_agent_id, :max_idle_on_data_set, :_destroy] }
      end
    ] }
  ]
  params.require(:data_set)
  .permit(
    _permit
  )
end

a teraz upiększymy kod.

Pozdrawiam.