14

Estoy tratando de resolver una tarea bastante común (como pensaba).Rails has_many a través de formulario con casillas de verificación y campo adicional en el modelo de unión

No tenemos tres modelos:

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 

Mis problemas comienzan cuando se trata de forma del producto/editar nuevo.

Al crear un producto, necesito verificar las categorías (mediante casillas de verificación) a las que pertenece. Sé que se puede hacer creando casillas con el nombre 'producto [category_ids] []'. Pero también necesito ingresar una descripción para cada una de las relaciones marcadas que se almacenarán en el modelo de unión (Categorización).

Vi esos hermosos Railscasts en formularios complejos, casillas de verificación de habtm, etc. He estado buscando StackOverflow apenas. Pero no he tenido éxito.

Encontré un post que describe casi exactamente el mismo problema que el mío. Y la última respuesta tiene algún sentido para mí (parece que es la forma correcta de hacerlo). Pero en realidad no está funcionando bien (es decir, si la validación falla). Quiero que las categorías se muestren siempre en el mismo orden (en formularios nuevos/de edición, antes/después de la validación) y casillas de verificación para permanecer donde estaban si falla la validación, etc.

Cualquier cosa apreciada. Soy nuevo en Rails (cambio desde CakePHP) así que sea paciente y escriba lo más detallado posible. Por favor, apúntame de la manera correcta!

Gracias. :)

Respuesta

26

Parece que lo he descubierto! Aquí es lo que tengo:

Mis modelos:

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 

la forma:

<%= 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 %> 

y el controlador:

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 

Desde el primer vistazo funciona muy bien. Espero que no se rompa de alguna manera ... :)

Gracias a todos. Un agradecimiento especial a Sandip Ransing por participar en la discusión. Espero que sea útil para alguien como yo.

+1

muy bien hecho. me da la sensación de que puede haber una manera más fácil, sin embargo. – courtsimas

+0

Muchas gracias por compartir, también tuve que complementar con http://stackoverflow.com/a/15920542/148421 porque mis valores no se guardaban y me faltaba cómo permitir los atributos anidados – Andrea

1

uso accepts_nested_attributes_for para insertar en intermediate table decir categorizations forma vista parecerá -

# 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 

Vista interior

= 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 
+0

¡Gracias! Pero parece que tu solución no funciona bien con la validación y otras cosas. Las casillas de verificación siempre están marcadas e incluso si las desactivo, se ignoran (vuelven después de la validación fallida o si las relaciones de aprobación de validación no se eliminan). También quiero enumerar las categorías siempre en el mismo orden (en su ejemplo, agrega categorías no asignadas después de las que ya están asociadas con el producto). ¿Algunas ideas? – ok32

+0

puede hacer un pedido en la colección '@ product.categorizations' –

+0

¿puede pegar los parámetros que se pasan? –

1

Acabo de hacer lo siguiente. Funcionó para mí ..

<%= f.label :category, "Category" %> 
<%= f.select :category_ids, Category.order('name ASC').all.collect {|c| [c.name, c.id]}, {} %> 
+0

Esto no permite una unión adicional modelo de campo como el operador pidió – dft

Cuestiones relacionadas