Problem z listą wielokrotnego wyboru

Mam tabele books i authors w relacji wiele do wielu (tabela buforowa authorbooks). Wyświetla dane, jednak przy próbie wysłania formularza dane z list nie przechodzą walidacji.

formularz:

[code] <%= fields_for(:author_book) do |ab| %>

<%= ab.label "Autorzy" %>
<%= collection_select(:authors, :id, Author.all, :id, :last_name, {}, {:multiple => true} ) %>
<% end %>

<%= fields_for(:category_book) do |cb| %>

<%= cb.label "Kategorie" %>
<%= collection_select(:categories, :id, Category.all, :id, :name, {}, {:multiple => true} ) %>
[/code] fragment kontrolera: [code] # GET /books/new # GET /books/new.json def new @book = Book.new @author_book = @book.authorbooks.build @category_book = @book.categorybooks.build
respond_to do |format|
  format.html # new.html.erb
  format.json { render json: @book }
end

end

GET /books/1/edit

def edit
@book = Book.find(params[:id])
end

POST /books

POST /books.json

def create
@book = Book.new(params[:book])

params[:authors][:id].each do |author|
  if !author.empty?
    @book.authorbooks.build(:author_id => author)
  end
end

params[:categories][:id].each do |category|
  if !category.empty?
    @book.categorybooks.build(:category_id => category)
  end
end

respond_to do |format|
  if @book.save
    format.html { redirect_to @book, notice: 'Book was successfully created.' }
    format.json { render json: @book, status: :created, location: @book }
  else
    format.html { render action: "new" }
    format.json { render json: @book.errors, status: :unprocessable_entity }
  end
end

end[/code]
log:

Processing by BooksController#create as HTML Parameters: {"utf8"=>"âś“", "authenticity_token"=>"YySsW0VfFaqtawltpqbWA34MH5cN7XGOiJ//VDDZj5Q=", "book"=>{"title"=>"Zbrodnia i Kara", "description"=>"asd", "isbn"=>"hfgurenf", "year"=>"8"}, "authors"=>{"id"=>["", "2"]}, "categories"=>{"id"=>["", "1", "2"]}, "commit"=>"Create Book"}
pierwszy element tablicy jest zawsze pusty.

Dlaczego tak jest i co można z tym zrobić? :wink:

Widocznie masz coś nie tak w liście autorów. Podeślij html tego formularza.

form:

[code]<%= form_for(@book) do |f| %>
<% if @book.errors.any? %>


<%= pluralize(@book.errors.count, “error”) %> prohibited this book from being saved:

  <ul>
  <% @book.errors.full_messages.each do |msg| %>
    <li><%= msg %></li>
  <% end %>
  </ul>
</div>

<% end %>

<%= f.label :title %>
<%= f.text_field :title %>
<%= f.label :description %>
<%= f.text_area :description %>
<%= f.label :isbn %>
<%= f.text_field :isbn %>
<%= f.label :year %>
<%= f.number_field :year %>

<%= fields_for(:author_book) do |ab| %>

<%= ab.label "Autorzy" %>
<%= collection_select(:authors, :id, Author.all, :id, :last_name, {}, {:multiple => true} ) %>
<% end %>

<%= fields_for(:category_book) do |cb| %>

<%= cb.label "Kategorie" %>
<%= collection_select(:categories, :id, Category.all, :id, :name, {}, {:multiple => true} ) %>
<% end %>
<%= f.submit %>
<% end %>[/code] Formularz jest raczej w porządu, wyświetla dane.

nie wiem czy w kontrolerze czegoś nie przekombinowałem:

[code] def new
@book = Book.new
@author_book = @book.authorbooks.build
@category_book = @book.categorybooks.build

respond_to do |format|
  format.html # new.html.erb
  format.json { render json: @book }
end

end

GET /books/1/edit

def edit
@book = Book.find(params[:id])
end

POST /books

POST /books.json

def create

params[:authors][:id].each do |author|
  if !author.empty?
    @book.authorbooks.build(:author_id => author)
  end
end
categories.id = categories.id.collect{|s| s.to_i}
params[:categories][:id].each do |category|
  if !category.empty?
    @book.categorybooks.build(:category_id => category)
  end
end

respond_to do |format|
  if @book.save
    format.html { redirect_to @book, notice: 'Book was successfully created.' }
    format.json { render json: @book, status: :created, location: @book }
  else
    format.html { render action: "new" }
    format.json { render json: @book.errors, status: :unprocessable_entity }
  end
end

end[/code]

Tak się składa, że ostatnio pracowałem przy podobnym temacie (powiązanie autorów z książkami) i u mnie działa to tak:

book.rb

