5

I am creating a Rails library app where a genre has_many books and a book belongs_to one genre. I need to use form objects because my data model will eventually have multiple many to many relationships, and because I want to learn how to use form objects. I've based my form object off Rails Cast #416 Form Objects. My form, form object, models, and controller all seem to work. New books are created and they are associated with genres, but they are all of the genre "genre". I am using find_or_create_by. I think the problem lies in the book_form.rb where @genre ||= Genre.find_by(name: :name) is not actually passing in the Genre information from the form. My relevant code is as follows:

Model book.rb

class Book < ActiveRecord::Base
  belongs_to :genre
  before_save :convert_isbn
.
.
.
end

Model genre.rb

class Genre < ActiveRecord::Base
  has_many :books, dependent: :destroy
  validates_associated :books
end

book_form.rb

class BookForm
  include ActiveModel::Model

  def self.model_name
    ActiveModel::Name.new(self, nil, "Book")
  end
.
.
.
  validates :name, presence: true

  delegate :name, to: :genre
  delegate :title, :year, :pages, :isbn, :summary, :volume_a, :volume_b, to: :book

  def genre
    @genre ||= Genre.find_by(name: :name)
  end

  def book
    @book ||= genre.books.build
  end

  def submit(params)
    genre.attributes = params.slice(:name).permit(:name)
    book.attributes = params.slice(:title, :year, :pages, :isbn, :summary, :volume_a,:volume_b).permit(:title, :year, :pages, :isbn, :summary, :volume_a,:volume_b)
    if valid?
      genre.save!
      book.save!
      true
    else
      false
    end
  end
end

books_controller.rb

class BooksController < ApplicationController
  before_action :admin_user, only: [:new, :edit, :destroy]

  def new
    @book_form = BookForm.new
    @genres = Genre.all
  end

  def create
    @book_form = BookForm.new
    @genres = Genre.all
    if @book_form.submit(params[:book])
      flash[:success] = "You've added a new book to the library!"
      redirect_to @book_form
    else
      render 'new'
    end
  end

View new.html.erb

<%= form_for @book_form do |f| %>
  <%=render 'shared/error_messages', object: f.object %>

  <%= f.label :title %>
  <%= f.text_field :title, class: 'form-control' %>
  .
  .
  .
  <%= f.label :genre %><br>

  <%= collection_select(:genre, :name, Genre.all, :name, :name) %>
  <br>
  <br>
  <%= f.submit "Add book to library", class: "btn btn-primary" %>
<% end %>

Using the Pry gem, I get this from the server on creating a book:

Started POST "/books" for ::1 at 2016-02-22 14:19:20 -0700
Processing by BooksController#create as HTML
  Parameters: {"utf8"=>"✓",     "authenticity_token"=>"bPusgyl9n+p07eQsEAe9CpSsithtkg33HMifj8KTsidv3GDLuhjibOC7d2mm5boC4w7ZUne64R4n4OMQotDE4g==",
               "book"=>{"title"=>"test",
               "year"=>"2016",
               "pages"=>"222",
               "isbn"=>"9780-xx-xx-xx",
               "summary"=>"fake summary",
               "volume_a"=>"1",
               "volume_b"=>"2"},
               "genre"=>{"name"=>"Mystery"},
               "commit"=>"Add book to library"}

From: /home/nathaniel/rails_apps/allredlib/app/forms/book_form.rb @ line 38 BookForm#submit:

    37: def submit(params)
 => 38:   binding.pry
    39:   genre.attributes = params.slice(:name).permit(:name)
    40:   book.attributes = params.slice(:title, :year, :pages, :isbn, :summary, :volume_a,:volume_b).permit(:title, :year, :pages, :isbn, :summary, :volume_a,:volume_b)
    41:   if valid?
    42:     genre.save!
    43:     book.save!
    44:     true
    45:   else
    46:     false
    47:   end
    48: end

So book genre is getting passed in the parameters, but I'm accessing it the wrong way in my book form. If I comment out binding.pry, the form creates a new book with genre "name" instead of genre "Mystery" like I want it to do.

When I type @genre into rails while using binding.pry, I get

