0

I have a model Books and a model Authors.

The form for adding books, contains a nested for allowing to add authors. That works. However, I have an autocomplete function on the authors fields, so when the form is posted to the controller, the author (almost) for sure exists in the database.

I should somehow do a find_or_initialize_by on the nested attributed.

I'm maybe looking at the wrong place, but I can't find this in the rails guides. I tried this (found on SO):

def create

    @book = Book.new(params_book)
    small_name = params[:book][:authors_attributes]["0"]["name"].downcase
    aut_id = Author.where("\"authors\".\"name\" = :name",{name: small_name}).pluck(:id).join
    @book.authors = Author.find_or_initialize_by(id: aut_id)

    if @book.save
        redirect_to see_book_url(Book.last)
    else
       render 'new'
    end
end

This creates an error:

undefined method `each' for #<Author:0x007fac59c7e1a8>

referring to the line @book.authors = Author.find_or_initialize_by(id: aut_id)

EDIT

After the comments on this question, I updated the code to this:

def create

    book_params = params_book
    small_name = params[:book][:authors_attributes]["0"]["name"].downcase
    id = Author.where("\"authors\".\"name\" = :name",{name: small_name}).pluck(:id).join
    book_params["authors_attributes"]["0"]["id"] = id

    @book = Book.new(book_params)

    if @book.save
          redirect_to see_book_url(Biblio.last)
    else
        ....

The book params look like this:

<ActionController::Parameters {"title"=>"Testus Testa",
 "authors_attributes"=><ActionController::Parameters {
    "0"=><ActionController::Parameters {"name"=>"Vabien", "id"=>"22"}
         permitted: true>} permitted: true>} permitted: true>

That looks fine to me, BUT, I get this error:

ActiveRecord::RecordNotFound in Administration::BooksController#create
Couldn't find Author with ID=22 for Book with ID=
thiebo
  • 1,339
  • 1
  • 17
  • 37

2 Answers2

1

Ok so the easiest way to get what you want is to change autocomplete in your form from an array of names like: ['author 1 name', 'author 2 name'] change it to an array of objects containing the name and id of the author like: [{label: 'author 1 name', value: 0}, {label: 'author 2 name', value: 1}] so then as long as that form field is now for "id" instead of "name" then in your controller all you have to do is:

def create
    @book = Book.new(params_book)
    if @book.save
        redirect_to see_book_url(Book.last)
    else
       render 'new'
    end
end

Because only attributes without an ID will be created as new objects. Just make sure you set accepts_nested_attributes_for :authors in your Book model.


The error you are getting is because @book.authors is a many relationship so it expects a collection when you set it not an individual author. To add an individual author to the collection you do @book.authors << Author.find_or_initialize_by(id: aut_id) instead of @book.authors = Author.find_or_initialize_by(id: aut_id) although its redundant to fetch the id using the name just to initialize with an id. The id will be created automatically. Use Author.find_or_initialize_by(name: small_name) instead.

In your current code you have multiple authors being created not only due to the lack of "id" being used but because @book = Book.new(params_book) passes the nested attributes to the object initializer and then after you are accessing the nested attribute params and adding authors again. Also if you have multiple authors with the same name then Author.where("\"authors\".\"name\" = :name",{name: small_name}).pluck(:id).join would actually make an ID out of the combined ID of all authors with that name.

If you want to do it manually then remove :authors_attributes from your permit in "params_book" method so it won't be passed to Book.new then do the following:

def create
    @book = Book.new(params_book)
    params[:book][:author_attributes].each{|k,v| @book.authors << Author.find_or_initialize_by(name: v['name'])}

    if @book.save
        redirect_to see_book_url(Book.last)
    else
       render 'new'
    end
end

Let me know if you have trouble!

After response from poster

remove :authors_attributes from your permit in "params_book" method and try this:

def create
    @book = Book.new(params_book)
    @book.authors_attributes = params[:book][:author_attributes].inject({}){|hash,(k,v)| hash[k] = Author.find_or_initialize_by(name: v['name']).attributes.merge(v) and hash}

    if @book.save
        redirect_to see_book_url(Book.last)
    else
       render 'new'
    end
end
Jose Castellanos
  • 528
  • 4
  • 11
  • Wow ! Thanks a lot ! I do have a little problem with this though. I had a very similar method in my books_controller and the reason why I didn't do this, is because I then can't validate the author parameters. By doing as you suggested, how can I validate, i.e. send the form back if no author is chosen, i.e. generate error messages that are concomitant with other missing fields, such as the title not being filled out? – thiebo Feb 08 '17 at 20:59
  • @thiebo the suggested way I put at the top should validate author attributes. If you want to do it the way I show on the bottom see if this works.. Instead of `params[:book][:author_attributes].each{|k,v| @book.authors << Author.find_or_initialize_by(name: v['name'])}` try `@book.authors_attributes = params[:book][:author_attributes].inject({}){|hash,(k,v)| hash[k] = Author.find_or_initialize_by(name: v['name']).attributes.merge(v) and hash}` – Jose Castellanos Feb 08 '17 at 21:10
  • I'll check that one out tomorrow. Thanks a lot for this great answer. I'll post if problems (and if everything is fine ;) ) – thiebo Feb 08 '17 at 21:17
  • @thiebo for sure, let me know how it goes! – Jose Castellanos Feb 08 '17 at 21:25
  • The second try that you proposed created 2 authors entries into the db : `@book.authors_attributes = params[:book][:author_attributes].inject({}){|hash,(k,v)| hash[k] = Author.find_or_initialize_by(name: v['name']).attributes.merge(v) and hash}` . As you suggested that only entries without an id would be newly created, I added the id to the params. I edited the question to show what I now do. – thiebo Feb 10 '17 at 12:39
0

Solved, thanks a lot to Jose Castellanos and this post:

Adding existing has_many records to new record with accepts_nested_attributes_for

The code now is:

# the strong params isn't a Hash, so this is necessary 
# to manipulate data in params :
book_params = params_book

# All registrations in the DB are small case
small_name = params[:book][:authors_attributes]["0"]["name"].downcase

# the form sends the author's name, but I need to test against the id:
id = Author.where("\"authors\".\"name\" = :name",{name: small_name}).pluck(:id).join
book_params["authors_attributes"]["0"]["name"] = params[:book][:authors_attributes]["0"]["name"].downcase

# this author_ids is the line that I was missing! necessary to 
# test whether the author already exists and avoids adding a 
# new identical author to the DB.
book_params["author_ids"] = id
book_params["authors_attributes"]["0"]["id"] = id

# the rest is pretty standard:
@book = Book.new(book_params)

if @book.save
  redirect_to see_book_url(Book.last)
else
Community
  • 1
  • 1
thiebo
  • 1,339
  • 1
  • 17
  • 37
  • Glad I could help! Sorry, I wasn't aware you didn't have the nested attributes set up in your model or else I would have put that. However I am still a little confused by your code. It seems you are only finding the id of the first author and only the first author. Additionally if you have 2 authors by the same name your code will not work which is probably the error you were getting before. – Jose Castellanos Feb 10 '17 at 15:10
  • Lets say an author's name is simply "bob" and he has an id of 1. Another other also know as "bob" has an id of 2. The code `id = Author.where("\"authors\".\"name\" = :name",{name: 'bob'}).pluck(:id).join` returns '12' which is not an ID of any bob or may not be an id of any other either. – Jose Castellanos Feb 10 '17 at 15:12
  • Concering the authors, I changed the code so the autocomplete gets the authors `given_name` and `name` and the query for getting the id takes both the name and the given_name as arguments. – thiebo Feb 10 '17 at 15:36