[code=ruby]class Book < ActiveRecord::Base
attr_accessible :author_ids

has_and_belongs_to_many :authors

accepts_nested_attributes_for :authors
end[/code]
author.rb

[code=ruby]class Author < ActiveRecord::Base
attr_accessible :book_ids

has_and_belongs_to_many :books
end[/code]
books_controller.rb

class BooksController < ApplicationController def new @book = Book.new @authors = Author.all end end
books/_form.html.erb

[code=ruby]…
<% @authors.each do |author| %>

  • <%= check_box_tag 'book[author_ids][]', author.id, @book.author_ids.include?(author.id), id: dom_id(author), class: 'author-checkbox' %>
  • <% end %> ...[/code] Oczywiście są to przykładowe kawałki kodu, które należy wpleść w odpowiednie miejsca. Nie mniej jednak zasada działania powinna być jasna. Oprócz kodu należy jeszcze ręcznie stworzyć tabelę w bazie odpowiedzialną za relację many-to-many:

    xxx_create_authors_books_join_table.rb

    class CreateAuthorsBooksJoinTable < ActiveRecord::Migration def change create_table :authors_books, id: false do |table| table.integer :author_id table.integer :book_id end add_index :authors_books, [:author_id, :book_id], unique: true end end
    Dodatkowo zauważyłem, że przy aktualizacji istniejącego wpisu, gdy nie zaznaczymy żadnego autora (nie zostanie przesłany parametr author_ids), lista autorów pozostanie bez zmian. Jest to nieprawidłowe działanie, ponieważ chcemy usunąć wszystkich autorów. Problem rozwiązuje poniższy kod (umieszczony w metodzie update):

    books_controller.rb

    @book.authors.destroy_all unless params[:book].has_key?('author_ids')

    Dziękuję, jesteś niesamowity.

    Teraz muszę zmienić format wyświetlania, obecnie wyświetlają się checkboxy, a pod nimi wartości wszystkich kolumn.

    [#<Category id: 1, name: “dramat”, created_at: “2013-03-09 13:35:50”, updated_at: “2013-03-09 13:35:50”>, #<Category id: 2, name: “powieść”, created_at: “2013-03-09 13:35:57”, updated_at: “2013-03-09 13:35:57”>]

    Z obsługą dotarłem do prawie samego końca.
    Nie rozumiem błędu. kontroler w zły sposób przekazuje dane?

    [code]Started POST “/books” for 193.239.103.39 at 2013-03-09 14:53:14 +0100
    Processing by BooksController#create as HTML
    Parameters: {“utf8”=>“âś“”, “authenticity_token”=>“YySsW0VfFaqtawltpqbWA34MH5cN7XGOiJ//VDDZj5Q=”, “book”=>{“title”=>“Zbrodnia i Kara”, “description”=>"", “isbn”=>“hfgurenf”, “year”=>“12”, “author_ids”=>[“1”], “category_ids”=>[“1”, “2”]}, “commit”=>“Create Book”}
    Completed 500 Internal Server Error in 1ms

    NoMethodError (undefined method save' for nil:NilClass): app/controllers/books_controller.rb:47:inblock in create’
    app/controllers/books_controller.rb:46:in `create’[/code]
    Jeszcze (co dziwne) przy analogicznym rozwiązaniu z kategoriami, ruby szuka tabeli books_categories, w przypadku autorów authors_books (choć zaimplementowałem wszystko tak samo).

    [quote=kejwmen][code]Started POST “/books” for 193.239.103.39 at 2013-03-09 14:53:14 +0100
    Processing by BooksController#create as HTML
    Parameters: {“utf8”=>“âś“”, “authenticity_token”=>“YySsW0VfFaqtawltpqbWA34MH5cN7XGOiJ//VDDZj5Q=”, “book”=>{“title”=>“Zbrodnia i Kara”, “description”=>"", “isbn”=>“hfgurenf”, “year”=>“12”, “author_ids”=>[“1”], “category_ids”=>[“1”, “2”]}, “commit”=>“Create Book”}
    Completed 500 Internal Server Error in 1ms

    NoMethodError (undefined method save' for nil:NilClass): app/controllers/books_controller.rb:47:inblock in create’
    app/controllers/books_controller.rb:46:in `create’[/code]
    [/quote]
    Co do tego błędu to odpowiedź tkwi w:

    NoMethodError (undefined method `save' for nil:NilClass)

    Wynika z tego, że próbujesz wywołać metodę save na nieistniejącym obiekcie. Przyjrzyj się akcji create w kontrolerze książek.

    [quote=kejwmen][code=ruby]def create

    params[:authors][:id].each do |author|
      if !author.empty?
        @book.authorbooks.build(:author_id => author)
      end
    end
    categories.id = categories.id.collect{|s| s.to_i}
    params[:categories][:id].each do |category|
      if !category.empty?
        @book.categorybooks.build(:category_id => category)
      end
    end
    
    respond_to do |format|
      if @book.save
        format.html { redirect_to @book, notice: 'Book was successfully created.' }
        format.json { render json: @book, status: :created, location: @book }
      else
        format.html { render action: "new" }
        format.json { render json: @book.errors, status: :unprocessable_entity }
      end
    end
    

    end[/code]
    [/quote]
    Wygląda to dość podejrzanie. Zmienna instancyjna @book nie jest nigdzie tworzona, także nie ma co się dziwić, że wali błędem.
    Jeśli rozwiązujesz to podobnie do tego o czym pisałem wcześniej to ta metoda mogłaby wyglądać tak:

    [code=ruby]def create
    @book = Book.new(params[:book])

    if @book.save
    redirect_to @book, notice: ‘Book was successfully created.’
    else
    render action: “new”
    end
    end[/code]
    Oczywiście jeśli faktycznie potrzebujesz odpowiadać json’em to należy dołożyć metodę respond_to.

    Zawartość create przed respond się pozbyłem (nie potrzebna przy tym koncepcie).

    Obecny kontroler wygląda tak:

    [code] # GET /books/new

    GET /books/new.json

    def new
    @book = Book.new
    @authors = Author.all
    @categories = Category.all

    respond_to do |format|
      format.html # new.html.erb
      format.json { render json: @book }
    end
    

    end

    POST /books

    POST /books.json

    def create
    @book = Book.new(params[:book]) //dodane zgodnie z Twoim zaleceniem
    respond_to do |format|
    if @book.save
    format.html { redirect_to @book, notice: ‘Book was successfully created.’ }
    format.json { render json: @book, status: :created, location: @book }
    else
    format.html { render action: “new” }
    format.json { render json: @book.errors, status: :unprocessable_entity }
    end
    end
    end

    PUT /books/1

    PUT /books/1.json

    def update
    @book = Book.find(params[:id])
    @book.authors.destroy_all unless params[:book].has_key?(‘author_ids’)
    @book.categories.destroy_all unless params[:book].has_key?(‘category_ids’)

    respond_to do |format|
      if @book.update_attributes(params[:book])
        format.html { redirect_to @book, notice: 'Book was successfully updated.' }
        format.json { head :no_content }
      else
        format.html { render action: "edit" }
        format.json { render json: @book.errors, status: :unprocessable_entity }
      end
    end
    

    end[/code]
    Tylko w tym momencie próbuje dodać puste wartości do bazy:

    [code]Started GET “/books/new” for 193.239.103.39 at 2013-03-09 18:56:26 +0100
    Processing by BooksController#new as HTML
    Rendered books/_form.html.erb (157.0ms)
    Rendered books/new.html.erb within layouts/application (167.4ms)
    Completed 200 OK in 294ms (Views: 162.5ms | ActiveRecord: 12.8ms)
    Started POST “/books” for 193.239.103.39 at 2013-03-09 18:56:36 +0100
    Processing by BooksController#create as HTML
    Parameters: {“utf8”=>“âś“”, “authenticity_token”=>“PTBw/jww26xkzIB5Ov4MtB/rIoLVCEHTa5u9r5ac5d0=”, “book”=>{“title”=>“asd”, “description”=>“hgfh”, “isbn”=>“fgh”, “year”=>“7”, “author_ids”=>[“2”], “category_ids”=>[“2”]}, “commit”=>“Create Book”}
    Completed 500 Internal Server Error in 52ms

    ActiveRecord::StatementInvalid (SQLite3::ConstraintException: books.author_id may not be NULL: INSERT INTO “books” (“author_id”, “category_id”, “created_at”, “description”, “isbn”, “title”, “updated_at”, “year”) VALUES (?, ?, ?, ?, ?, ?, ?, ?)):
    app/controllers/books_controller.rb:47:in block in create' app/controllers/books_controller.rb:46:increate’[/code]
    czego nie rozumiem, bo kilka linijek wcześniej według loga :book istnieje (i ma w sobie dane).

    teraz :book też ma w sobie dane… rzuca się o author_id to pole nie może być puste w Twoim przypadku

    widze ze masz jakieś author_ids w danych :book a w zapytaniu oczekuje author_id (to może to) nie zagłębiałem się w kod

    @kejwmen
    Podeślij modele bo wygląda na to, że coś tam jeszcze nie gra. schema.rb też się przyda.

    miałem straszny bajzel, skończyło się na restarcie całego projektu
    W postaci którą podsyłam, książka się dodaje, jednak bez informacji o autorze i kategorii (brakuje czegoś w kontrolerze).

    author.rb

    [code]class Author < ActiveRecord::Base
    attr_accessible :description, :name, :book_ids

    has_and_belongs_to_many :books
    end[/code]
    book.rb

    class Book < ActiveRecord::Base attr_accessible :description, :isbn, :title, :year has_and_belongs_to_many :authors has_and_belongs_to_many :categories accepts_nested_attributes_for :authors accepts_nested_attributes_for :categories end
    category.rb analogicznie do authors.rb

    books_controller.rb

    [code] # GET /books/new

    GET /books/new.json

    def new
    @book = Book.new
    @authors = Author.all
    @categories = Category.all
    respond_to do |format|
    format.html # new.html.erb
    format.json { render json: @book }
    end
    end

    GET /books/1/edit

    def edit
    @book = Book.find(params[:id])
    end

    POST /books

    POST /books.json

    def create
    @book = Book.new(params[:book])

    respond_to do |format|
      if @book.save
        format.html { redirect_to @book, notice: 'Book was successfully created.' }
        format.json { render json: @book, status: :created, location: @book }
      else
        format.html { render action: "new" }
        format.json { render json: @book.errors, status: :unprocessable_entity }
      end
    end
    

    end[/code]
    fragment _form

    [code] <%= @authors.each do |author| %>

  • <%= check_box_tag 'book[author_ids][]', author.id, @book.author_ids.include?(author.id), id: dom_id(author), class: 'author-checkbox' %>
  • <% end %> <%= @categories.each do |category| %>
  • <%= check_box_tag 'book[category_ids][]', category.id, @book.author_ids.include?(category.id), id: dom_id(category), class: 'category-checkbox' %>
  • <% end %>[/code] schema.rb [code]ActiveRecord::Schema.define(:version => 20130309192836) do

    create_table “authors”, :force => true do |t|
    t.string “name”
    t.string “description”
    t.datetime “created_at”, :null => false
    t.datetime “updated_at”, :null => false
    end

    create_table “authors_books”, :id => false, :force => true do |t|
    t.integer “author_id”
    t.integer “book_id”
    end

    add_index “authors_books”, [“author_id”, “book_id”], :name => “index_authors_books_on_author_id_and_book_id”

    create_table “books”, :force => true do |t|
    t.string “title”
    t.text “description”
    t.string “isbn”
    t.integer “year”
    t.datetime “created_at”, :null => false
    t.datetime “updated_at”, :null => false
    end

    create_table “books_categories”, :id => false, :force => true do |t|
    t.integer “book_id”
    t.integer “category_id”
    end

    add_index “books_categories”, [“book_id”, “category_id”], :name => “index_books_categories_on_book_id_and_category_id”

    create_table “categories”, :force => true do |t|
    t.string “name”
    t.string “description”
    t.datetime “created_at”, :null => false
    t.datetime “updated_at”, :null => false
    end

    end[/code]
    migacja dla authorsbooks

    [code]class CreateAuthorsBooksTable < ActiveRecord::Migration
    def self.up
    create_table ‘authors_books’, :id => false do |t|
    t.integer :author_id
    t.integer :book_id
    end
    add_index :authors_books, [:author_id, :book_id]
    end

    def self.down
    drop_table :authors_books
    end
    end[/code]
    Nie wiem czy dobrze myślę, ale po odebraniu formularza dane trzeba jakoś przetworzyć i dodać do tabeli authorsbooks.

    [quote=kejwmen]book.rb

    class Book < ActiveRecord::Base attr_accessible :description, :isbn, :title, :year has_and_belongs_to_many :authors has_and_belongs_to_many :categories accepts_nested_attributes_for :authors accepts_nested_attributes_for :categories end
    [/quote]
    Dodaj do modelu książek atrybuty :author_ids oraz category_ids, powinno zadziałać. Rozumiem, że model category.rb wygląda analogicznie do author.rb.

    Ps. koloruj składnię, lepiej się czyta (tag code=ruby) :slight_smile:

    [quote=Dawids][quote=kejwmen]book.rb

    class Book < ActiveRecord::Base attr_accessible :description, :isbn, :title, :year has_and_belongs_to_many :authors has_and_belongs_to_many :categories accepts_nested_attributes_for :authors accepts_nested_attributes_for :categories end
    [/quote]
    Dodaj do modelu książek atrybuty :author_ids oraz category_ids, powinno zadziałać. Rozumiem, że model category.rb wygląda analogicznie do author.rb.

    Ps. koloruj składnię, lepiej się czyta (tag code=ruby) :)[/quote]
    Jak mogłem tego nie zauważyć, teraz wszystko pięknie działa.