[1] pry(#<BookForm>)> @genre
=> #<Genre:0x007f19554e1220
 id: 20,
 name: "name",
 book_id: nil,
 created_at: Thu, 25 Feb 2016 20:28:45 UTC +00:00,
 updated_at: Thu, 25 Feb 2016 20:28:45 UTC +00:00>

latest binding.pry results 27: def genre => 28: binding.pry 29: @genre ||= Genre.find_or_initialize_by(name: :name) 30: #@genre ||= Genre.find_or_initialize_by(name: params[:genre][:name]) 31: end

neallred
  • 740
  • 1
  • 8
  • 24
  • 1
    I think you may be looking for this? http://stackoverflow.com/questions/7537180/how-to-pass-argument-to-delegate-method-in-rails – lacostenycoder Feb 20 '16 at 06:51
  • Yes, I think so. I'm pretty new to Rails and don't know how to implement their solution the right way. Any pointers? – neallred Feb 20 '16 at 18:48

2 Answers2

3

If you're going to look up the Genre by it's name then you need to pass in the name attribute in the method's options hash.

def genre
  @genre ||= Genre.find_or_create_by(name: :name)
end

UPDATE

change

genre.attributes = params.slice(:name).permit(:name)

to

genre.attributes = { :name => params[:genre][:name] }

Also throw binding.pry before and after genre.save! and see what you get if you type @genre because your genre method should have created that instance variable which your just sending params to with genre.attributes which accepts the hash.

I would consider the same on book params but if slice is working than fine.

Also, since you're calling Genre.all in your form view, you probably don't need to do this in the controller actions, but if you do, then consider using the object @genres inside your form view collection_select instead because you're making the all request twice on the model.

@genres = Genre.all
lacostenycoder
  • 10,623
  • 4
  • 31
  • 48
  • This is very helpful. I've added the output from Pry at the bottom of my answer. The right Genre is at least passed into the hash, so it looks like `name: :name` from `@genre ||= Genre.find_or_create_by(name: :name)` is the wrong way to access the genre name. I can't access it using @genre.name or genre.name, that causes an error or crashes the server. I agree I need testing but I don't know enough about testing yet to write helpful tests. – neallred Feb 22 '16 at 21:29
  • I'll have a look at this. – lacostenycoder Feb 22 '16 at 21:39
  • It is ` Parameters: {"utf8"=>"✓", "authenticity_token"=>"bPusgyl9n+p07eQsEAe9CpSsithtkg33HMifj8KTsidv3GDLuhjibOC7d2mm5boC4w7ZUne64R4n4OMQotDE4g==", "book"=>{"title"=>"test", "year"=>"2016", "pages"=>"222", "isbn"=>"9780-xx-xx-xx", "summary"=>"fake summary", "volume_a"=>"1", "volume_b"=>"2"}, "genre"=>{"name"=>"Mystery"}, "commit"=>"Add book to library"}` – neallred Feb 22 '16 at 21:41
  • I updated my answer, hopefully this gets it working for you. – lacostenycoder Feb 23 '16 at 14:04
  • Sadly, neither solution is working. When I try `genre.name = params[:genre][:name]`, I get an error "undefined method `[]' for nil:NilClass". When I try `genre.attributes = params.slice(:genre).permit(:name)` , I get the same effect as before where it searched for a Genre name of "name" to associate it with. One clue that might help is that when I reset the database so that Genre name of "name" has not been created yet, `@genre ||= Genre.find_by(name: :name)` fails. If I use `find_or_create_by` instead, loading the form page causes creation of genre name "name" before the form is even sumitted – neallred Feb 25 '16 at 19:18
  • I wondering if this is a good use of form objects. What is the main purpose of this use case? change ```Genre.find_or_create_by(genre: :genre)``` to ```Genre.find_or_initialize_by(genre: :genre)``` so that the genre won't get created, but just instantiated without saving to database. – lacostenycoder Feb 25 '16 at 20:11
  • For this particular use case, I merely want to create a new book with an already existing genre associated to it. However, I soon want to add an author model and a comments model that would both have many-to-many relationships with book. I want to use this simpler case to learn form objects so that I can use a form object on the more complicated relationships. – neallred Feb 25 '16 at 20:14
  • 1
    Watch Railscast video again and try to understand WHY you want to use a form object. Rails works fine out of the box using it's associations. I could see it being useful if you were building a new associated model in a belongs to relationship, perhaps Book has_one Author. But I think you're adding a layer of complexity here where it doesn't make much sense. What happens if you do ```genre.attributes = params.slice(:genre)``` and leave off the rest? – lacostenycoder Feb 25 '16 at 20:23
  • I can watch the video again. When I change `genre.attributes = params.slice(:genre).permit(:name)` to `genre.attributes = params.slice(:genre)`, I get the same outcome. – neallred Feb 25 '16 at 20:30
  • Updated my answer again. see ```genre.attributes = { :name => params[:genre][:name] }``` – lacostenycoder Feb 25 '16 at 22:32
  • 1
    also this might help you. I don't know what's going on with ```validates_associated :books``` but looks like you could be doing double validations? http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_associated and http://apidock.com/rails/ActiveRecord/Base/attributes%3D – lacostenycoder Feb 25 '16 at 22:36
  • Using `genre.attributes = { :name => params[:genre][:name] }`, I get "undefined method '[]' for nil:NilClass". When I type in `@genre` into the console after inserting binding.pry, I get a Genre of name "name" and a nil book (see end of question above). – neallred Feb 25 '16 at 22:53
  • 1
    When the genre method gets first called, it creates an instance of @genre because it's finding a match in the database of "name" . Run ```rails c``` and ```Genre.all.find_by(name: 'name')``` Which will return the first match. Do ```Genre.where(name: 'name').count``` which will tell you how many matches are in the database. You could delete them all with ```Genre.where(name: 'name').delete_all``` But this is the problem. You need to use ```@genre =|| Genre.find_or_initialize_by(name: params[:genre][:name])``` – lacostenycoder Feb 26 '16 at 18:32
  • Also try commenting out ```validates :name, presence: true``` and see if that changes anything. – lacostenycoder Feb 26 '16 at 18:38
  • I've deleted the Genres of name "name. Only one such record ever gets created. Using `@genre ||= Genre.find_or_initialize_by(name: params[:genre][:name])` and `genre.attributes = params.slice(:genre).permit(:name)`, I get the error "undefined local variable or method 'params' for #" – neallred Feb 26 '16 at 19:05
  • It lets me load the page, but I get the error when I go to save the new record. I also commented out `validates :name, presence: true` – neallred Feb 26 '16 at 19:11
  • Try this without validation. ```@genre ||= Genre.find_or_initialize_by(name: :name)``` and also put a ```binding.pry``` before that line, see what's happening each time it gets called. The problem may also be that if your Genre already exists in the database, there's no need to save it again. This is why I think your form object doesn't make sense. You only need to create a new and save a new Genre if it doesn't exist. If it's already in the database, then no need to create it or even update it. Thus only save if @genre.persisted? == false. – lacostenycoder Feb 26 '16 at 19:18
  • With `binding.pry` before `@genre ||= Genre.find_or_initialize_by(name: :name)`, I get the result shown at the end of my question. The form page does not finish loading. – neallred Feb 26 '16 at 20:23
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/104676/discussion-between-lacostenycoder-and-neallred). – lacostenycoder Feb 26 '16 at 20:27
0

