I’m trying to learn Rails by creating a very simple application which just creates a website where someone can create a list of authors and books with an association that the book is written by an author. I was hoping this would be simple and DRY, but I’ve been having an unexpected amount of trouble with it.
First looking at my models, I’ve set up the association and made every data point required (author.name, book.title, and book.author). I do not want to add :author or :author_id to the attr_accessible lists because I want to use the appropriate Rails conventions.
app/models/author.rb:
class Author < ActiveRecord::Base
attr_accessible :name
validates_presence_of :name
has_many :books
end
app/models/book.rb:
class Book < ActiveRecord::Base
attr_accessible :title
validates_presence_of :title
belongs_to :author
validates_associated :author
end
The books controller and view I think is exactly from the scaffolding and very uninteresting. What is interesting is the books controller. Looking at the new method, all I did was add a collector which gets the array of author names with ids to pass to the view. (Honestly, I think I would prefer to not pass the id at all.)
app/controllers/books_controller.rb
# GET /books/new
# GET /books/new.json
def new
@book = Book.new
@authors = Author.all.collect {|a| [a.name, a.id]}
respond_to do |format|
format.html # new.html.erb
format.json { render json: @book }
end
end
Now over to the views, I used the default new.html.haml, but made changes to _form.html.haml. I added a select field using the values in @authors.
app/views/books/_form.html.haml
= form_for @book do |f|
- if @book.errors.any?
#error_explanation
%h2= "#{pluralize(@book.errors.count, "error")} prohibited this book from being saved:"
%ul
- @book.errors.full_messages.each do |msg|
%li= msg
.field
= f.label :name
= f.text_field :name
.field
= f.label :author
= f.select(:author, @authors, {:include_blank => ""})
.actions
= f.submit 'Save'
Lastly, back to my controller for the create method. I try to save the basic parameters and create an author association from the selected author.
app/controllers/books_controller.rb
# POST /books
# POST /books.json
def create
@book = Book.new(params[:book])
@book.author = Author.find_by_id(params[:author])
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
When I click ‘Save’ I get the following error:
Can't mass-assign protected attributes: author
I understand that this is because the value I selected was put in params[:book] instead of params[:author]. So I have two questions.
1) How do I fix my select statement to have it send in params[:author] instead of params[:book]?
2) Is there an even better way to do this that completely hides the id association?
I think I’ve mostly figured it out, and as I suspected, I didn’t need to change my model code at all.
I changed my controllers new method definition of @authors to only return author names with the following:
This accomplished my goal of hiding the id though it’s probably a tad slower when I need to search by name instead of id in the controller create method (below).
Next, I fixed my views to set params[:author] instead of params[:book][:author].
Finally, I changed the new methods creation of the @book:
I’m fairly happy with this. The only thing I don’t really like is that the label creates an “author_name” label instead of simply “author”.