Lab 4: BookManager
Due Date: February 28
Objectives
- Learn how to setup rails models with relationships, validations, and scopes
- Learn setup of rails views
- Understand how to modify and update views
- Gain a basic understanding of rails partials
Due Date: February 28
Objectives
Create a new Rails application called "BookManager", switch directories (cd
) into this Rails app from the command line. Create a git repository, add and commit the initial files with the commit message "Initial commit".
Create three models and the scaffolding from the command line using the command:
rails generate scaffold <Model> <attribute>:<data type>
Below are the details of each model:
Publisher
name (string)
Author
first_name (string)
last_name (string)
Book
title (string)
publisher (use 'references' since it is a FK to publishers)
proposal_date (date)
contract_date (date)
published_date (date)
units_sold (integer)
Create an additional model called BookAuthor
using the rails generate model
command. The attributes of this model are book:references
and author:references
.
We don't need a full set of views or a controller, just an associative entity to connect books and authors, so using a model generator is sufficient in this case.
After creating these models, migrate the database and save all this generated code to git.
Create and switch to a new branch in git called models. Open the Book
model in your editor and add the following three relationships to that model:
belongs_to :publisher # Should already exist in the file
has_many :book_authors
has_many :authors, through: :book_authors
In addition, add the following validations:
validates_presence_of :title
validates_numericality_of :units_sold
For the units_sold
, refer to the Rails API for the option that restricts the values to integers only.
Finally, add the following scope:
scope :alphabetical, -> { order('title') }
Go to the Publisher model and add the following relationship, scope, and validation:
has_many :books
scope :alphabetical, -> { order('name') }
validates_presence_of :name
Go to the Author model and add the following relationships:
has_many :book_authors
has_many :books, through: :book_authors
In addition, add the following validations, scopes and methods:
validates_presence_of :first_name, :last_name
scope :alphabetical, -> { order('last_name, first_name') }
def name
"#{last_name}, #{first_name}"
end
Go to the BookAuthor model and ensure the following relationships exist:
belongs_to :book
belongs_to :author
Once the model code is complete, start the server, be sure it is running and that there are no typos (common error) in your code by looking at the index pages of books, authors and publishers, which can be found at:
For Local Rails Installations
http://localhost:3000/books
http://localhost:3000/publishers
http://localhost:3000/authors
If it’s all good, commit these changes to your git repo and then merge these changes into the master branch. Reminder, that should look like git checkout master
to get back to the master branch and git merge models
to incorporate your most recent updates.
Before we dive into views, we are going to complete some of the missing validations in the Book model. Create and move to a new book branch with git checkout -b book
.
Since Rails does not have built-in date validations, we are using the validates_timeliness gem; find the documentation for this gem by clicking on the link and scroll down the webpage to see some example uses.
First, we need to include the gem in our project. We do this by going to the Gemfile and adding in gem 'validates_timeliness'
. Now run bundle update
to make sure you have the gem on your machine. Lastly, it is necessary to also run the rails generate validates_timeliness:install
command to have a couple of files generated for the project, so do so now to make sure that you have them.
Now that we have the validates_timeliness
gem installed, add the following validations to the following fields in the Book model.
proposal_date
is a legitimate date and that it is either the current date or some time in the past. (The reason is you shouldn't be allowed to record a proposal you haven't yet received.)Add a validation so that the contract_date
is also a legitimate date and that it is either the current date or some time in the past. (The reason is you shouldn't be allowed to record a contract you haven't yet signed.)
Also make sure that the contract_date
is some time after the proposal_date
as you can't sign contracts for books yet to be proposed.
Finally allow the contract_date
to be blank as not all books we are tracking have contracts yet.
Add a validation so that the published_date
is also a legitimate date and that it is either the current date or some time in the past.
Also make sure that the published_date
is some time after the contract_date
as you can't publish books without contracts.
Finally allow the published_date
to be blank as not all books we are tracking are published yet.
Notice that although we want to allow blank values for contract_date
and published_date
, we do not have the option to select a blank value in our book form. Go to the _form
partial in the Book view and replace the contract_date date_select with the following. Replace the published_date date_select with something similar.
<%= form.date_select :contract_date, {id: :book_contract_date, include_blank: :true, default: :nil} %>
Start (or restart) your rails server and verify that these validations work in the interface. Confirm this with a TA before continuing. Do not merge back into master
.
Show a TA that you have the basic Rails app set up and working, and that you have properly saved the code to git. Make sure the TA initials your sheet.
Now go to the web interface and add a new publisher: "Pragmatic Bookshelf". After that, go to the books section and add a new book: "Agile Web Development with Rails" which was published by Pragmatic Bookshelf in 2016 (actually is 2013 but for Rails default purposes, say 2016). Make sure to update the three date fields with dates that follow the validations for proposal_date
, contract_date
, and published_date
from Part 1! Note that you need to refer to the publisher by its id (1), rather than its name in the current interface. Thinking about this, and some other problems with the current interface, we will begin to make the interface more usable. Checkout to a new branch called 'interface'.
We'll begin by adding some more publishers directly into the database using the command line. If we think back to the SimpleQuotes lab last week, the easiest way to insert new data is by opening a new command line tab in the same directory and running rails db
. Then paste the publishers_sql and authors_sql code given so that we have multiple publishers and authors to choose from (and sharpen our db skills slightly). Note: do not add the first publisher since we have already added Pragmatic Bookshelf via the web interface; if you do you will get an error because they are already in the db with a id=1. Otherwise, you can add all of the publishers and authors after dropping/migrating the database by running rails db:drop
and rails db:migrate
.
-- SQL for authors
INSERT INTO "authors" VALUES (1, 'Sam', 'Ruby', '2015-02-09 12:00:00', '2014-02-10 12:00:00');
INSERT INTO "authors" VALUES (2, 'Dave', 'Thomas', '2015-02-09 12:00:00', '2014-02-10 12:00:00');
INSERT INTO "authors" VALUES (3, 'Hal', 'Fulton', '2015-02-09 12:00:00', '2014-02-10 12:00:00');
INSERT INTO "authors" VALUES (4, 'Robert', 'Hoekman', '2015-02-09 12:00:00', '2014-02-10 12:00:00');
INSERT INTO "authors" VALUES (5, 'David', 'Hannson', '2015-02-09 12:00:00', '2014-02-10 12:00:00');
INSERT INTO "authors" VALUES (6, 'Dante', 'Alighieri', '2015-02-09 12:00:00', '2014-02-10 12:00:00');
INSERT INTO "authors" VALUES (7, 'William', 'Shakespeare', '2015-02-09 12:00:00', '2014-02-10 12:00:00');
INSERT INTO "authors" VALUES (8, 'Jane', 'Austen', '2015-02-09 12:00:00', '2014-02-10 12:00:00');
-- SQL for publishers
INSERT INTO "publishers" VALUES (1, 'Pragmatic Bookshelf', '2015-02-09 12:00:00', '2014-02-10 12:00:00');
INSERT INTO "publishers" VALUES (2, 'Washington Square Press', '2015-02-09 12:00:00', '2014-02-10 12:00:00');
INSERT INTO "publishers" VALUES (3, 'Addison Wesley', '2015-02-09 12:00:00', '2014-02-10 12:00:00');
INSERT INTO "publishers" VALUES (4, 'Everyman Library', '2015-02-09 12:00:00', '2014-02-10 12:00:00');
INSERT INTO "publishers" VALUES (5, 'New Riders', '2015-02-09 12:00:00', '2014-02-10 12:00:00');
The first thing we will do is switch the 'publisher_id' field (a text box where you are supposed to remember and type out the appropriate publisher's id) to a drop-down list. Now that we have some publishers in the system, go to the _form
partial in the Books view and change the publisher_id text_field to the following line:
<%= form.collection_select :publisher_id, Publisher.alphabetical, :id, :name %>
Look at the new form on the web page. It's an improvement (I also like to convert the number_field for year_published to a straight text field, but not required), but it'd be a little nicer if it didn't default to the first publisher. Go to Rails API and look up collection_select
and see if there is an option that will prompt the user for input rather than just display the first record. After fixing this, I'd recommend you save this work to your git repository.
Of course, we also need to be able to select one or more authors for each book. In the _form.html.erb
template for books, add in a partial that will create the checkboxes for assigning an author to a book. Add the line
<%= render :partial => 'authors_checkboxes' %>
just prior to the submit button in the template.
Within the app/views/books
directory, create a new file called _authors_checkboxes.html.erb
and add to it the following code:
<% for author in Author.alphabetical %>
<%= check_box_tag "book[author_ids][]", author.id, @book.authors.include?(author) %>
<%= author.name %>
<br/>
<% end %>
Note: in Rails 3 and above, render
assumes by default you are rendering a partial, so you could just say render 'authors_checkboxes'
here, but I want you to put the :partial =>
in for now to reinforce the idea of partials.
If you were to try and submit the data for this form, it would reject the information for the authors (You could check this by looking in the BookAuthor table). We will talk about this later in the course. For now, add :author_ids
to the list of attributes that your controller will allow to be passed to your Book model. We can find that list in a private method called book_params
at the bottom of the BooksController
-- add :author_ids
there. Because this is an array of ids, we need to let Rails know that with the code below:
# controllers/books_controller.rb
def book_params
params.require(:book).permit(:title, :publisher_id, :proposal_date, :contract_date, :published_date, :units_sold, :author_ids => [])
end
In the show template for books, change @book.publisher
to @book.publisher.name
so that we are displaying more useful information regarding the publisher. After that, add in a partial that will create a bulleted list of authors for a particular book. To do that, add the line:
<%= render partial: 'list_of_authors' %>
after the publisher information in the template and in the code. Then add a file called _list_of_authors.html.erb
to the app/views/books directory. Within this new file add the following code:
<p>
<b><%= pluralize(@book.authors.size, 'Author') %></b><br />
<ul>
<% for author in @book.authors %>
<li><%= link_to((author.name), author_path(author)) %></li>
<% end %>
</ul>
</p>
After that, add some books to the system using the web interface. Given the authors added, there are some suggested books listed at the end of this lab, but you can do as you wish. View and edit the books to be sure that everything is worked as intended. Of course, looking at the books index page, we realize it too has issues; fix it so that the publisher's name is listed rather than the object (see previous step if you forgot how) and the books are in alphabetical order by using the alphabetical scope in the appropriate place in the books_controller. (Try it yourself, but see a TA if you are struggling on this last one for more than 5 minutes.)
BTW, have you been using git after each step? If not, time to do so...
Look at the partial list_of_authors
. There are three things to take note of:
Before having the TA sign off, you decide to test the following: go to a book in the system, uncheck all the authors, and save. It saves 'successfully', but the list of authors remains unchanged. Ouch. Good thing we are testing this app pretty carefully. How do we fix this? First, we need to realize that this happens because if no values are checked for author_ids, then rails by default just doesn't submit the parameter book[:author_ids][]
. We can force it to submit an empty array by default by adding to the book form (right after the form_with tag) the following line:
<%= hidden_field_tag "book[author_ids][]", nil%>
Once this is working (test it again to be sure), then you can merge the interface
branch in git back into the master
branch.
(Optional, but recommended if you have time left in lab) Having developed the interface for books, go back to theinterface
branch and write your own partial for show template of the authors view so that it added a list of all the books the author has written. (This is very similar to what was done for the show book functionality and those instructions/code can guide you.) Once you know it is working properly, save the code to the repo and merge back into the master branch.
Show a TA that you have completed the lab. Make sure the TA initials your sheet.
This week the "on your own" assignment is to go to RubyMonk's free Ruby Primer and complete any of the previously assigned exercises you have not yet done. If you are caught up and understand the previous exercises (repeat any you are unsure of), you may if time allows choose any of the other primer exercises and try to get ahead (we will do more of these exercises after the exam). Note: be aware that questions from RubyMonk can and will show up on the exam, so 'doing it on your own' does not mean 'doing it if you want to'.
Agile Web Development with Rails
Romeo and Juliet
King Lear
The Divine Comedy
Pride and Prejudice