You are creating the Genre with genre set to genre every time. You don't want to create a Genre, you probably just want to instantiate a Genre (with new).

Try changing this

@genre ||= Genre.find_or_create_by(genre: :genre)

to something like this

@genre ||= Genre.new

Also, I would suggest renaming the field "genre" on the model Genre to something else like "name". Otherwise, things get really confusing.

I think this line:

collection_select(:genre, :id, Genre.all, :genre, :genre)

should be more like:

collection_select(:genre, :genre, Genre.all, :genre, :genre)

and if you were to change to "name":

collection_select(:genre, :name, Genre.all, :name, :name)
msergeant
  • 4,771
  • 3
  • 25
  • 26
  • That is true. The problem with @genre ||= Genre.new is that it won't find or create my genre. It only creates a new genre with attribute genre as nil and attribute genre_id as the next id number. – neallred Feb 12 '16 at 01:21
  • I added some more comments. The problem with find_or_create the way you are using it, is that it will create the Genre "genre" the first time, then it will find that same Genre each subsequent time and you won't be creating new ones. Or you'll just be changing the name of the old ones. – msergeant Feb 12 '16 at 01:45
  • Thank you for your help. I agree that genre is a confusing column name and have changed it. What I really want to do is select an already existing genre, and associate a new book with it. I can do this statically with "def genre @genre ||= Genre.find_by(name: :History) end" but I need to replace :History with something that reads the form input value. Any ideas? – neallred Feb 18 '16 at 19:03