Rails has_many через форму с флажками и дополнительным полем в модели объединения
Я пытаюсь решить довольно обычную задачу (как я думал).
Есть три модели:
class Product < ActiveRecord::Base
validates :name, presence: true
has_many :categorizations
has_many :categories, :through => :categorizations
accepts_nested_attributes_for :categorizations
end
class Categorization < ActiveRecord::Base
belongs_to :product
belongs_to :category
validates :description, presence: true # note the additional field here
end
class Category < ActiveRecord::Base
validates :name, presence: true
end
Мои проблемы начинаются, когда дело доходит до новой формы/редактирования продукта.
При создании продукта мне нужно проверить категории (через флажки), к которым он принадлежит. Я знаю, что это можно сделать, создав флажки с именем типа 'product [category_ids] []'. Но мне также нужно ввести описание для каждого из проверенных отношений, которые будут сохранены в модели соединения (категоризация).
Я видел эти красивые Railscasts на сложных формах, флажках habtm и т.д. Я искал StackOverflow. Но мне это не удалось.
Я нашел один пост, который описывает почти ту же проблему, что и моя. И последний ответ имеет для меня какой-то смысл (похоже, это правильный путь). Но это не работает нормально (т.е. Если проверка не завершена). Я хочу, чтобы категории отображались всегда в том же порядке (в новых/редактируемых формах, до/после проверки) и флажках, чтобы оставаться там, где они были, если проверка не удалась и т.д.
Любые тысячи оценены.
Я новичок в Rails (переключение с CakePHP), поэтому, пожалуйста, будьте терпеливы и напишите как можно подробнее. Пожалуйста, укажите мне правильный путь!
Спасибо.:)
Ответы
Ответ 1
Похоже, я понял это! Вот что я получил:
Мои модели:
class Product < ActiveRecord::Base
has_many :categorizations, dependent: :destroy
has_many :categories, through: :categorizations
accepts_nested_attributes_for :categorizations, allow_destroy: true
validates :name, presence: true
def initialized_categorizations # this is the key method
[].tap do |o|
Category.all.each do |category|
if c = categorizations.find { |c| c.category_id == category.id }
o << c.tap { |c| c.enable ||= true }
else
o << Categorization.new(category: category)
end
end
end
end
end
class Category < ActiveRecord::Base
has_many :categorizations, dependent: :destroy
has_many :products, through: :categorizations
validates :name, presence: true
end
class Categorization < ActiveRecord::Base
belongs_to :product
belongs_to :category
validates :description, presence: true
attr_accessor :enable # nice little thingy here
end
Форма:
<%= form_for(@product) do |f| %>
...
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<%= f.fields_for :categorizations, @product.initialized_categorizations do |builder| %>
<% category = builder.object.category %>
<%= builder.hidden_field :category_id %>
<div class="field">
<%= builder.label :enable, category.name %>
<%= builder.check_box :enable %>
</div>
<div class="field">
<%= builder.label :description %><br />
<%= builder.text_field :description %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
И контроллер:
class ProductsController < ApplicationController
before_filter :process_categorizations_attrs, only: [:create, :update]
def process_categorizations_attrs
params[:product][:categorizations_attributes].values.each do |cat_attr|
cat_attr[:_destroy] = true if cat_attr[:enable] != '1'
end
end
...
# all the rest is a standard scaffolded code
end
С первого взгляда он работает отлично. Надеюсь, что это не сломается...:)
Спасибо всем. Особая благодарность Сандипу Рансину за участие в обсуждении. Надеюсь, это будет полезно для кого-то вроде меня.
Ответ 2
используйте accepts_nested_attributes_for
для вставки в intermediate table
i.e. categorizations
вид будет выглядеть так:
# make sure to build product categorizations at controller level if not already
class ProductsController < ApplicationController
before_filter :build_product, :only => [:new]
before_filter :load_product, :only => [:edit]
before_filter :build_or_load_categorization, :only => [:new, :edit]
def create
@product.attributes = params[:product]
if @product.save
flash[:success] = I18n.t('product.create.success')
redirect_to :action => :index
else
render_with_categorization(:new)
end
end
def update
@product.attributes = params[:product]
if @product.save
flash[:success] = I18n.t('product.update.success')
redirect_to :action => :index
else
render_with_categorization(:edit)
end
end
private
def build_product
@product = Product.new
end
def load_product
@product = Product.find_by_id(params[:id])
@product || invalid_url
end
def build_or_load_categorization
Category.where('id not in (?)', @product.categories).each do |c|
@product.categorizations.new(:category => c)
end
end
def render_with_categorization(template)
build_or_load_categorization
render :action => template
end
end
Внутренний вид
= form_for @product do |f|
= f.fields_for :categorizations do |c|
%label= c.object.category.name
= c.check_box :category_id, {}, c.object.category_id, nil
%label Description
= c.text_field :description
Ответ 3
Я просто сделал следующее. Это сработало для меня.
<%= f.label :category, "Category" %>
<%= f.select :category_ids, Category.order('name ASC').all.collect {|c| [c.name, c.id]}, {} %>