MOVED TO CW Book Inventory Management (edited – lorraine)

Practical Rails Projects

Written by Eldon Alameda

Note – fix tables & empty spaces

Published by Apress

Chapter 3



In this chapter, we will quickly implement a complete book inventory management system using the built-in scaffolding feature of Ruby on Rails. While implementing the system, we will write Ruby on Rails integration tests that exercise the whole book inventory management system from end to end. As we work through this sprint, we will show you how to map database relationships with ActiveRecord, including one-to-many, many-to-one, and many-to-many relationships. Plus, you will learn how to implement file upload functionality with Rails. We’ll also introduce you to the Textile markup language, which can be used to author web content.

Getting the Requirements

To figure out the requirements for this sprint, we ask George what tasks he should be able to perform with the book management system. He tells us that he receives information from publishers about when new books are published and when old ones go out of print. He asks us to build a system that will allow him to update the Emporium book inventory accordingly.

George tells us that he is a big fan of Amazon, even if the giant is eating away at his profits. He likes, for example, the way Amazon is able to recommend books that are similar to what the customer has browsed and bought before, and the small details like the book cover shown next to the details of the book.

We tell George that he at least has to be able to find books in the system, and to view and edit the book details. George confirms that he indeed needs those features. We take a short break, to let George have a coffee. This far into the discussion we have identified the following user stories:

  1. Add book: George, the administrator of Emporium, must be able to add new books to th e inventory. 
     
  2. Upload book cover: The administrator must be able to upload an image of the book cover. This image will be shown to customers. 
     
  3. List books: The administrator must be able to list all of the books that are currently available in the inventory. 
     
  4. View book: The administrator must be able to view the details of a book. 
     
  5. Edit book: The administrator must be able to edit the details of a book. 
     
  6. Delete book: The administrator must be able to delete books from the inventory.

We aren’t sure how we should implement the book recommendation feature George wanted, so we decide to postpone that for a later sprint (covered in Chapter 7).

When George comes back, he tells us that he must also be able to keep track of the book publishers. After a brief brainstorming session, we come up with a list of user stories related to publisher management:

  1. Add publisher: The administrator must be able to add publishers to the system. 
     
  2. List publishers: The administrator must be able to view a list of all publishers in the system. 
     
  3. View publisher: The administrator must be able to view the details of a publisher. 
     
  4. Edit publisher: The administrator must be able to edit the details of a publisher. 
     
  5. Delete publisher: The administrator must be able to remove publishers from the system.

We will also implement these in this sprint, as we are confident that we can finish them within the schedule.

Using Scaffolding

In this chapter, we’ll show you how to use a built-in Rails feature called scaffolding to jump-start the implementation of the publisher administration and book administration user interfaces.

You can use scaffolding to generate a complete CRUD implementation for objects stored in a database. Scaffolding can generate code for all three MVC layers: the model, view, and controller. Scaffolding comes in two flavors:

  1. Static scaffolding creates the code physically on disk. Static scaffolding is suited for generating boilerplate code, which you can modify later to fit your requirements.
  2. Dynamic scaffolding creates the code in memory only. It does the same as static scaffolding but at runtime, and no files are generated. This is suited only for simple functionality, such as an interface used only by administrators for editing a list of options shown to users.

The scaffolding script accepts a set of parameters that tells it what to generate. You can specify the model, view, controller, and actions as parameters to the scaffold script:

script/generate scaffold ModelName ControllerName action1 action2

When invoked, the scaffolding script connects to the database and inspects the table structure for the specified model. It then creates the controller, actions, and views necessary for creating, viewing, editing, and deleting the model you specified.

Dynamic scaffolding is done by adding a call to the scaffold method to a controller:

class JebusController < ActionController::Base
 
scaffold :jebu s
end

Scaffolding can’t generate code that fits your requirements perfectly, so we’ll also show you how to modify and extend the generated code.


Tip  Scaffolding provides examples of Rails best practices and coding conventions. It’s a good idea to have a closer look at the code that is generated with scaffolding, even if you don’t plan on using scaffolding.


Implementing the Publisher Administration Interface

We will start by implementing the administrator interface for maintaining the list of publishers. We need a table for storing publishers, so the first thing we need to do is to update the database schema by adding the publishers table to the database schema.

Updating the Schema with the Publishers Table

As in the previous chapter, we will use ActiveRecord migrations to make the necessary modifications to the database schema. We could also use plain SQL, but migrations have the added benefit of being database-agnostic and allowing you to roll back changes.

First, create the create_publishers migration file, which you will use for adding the publishers table to the database schema:

$ script/generate migration create_publishers

      exists  db/migrate
      create db/migrate/002_create_publishers.rb

Open db/migrate/002_create_publishers.rb in your editor and change it as follows:

class CreatePublishers < ActiveRecord::Migration
  def self.up
    create_table :publishers do |table|
      table.column :name, :string, :limit => 255, :null => false, :unique => true
    end
  end

  def self.down
    drop_table :publishers
  end
end

The migration will create a table named publishers when run, as the following sample output shows:

$ rake db:migrate

——————————————–(in /home/george/projects/emporium)
== CreatePublishers: migrating ================================================ — create_table(:publishers)
   -> 0.2030s
== CreatePublishers: migrated (0.2030s) =======================================

——————————————–

The new table has two columns: id and name . Note that the id column is automatically added by ActiveRecord migrations, so we need to add only the name column to the migration script. We limit the name column’s length to a maximum length of 255 characters. We also specify that we don’t accept null values in the name field and that the name must be unique.

Following good practices, we undo all changes in the down method by telling ActiveRecord to delete the publishers table.

Generating Publisher Code with the Scaffolding Script

With the database table in place, you can use the scaffolding script to create an almost complete CRUD implementation for the publisher administration, unlike in Chapter 2 where all code was created by hand. Execute the scaffolding script as follows:

$ script/generate scaffold Publisher ‘admin/publisher’

existsapp/controllers/admin
existsapp/helpers/admin
createapp/views/admin/publisher
existstest/functional/admin
dependencymodel
existsapp/models/
existstest/unit/
existstest/fixtures/
createapp/models/publisher.rb
createtest/unit/publisher_test.rb
createtest/fixtures/publishers.yml
createapp/views/admin/publisher/_form.rhtml
createapp/views/admin/publisher/list.rhtml
createapp/views/admin/publisher/show.rhtml
createapp/views/admin/publisher/new.rhtml
createapp/views/admin/publisher/edit.rhtml
createapp/controllers/admin/publisher_controller.rb
createtest/functional/admin/publisher_controller_test.rb
createapp/helpers/admin/publisher_helper.rb
createapp/views/layouts/publisher.rhtml
identicalpublic/stylesheets/scaffold.css

 

 

 

 

 

 

 

 

 

 

 

 

 

The scaffolding script creates the model, controller, and views required for doing CRUD operations on the publishers table. Furthermore, the scaffolding creates an empty unit test for the model, along with a fixture

The scaffolding script creates the model, controller, and views required for doing CRUD operations on the publishers table. Furthermore, the scaffolding creates an empty unit test for the model, along with a fixture file and a functional test.

Next, we demonstrate the wonders of scaffolding to George. He says: “Damned consultants! I’m gonna go blind if I have to look at that page for more than ten seconds! Is this all you can do?” We understand his point, and show him the new site design we have quietly been working on, to which he responds, “It’s not going to win any design awards. But, it will do until I can find the money to hire a professional web designer.”


Note  To get the new layout, download the source code for this book from the Apress website ( www.apress.com ), and copy the layout file ( application.rhtml ) and style sheet file ( style.css ) to your project directory.


Next, start WEBrick, if it isn’t running already, by executing script/server in the root directory of the application. Open http://localhost:3000/admin/publisher in your browser. You should see the user interface for listing publishers. We’ll do a small test just to verify that the generated code works. Click the New publisher link, enter the name Apress in the Name field, and then click Create. You should now see a success message telling you that the publisher was created successfully. You should also see a new row in the list of publishers, as shown in Figure 3-1.


Figure 3-1.  The publisher list page after adding a publisher

Scaffolding creates some files that we don’t need, including a style sheet and a layout file. Delete the public/stylesheets/scaffold.css file, as we already have a style sheet for Emporium. Also delete app/views/layouts/publisher.rhtml , as we want to use the same layout for all controllers.

Now is a good time to start writing some tests. But wait, didn’t the scaffolding script just create a functional test for us? Let’s execute the test with rake test:functionals . There are no errors and all tests pass. You could assume that you do not have to write any tests, but that assumption is wrong.


Note  Because we are using scaffolding, we won’t be following TDD very strictly in this chapter.


Completing the Add Publisher User Story

The scaffolding script has created a functional test in test/functional/admin/publisher_controller_test.rb. On closer inspection, we can see that it requires some modifications for it to be helpful in our efforts at producing bug-free code. For example, the test_create method doesn’t specify a name for the publisher it creates. This should have made the test fail when we ran the test, but it didn’t. We will fix that soon, but the first thing we will do is add validations. You never know what kind of data a user will try to enter into your application. As explained in the previous chapter, validations help ensure that only valid data is inserted into the database.

Adding Validations to the Model

Adding validations will make the test fail and show you where the parameters should be specified. So let’s begin by adding a validation for the name field in the Publisher model. Open app/models/publisher.rb and modify it to look as follows:

class Publisher < ActiveRecord::Base
 
validates_length_of :name, :in => 2..255
 
validates_uniqueness_of :name
end

As you might remember, we specified the maximum length of the name field in the publishers table to be 255 characters. We add a validation to the model to verify this constraint and specify that the minimum length of the name field to be 2 characters. We also add a validation that checks that the publisher name is unique.

Modifying the Generated Fixture Data

The fixture data generated by scaffolding is not very descriptive of our project. We can do better. Open test/fixtures/publishers.yml in your editor and remove everything from the file. Then add the following:

apress:
 
id: 1
 
name: Apress

Again, execute the functional tests with rake test:functionals . You should see the tests fail with the following error message:

——————————————–
Expected response to be a <:redirect>, but was <200>
——————————————–

The test fails as expected because the test_create method doesn’t provide any parameters to the post method that is supposed to create the publisher. To fix this, we’ll change the functional test.

Modifying the Generated Functional Test

Open test/functional/admin/publisher_controller_test.rb in your editor and change the test_create method as follows:

  def test_create
    num_publishers = Publisher.count

    post :create, :publisher => {:name => ‘The Monopoly Publishing Company’}

    assert_response :redirect
   
assert_redirected_to :action => ‘list’

    assert_equal num_publishers + 1, Publisher.count
  end

The only change is that we pass a hash to the post method, instead of no data at all. The hash contains the name for the publisher the test should create.

Run the tests again, and you should see the functional test pass. You should also do a quick test in the browser to verify that the Add Publisher user story functionality works. Open http://localhost:3000/admin/publisher/new in your browser. Test the validations you just added by clicking the Create button, without specifying a name. You should see the error message shown in Figure 3-2. This error message is automatically generated by Rails, and explains exactly what you should do in order to fix the error.


Figure 3-2.  Testing the Add Publisher user story

We further examine the functional tests and see that those for the List Publishers and Delete Publisher user stories are satisfactory. However, the test for the View Publisher and Edit Publisher user stories require some work.

Completing the View Publisher User Story

We are satisfied with the functional test that scaffolding created for the View Publisher user story, except for one detail. It doesn’t verify that the view really is showing the details of the publisher. This will be easy to fix, but first have a look at the view.

Modifying the View

Open app/views/admin/publisher/show.rhtml in your editor, and you can see that it is not using the same style as the View Author user story we created in the previous chapter. Change the view as follows, so that it uses the same style:

<dl>
 
<dt>Name</dt>
 
<dd><%= @publisher.name %></dd>
</dl>

<%= link_to ‘Edit’, :action => ‘edit’, :id => @publisher %> |
<%= link_to ‘Back’, :action => ‘list’ %>

Modifying the Generated Action

Recall that we modified the layout file ( application.rhtml ) in the previous chapter to display the page title, if it is made available to the view. Currently, this is not the case for the show publisher page, as can be verified by viewing the details of a publisher. Fix this by opening app/controllers/admin/publisher_controller.rb and changing the show method as follows:

  def show
    @publisher = Publisher.find(params[:id])
    @page_title = @publisher.name
  end

This allows the view to access the instance variable @page_title and print out the value.

Modifying the Generated Functional Test

Next, modify the generated test so that it verifies that the page is rendered correctly. Open test/functional/admin/publisher_controller_test.rb and change it as follows:

  def test_show
    get :show, :id => 1

    assert_response :success
   
assert_template ‘show’

    assert_not_nil assigns(:publisher)
    assert assigns(:publisher).valid?

    assert_tag "h1", :content => Publisher.find(1).name
  end

Note that we added an assert_tag assertion to the end of the test. This assertion is used to verify that the main heading on the page is showing the publisher’s name.

Run the functional tests again, and you should see all tests pass without errors. Access http://localhost:3000/admin/publisher/list and click the Show link next to the publisher you just created. You should see a page that looks like Figure 3-3.


Figure 3-3.  Testing the View Publisher user story

Completing the Edit Publisher User Story

The scaffold implementation of the Edit Publisher user story’s test suffers from the same problem as the Add Publisher implementation. The test doesn’t provide any parameters to the action other than the id of the publisher. This means that no data is updated when the test is run, but in this case, there is no error. This is okay, because Rails uses update_attributes to update only attributes that are included as request parameters.

We do want to verify that the editing is successful, so open test/functional/admin/ publisher_controller_test.rb and change the test_update method as shown in the following code snippet:

  def test_update
   
post :update, :id => 1, :publisher => { :name => ‘Apress.com’ }
    assert_response :redirect
   
assert_redirected_to :action => ‘show’, :id => 1
   
assert_equal ‘Apress.com’, Publisher.find(1).name
 
end

Note that we have added a new parameter to the post method call. This will update the name of the publisher to Apress.com in the database. At the end of the test, we verify that this really is the case with assert_equal . Execute the functional tests again, with rake test:functionals . Access http://localhost:3000/admin/publisher/list and click the Edit link next to the publisher you created. You should see a page that looks like Figure 3-4.


Figure 3-4.  Testing the Edit Publisher user story

You now have a functioning system for maintaining the publishers. We call George over and show him the user interface. He does a quick acceptance test and tells us that he has no complaints, so let’s continue implementing the user stories for book management.

Implementing the Book Administration Interface

Now we implement the administrator interface for managing books. For this, we need to first create a table for storing books in the database, and then create the ActiveRecord model for books. Although we found some issues with scaffolding while implementing the publisher administration functionality, it saved us some time and George was happy, so we’ll continue using scaffolding to implement the book management administration functionality.

Updating the Schema with the Books Table

The first thing we need to do is to create a table for storing books in the database. Following the Rails naming conventions, we name the table books, and as usual, we’ll update the database schema using ActiveRecord migrations.

As a recap, the Emporium database schema already has the authors table we created in the previous chapter and the publishers table we created earlier in this chapter. Now, we’ll add the books table, as well as the books_authors table, which will be used to link authors to publishers .

George is still with us, at least physically. He asks us why we have to use so many tables. “You consultants, you always want to build these fancy systems. I could do this with just one Excel sheet!” We don’t know if he’s kidding or not, but we’ll try to get George to understand when we show him a picture and explain how mapping works to link the tables and get the data we want. We’ll do that after we add the new tables and create the Book model.

To start, create the migration file for adding the books table to the database schema:

$ script/generate migration create_books_and_authors_books

——————————————–
  exists  db/migrate
  create db/migrate/003_create_books_and_authors_books.rb
——————————————–

Open db/migrate/003_create_books_and_authors_books.rb in your editor and add the code in Listing 3-1 to it.

Listing 3-1. ActiveRecord Migration for the books and authors_books Tables and Foreign Keys

class CreateBooksAndAuthorsBooks < ActiveRecord::Migration
  def self.up
   
create_table :books do |table|
      table.column :title, :string, :limit => 255, :null => false
      table.column :publisher_id, :integer, :null => false
      table.column :published_at, :datetime
      table.column :isbn, :string, :limit => 13, :unique => true
      table.column :blurb, :text
      table.column :page_count, :integer
      table.column :price, :float
      table.column :created_at, :timestamp
      table.column :updated_at, :timestamp
    end

    create_table :authors_books, :id => false do |table|
      table.column :author_id, :integer, :null => false
      table.column :book_id, :integer, :null => false
   
end

    say_with_time ‘Adding foreign keys’ do
      # Add foreign key reference to books_authors table
      execute ‘ALTER TABLE authors_books ADD CONSTRAINT fk_bk_authors ➥
FOREIGN KEY ( author_id ) REFERENCES authors( id ) ON DELETE CASCADE’
      execute ‘ALTER TABLE authors_books ADD CONSTRAINT fk_bk_books ➥
FOREIGN KEY ( book_id ) REFERENCES books( id ) ON DELETE CASCADE’
     
# Add foreign key reference to publishers table
      execute ‘ALTER TABLE books ADD CONSTRAINT fk_books_publishers ➥
FOREIGN KEY ( publisher_id ) REFERENCES publishers( id ) ON DELETE CASCADE’
    end
  end

  def self.dow n
    drop_table :authors_books
    drop_table :books
 
end
end


Note  This migration in Listing 3-1 uses MySQL-specific SQL. This means that you would need to change the code in order to run it on other databases.


The migration file creates two new tables, books and authors_books ( authors_books in the join table, as explained in the “Many-to-Many Relationship” section later in this chapter). To ensure data integrity, we also add foreign key constraints to the tables. ActiveRecord doesn’t support adding foreign keys constraints to tables. You need to add foreign keys using the ALTER TABLE SQL command and the ActiveRecord execute method, which can execute raw SQL on the database. The say_with_time method is used to print out the time it takes to execute the commands that add foreign keys to the database schema. Also note that ISBN numbers must be unique and that this is ensured by setting the :unique option to true .


Tip  ActiveRecord has a built-in time stamping behavior that is triggered for database columns named created_at / created_on and updated_at / updated_on . When ActiveRecord finds one of these columns in a database schema, it will automatically set the creation timestamp when a new object is created and the modification time when the object is updated. ActiveRecord also has other behaviors that are triggered for other column names. For example, the lock_version column name enables optimistic locking.


You are now ready to upgrade the Emporium database to its third version. Execute the migration script with the rake db:migrate command:

$ rake db:migrate

——————————————–
(in /home/george/projects/emporium)
== CreateBooksAndAuthorsBooks: migrating ======================================
— create_table(:books)
  
-> 0.1410s
— create_table(:authors_books, {:id=>false})
  
-> 0.1400s
— Adding foreign keys
— execute("ALTER TABLE authors_books ADD CONSTRAINT fk_bk_authors ➥
FOREIGN KEY ( author_id ) REFERENCES authors( id ) ON
 
DELETE CASCADE")
   -> 0.3440s
— execute("ALTER TABLE authors_books ADD CONSTRAINT fk_bk_books ➥
FOREIGN KEY ( book_id ) REFERENCES books( id ) ON DELETE CASCADE")
  
-> 0.3280s
— execute("ALTER TABLE books ADD CONSTRAINT fk_books_publishers ➥
FOREIGN KEY ( publisher_id ) REFERENCES publishers( id
) ON DELETE CASCADE")
  
-> 0.3440s
   -> 1.0160s
== CreateBooksAndAuthorsBooks: migrated (1.2970s) =============================
——————————————–

You should see all commands run without any errors. If you connect to MySQL with the command-line client, you can see the two new tables that were created by the migration:

$ mysql -uemporium -phacked

——————————————–
Welcome to the MySQL monitor. Commands end with ; or g.
Your MySQL connection id is 1 to server version: 5.0.20-community

Type ‘help;’ or ‘h’ for help. Type ‘c’ to clear the buffer.

mysql> use emporium_development;
Database changed
mysql> show tables;  

 

Tables_in_emporium_development

 

authors

 

authors_books

 

books

 

publishers

 

schema_info

 

 

5 rows in set (0.08 sec)
——————————————–

The schema_info table is where ActiveRecord stores the current version of the database schema, as explained in the previous chapter. Running select * from schema_info; should print 3 , which is the current version of our database schema.


Tip  If you want to go back to a previous version of your database model, just specify the version number as a parameter to the migrate script, as in rake migrate VERSION=0 .


Creating the Book Model

With the database in place, you can now create the ActiveRecord model for books. Following the ActiveRecord naming conventions, we name it Book, and create it using the script/generate command.

$ script/generate model Book –skip-migration

——————————————–
  exists  app/models/
  exists  test/unit/
  exists  test/fixtures/
  create  app/models/book.rb
  create  test/unit/book_test.rb
  create  test/fixtures/books.yml
——————————————–


Note  We skipped the creation of the migration file because we already created it in the previous section. Normally, you would create the migration and the model at the same time with the command script/generate model Book , but in this case, we wanted a more descriptive name for the migration file.


The script creates the model, plus a unit test and fixture that you can use in your tests.

ActiveRecord Mapping

In the previous chapter, you created the table used for storing authors, and in this chapter, you have created three more: publishers, books, and authors_books. Figure 3-5 shows the entity relationship diagram (ERD) for the Emporium database, which we show to George to hopefully help him see how these tables work better than an Excel spreadsheet. The ERD shows the different relationships between the tables in the database (the 1 indicates the one record part of the relationship and the * represents the many records part), which contain one-to-many, many-to-one, and many-to-many relationships. Before we modify the generated models, let’s take a brief look at how to set up these relationships with ActiveRecord.


Figure 3-5.  Entity relationship diagram showing the current Emporium database

One-to-Many Relationship

A one-to-many relationship is required when you have one record that owns a set of related records. In Emporium, we have a one-to-many relationship between the publishers and books tables (see Figure 3-5), because a publisher can have one or more books.

With ActiveRecord, one-to-many relationships are implemented using the has_many mapping. Adding a has_many :books declaration to the Publisher model will inject the methods listed in Table 3-1 into the Publisher model.


Note  We list only part of the ActiveRecord API for associations. For a full list of methods, see the Ruby on Rails documentation at http://rubyonrails.org/api/classes/ActiveRecord/ Associations/ClassMethods.html .  


Table 3-1. Some Methods Introduced by has_many Mapping

Method

Description

publisher.books

Returns an array containing all books associated

 

with the publisher. An empty array is returned if no books belong to the publisher.

publisher.books << Book.create(…)

Adds a book to the publisher and sets up the

 

necessary foreign keys.

publisher.books.delete(some_book)

Deletes a book from a publisher’s collection

 

of books.

publisher.books = new_books

Replaces the publisher’s collection of books.

publisher.books_singular_ids=[1,2,3,4]

Replaces the publisher’s books collection with a

 

new collection containing the books having the ids 1, 2, 3, and 4.

Some Methods Introduced by has_many Mapping (continued)
 
Method Description
publisher.books.clear Removes the books from the publisher’s collection. The behavior can be configured so that the books are deleted, instead of just removed from the publisher. See the Ruby on Rails documentation on the :dependent parameter for more information.
publisher.books.empty? Returns true if the books collection is empty.
publisher.books.size Returns the number of books associated with the publisher.
publisher.books.find Finds an associated object according to the same rules as ActiveRecord::Base.find :
http://api.rubyonrails.org/classes/ ActiveRecord/Base.html#M000860 .

 

 

 

 

 

 

 

 

 

 

 

  

Many-to-One Relationship

In the previous section, we showed you how to map the Publisher model’s side of the publisher-book relationship by using a one-to-many relationship. Looking from the Book model’s perspective, this is a many-to-one relationship, since a particular book can have only one publisher.

Many-to-one relationships are implemented using the belongs_to ActiveRecord mapping. The belongs_to :publisher declaration injects the methods listed in Table 3-2 into the Book model. This gives you access to methods such as book.publisher.nil? .

Table 3-2. Some Methods Introduced by belongs_to Mapping  

Method

Description

book.publisher

Returns the publisher object or nil.

book.publisher = new_publisher

Sets the book publisher to the specified publisher and sets

 

up the required link between the database tables.

book.publisher.nil?

Returns true if the book’s publisher has not been set.

Many-to-Many Relationship

A many-to-many relationship is used when you have two tables that both contain a set of records that can refer to another set of records in the other table. In our case, authors can be associated with one or more books, and books can be authored by one or more authors, so a many-to-many relationship exists between the authors and books tables. Many-to-many relationships are more complex than one-to-one relationships, as they involve one extra table, referred to as the join table. The join table is used for setting up a link between the authors and books tables.

Many-to-many relationships are set up in ActiveRecord using the has_and_belongs_to_many mapping, also referred to as habtm . Adding a has_and_belongs_to_many :books declaration to the Author model will inject the methods listed in Table 3-3.

Table 3-3. Some Methods Introduced by has_and_belongs_to_many Mapping

Method Description
author.books Returns an array of books belonging to this author or an empty array if none have been associated.
author.books << Book.create(…) Adds a book to the author’s collection of books and sets up the necessary link in the database by inserting a record in the authors_books join table.
author.books.delete(some_book) Removes a book from the author’s collection of books. Also removes the corresponding record from the authors_books join table.
author.books = new_books Replaces the collection of books with a new one.
author.books_singular_ids=[1,2,3,4] Replaces the author’s collection of books with the books having the specified id s.
author.books.clear Removes all books from the author’s collection and
the corresponding row in the authors_books join table.
author.books.empty? Returns true if the collection of books is empty.
author.books.size Returns the number of books.
author.books.find(id) Finds the book that is in the author’s collection of books and has the specified id .

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

One-to-One Relationship

A one-to-one relationship is useful, for example, when you have a master entity that consists of two logically separate entities. For example, a customer can have both a shipping address and billing address. If you don’t want to store all of the information in one table, you could put the addresses into separate tables and use a one-to-one relationship to map them to the customer. One-to-one relationships are not used in the Emporium database at the moment.


Tip  Refer to Wikipedia’s entry on database normalization, http://en.wikipedia.org/wiki/ Database_normalization , for more information about how to organize data in a relational database and when to split entities into different tables.


ActiveRecord uses the has_one mapping to implement one-to-one relationships. Adding a has_one :address declaration to the Author model would inject the methods listed in Table 3-4 into the model.

Table 3-4. Some Methods Introduced by has_one Mapping  

Method

Description

author.address

Returns the author’s address object or nil.

author.address = new_address

Sets the authors address to the specified new address.

author.address.nil?

Returns true if the address object hasn’t been set.

Modifying the Generated Models

Now that you understand both the database schema and the way ActiveRecord maps to the schema, let’s modify the generated models.

Adding the has_many Mapping to the Publisher Model

As we just explained, the one-to-many relationship between the Publisher and Book model is set up in ActiveRecord by adding the has_many declaration to app/models/publisher.rb , as highlighted in the following code snippet:

class Publisher < ActiveRecord::Base
 
has_many :books

  validates_length_of :name, :in => 2..255
  validates_uniqueness_of :name
end

This gives you access to, for example, the books.empty? method:

Publisher.find_by_name(‘Apress’).books.empty?

In case you are wondering, find_by_name is a dynamic finder, which dynamically (at run-time) creates a SQL query that returns the Apress publisher, or nil if the publisher is not found.


DYNAMIC FINDERS

Dynamic finders are features of ActiveRecord that allow you to use the ActiveRecord API instead of SQL to find
objects. Dynamic finders use the find_by format and are created by ActiveRecord on the fly at runtime when
calling the following, for example:

Publisher.find_by_name("Apress")

As another example, you could make this call:

book.find_all_by_title_and_page_count(‘Drinking Tequila for Dummies’, 538)

This dynamically creates a SQL query that finds all books having both the specified title and page count.
Dynamic finders can also create a new record if the query returns no results. This is useful for implementing
one-liners like this:

Publisher.find_or_create_by_name(‘Apress’)

This example creates the publisher Apress (if it doesn’t already exist) and then returns it.


Adding the belongs_to Mapping to the Book Model

As you learned in the previous section about ActiveRecord mappings, the many-to-one relationship between the Book and Publisher model is set up with ActiveRecord by using belongs_to . Change app/models/book.rb as shown here:

class Book < ActiveRecord::Base
  
belongs_to :publisher
end

The belongs_to allows you to access, for example, the name of the publisher from the Book model:

Book.find_by_title(‘Elvis Peanut Butter Sandwich Recipes 5th Edition’).publisher.name

Adding the habtm Mapping to the Book and Author Models

Next, for the many-to-many mapping between the authors and books, add the has_and_belongs_to_many mapping to app/models/book.rb as follows:

class Book < ActiveRecord::Base
 
has_and_belongs_to_many :authors
 
belongs_to :publisher
end


Note  ActiveRecord tries to guess the name of the join table, authors_books , by combining the two table names. In our example, ActiveRecord will look for a table named authors_books , not books_authors , since the string authors comes before books when compared in lexical order.


We also want to be able to access the books from the author’s side of the relationship, so change app/models/author.rb as follows:

class Author < ActiveRecord::Base
 
has_and_belongs_to_many :books

  validates_presence_of :first_name, :last_name

  def name
    "#{first_name} #{last_name}"
  end
end

That takes care of the ActiveRecord mappings, but we also want to make sure only valid books are stored in the database. This can be done with validations, which we introduced in Chapter 2.

Adding Validations to the Book Model

Before writing unit tests for the model, you should add some validations to the model. The Book model has quite a few attributes that should be validated, as listed in Table 3-5.

Table 3-5. Validations on the Book Model  

Field

Description

title

The title should be at least 1 character long and have a maximum of 255

 

characters. This validation is done by adding validates_length_of :title,

 

:in => 1..255to the model.

publisher

A publisher should be assigned to the book. This validation is done by adding validates_presence_of :publisherto the model.

authors

A book should have at least one author. This validation is done by adding validates_presence_of :authorsto the model.

published_at

The published date should be specified. This validation is done by adding validates_presence_of :published_at to the model.

isbn

The ISBN number should be in the correct format. This validation is done by

 

adding validates_format_of :isbn, :with => /[0-9-xX]{13}/to the model. The validation uses a regular expression, which checks that there are 13 characters in the ISBN. Note that this is not a complete validation of an ISBN number, but sufficient for our requirements.

page_count

The page count should be an integer. This validation is done by adding validates_numericality_of :page_count, :only_integer => trueto the model.

price

The price should be a number. This validation is done by adding validates_numericality_of :priceto the model.

Next, add each of these validations to the book model, app/models/book.rb , as shown here:

class Book < ActiveRecord::Base 
  has_and_belongs_to_many :authors
  belongs_to :publisher

  validates_length_of :title, :in => 1..255
  validates_presence_of :publisher
  validates_presence_of :authors
  validates_presence_of :published_at 
  validates_numericality_of
:page_count, :only_integer => true
  validates_numericality_of :price
  validates_format_of :isbn, :with => /[0-9-xX]{13}/
  validates_uniqueness_of :isbn

end

Cloning the Database

There’s one important step left to do before we start writing unit tests: we need to clone the development database to the test environment. Your unit tests will use the test database, emporium_test, but it hasn’t been updated to the latest version. The easiest way of cloning the database structure from the development to the test database is by executing the following command:

rake db:test:clone_structure


Note  An alternative way of updating the test database is to recreate the database from scratch using migrations, by executing the rake command without specifying any parameters. This first runs all the migrations, and then executes the tests in the test directory.


This is a built-in task that copies the database schema from the emporium_development to the emporium_test database. If you skip this step, you’ll get the following error when running the unit test:

——————————————–
ActiveRecord::StatementInvalid: Mysql::Error: Table ’emporium_test.books’ doesn’t
exist: DELETE FROM books
——————————————–

Unit Testing Validations

You want to be absolutely sure that the validations are working. One way of doing this is to create a unit test that tests that all fields are validated correctly.

Scaffolding already created a unit test for you, but it contains only a dummy test, so replace the code in test/unit/book_test.rb with the following code:

require File.dirname(__FILE__) + ‘/../test_helper’

class BookTest < Test::Unit::TestCase
  def test_failing_create
   
book = Book.new
     
assert_equal false, book.save

      assert_equal 7, book.errors.size
     
assert book.errors.on(:title)
     
assert book.errors.on(:publisher)
      assert book.errors.on(:authors)
     
assert book.errors.on(:published_at)
      assert book.errors.on(:isbn)
     
assert book.errors.on(:page_count)
      assert book.errors.on(:price)
 
end
end

The unit test creates a new book without specifying any values for any of the validated fields, such as the price. It then tries to save the book to the database. This triggers a validation of each of the fields to which you added validations. When you run the test, there is a validation failure on each of the fields—eight in total. We test for this condition with the assert_equal method. We also check that validations are done on the correct fields, by calling book.errors.on on each validated field. This method returns the actual error message for the specified field, and if it returns nil, we know the validation is not working.

Run the unit test, and you should see it pass without errors:

$ ruby test/unit/book_test.rb

——————————————–
Loaded suite test/unit/book_test
Started
.
Finished in 0.172 seconds.

1 tests, 9 assertions, 0 failures, 0 errors
——————————————–

You should also test that a valid book can be saved to the database, by adding the test_create method to the unit test, as follows:

  fixtures :authors, :publishers

  def test_create
   
book = Book.new(
      :title => ‘Ruby for Toddlers’,
      :publisher_id => Publisher.find(1).id,
      :published_at => Time.now,
      :authors => Author.find(:all),
      :isbn => ‘123-123-123-1’,
      :blurb => ‘The best book since "Bodo Bär zu Hause"’,
      :page_count => 12,
      :price => 40.4
    )

    assert book.save
  end

This test looks up an author and a publisher from the database. These are inserted by the publisher and author fixtures, which have also been added. The test creates a new book with valid parameters, including a publisher and author, and then validates that the book was saved successfully. Recall that calling save on the book object returns true if there are no validation errors.

Unit Testing the ActiveRecord Mappings

You want to be sure that the mapping between authors, books, and publishers works. George won’t be happy at all, if there’s a bug in the code that prevents him from adding the latest best-sellers to the catalog.

Adding Fixtures for Books and Publishers

We’ll verify that the mapping works by creating unit tests for the mapping code. But let’s first add some useful data to the books and publishers fixture files, which we’ll use in later tests.

Open test/fixtures/books.yml and add the following two books:

pro_rails_ecommerce:
  id: 1
  title: Pro Rails E-Commerce
  publisher_id: 1
  isbn: 199-199-199-1
  published_at: <%= Time.now.strftime("%Y-%m-%d") %>
pro_rails_ecommerce_2:
  id: 2
  title: Pro Rails E-Commerce 2nd Edition
  publisher_id: 1
  isbn: 199-199-199-2
  published_at: <%= Time.now.strftime("%Y-%m-%d") %>

Note that the publisher_id column has been added to the fixture. This is a reference to a row in the database, which is inserted by the publishers.yml fixture file. Currently, no publisher has an id equal to 1, so you’ll need to add the data to the publishers fixture file to complete the mapping.


Tip  You can write ERB in fixtures in the same way as in views. This allows you to create dynamic fixtures, as demonstrated in the books.yml fixture file, where Time.now is used to generate the published_at value. Although dynamic fixtures are useful in some situations, they should generally be avoided as they make tests more complex and less predictable.


Next, add a publisher to the test/fixtures/ publishers.yml file:

apress:
 
id: 1
 
name: Apress
emporium:
 
id: 2
 
name: Emporium

Recall that you specify the fixtures that the unit test should load by adding a fixtures declaration. A couple of tests that we will implement later in this chapter depend on the authors , publishers , and books fixtures. The books fixture has not been added to the test yet, so change the fixtures line in the unit test as follows:

fixtures :authors, :publishers, :books

Unit Testing the One-to-Many Mapping

Now we’ll put the data into use and verify that we can access a collection of books from a publisher. This is done by adding the new test, test_has_many_and_belongs_to_mapping , to the test/unit/book_test.rb unit test:

  def test_has_many_and_belongs_to_mapping
    apress = Publisher.find_by_name("Apress")
    assert_equal 2, apress.books.size

    book = Book.new(
      :title => ‘Rails E-Commerce 3nd Edition’,
      :authors => [Author.find_by_first_name_and_last_name(‘Christian’, ‘Hellsten’),
      
Author.find_by_first_name_and_last_name(‘Jarkko’, ‘Laine’)],
      :published_at => Time.now,
      :isbn => ‘123-123-123-x’,
      :blurb => ‘E-Commerce on Rails’,
      :page_count => 300,
      :price => 30.5
   
)

    apress.books << book

    apress.reload
    book.reload

    assert_equal 3, apress.books.size
    assert_equal ‘Apress’, book.publisher.name
  end


Note  The unit test doesn’t call book.save explicitly. ActiveRecord is smart enough to know that it must persist the book to the database when the book is added to the author’s collection of books. Also note that you could use assert_difference (introduced in the previous chapter), instead of two calls to assert_equal .


The unit test performs the following tasks in order:

  1. Look up a publisher and verify that there are two books in the books collection. These two books are inserted by the fixture at the start of the test.
  2. Create a new book and associate two authors with it. 
     
  3. Add the new book to the publisher’s collection of books. 
     
  4. Reload the book and publisher data from the database. 
     
  5. Verify that the publisher has three books, instead of the original count of two. 
     
  6. Verify that the publisher’s name is the one we assigned.

Note  The order the fixtures are listed in is important. The fixture data is inserted in the order it is listed. For example, putting the publishers fixture after the books fixture would result in a foreign key error when the test is run and Rails tries to insert the fixture data: ActiveRecord::StatementInvalid: Mysql::Error: Cannot add or update a child row: a foreign key constraint fails .


Next, run the unit tests. You should see all tests pass without any errors.

$ ruby test/unit/book_test.rb

——————————————–
Loaded suite test/unit/book_test
Started

Finished in 0.359 seconds.

3 tests, 13 assertions, 0 failures, 0 errors ——————————————–

To see the SQL that is executed by ActiveRecord behind the scenes, tail the logs/test.log file by executing the following command in a separate console window:

$ tail -f logs/test.log

This will monitor the log for changes and print them out to the screen.

Run the unit tests again. You should see the following output from the test_has_many_ and_belongs_to_mapping test that you just implemented:

——————————————–SQL (0.000000) BEGIN 
  Publisher Columns (0.000000)  SHOW FIELDS FROM publishers
Publisher Load (0.016000)  SELECT * FROM publishers WHERE (publishers.`name` = ‘
  Book Columns (0.000000)   SHOW FIELDS FROM books
SQL (0.000000)  SELECT count(*) AS count_all FROM books WHERE (books.publisher_i
  Author Columns (0.015000)   SHOW FIELDS FROM authors
Author Load (0.000000)  SELECT * FROM authors WHERE (authors.`first_name` = ‘Joe
  Author Load (0.000000)   SELECT * FROM authors WHERE (authors.`first_name` = ‘
Book Load (0.000000)  SELECT * FROM books WHERE (books.publisher_id = 1)
  SQL (0.000000)   INSERT INTO books (`isbn`, `updated_at`, `page_count`, `price
authors_books Columns (0.016000)  SHOW FIELDS FROM authors_books
  SQL (0.000000)  INSERT INTO authors_books (`author_id`, `book_id`) VALUES (1, authors_books Columns (0.000000)  SHOW FIELDS FROM authors_books
  SQL (0.000000)  INSERT INTO authors_books (`author_id`, `book_id`) VALUES (2, Publisher Load (0.000000)  SELECT * FROM publishers WHERE (publishers.id = 1) LI
  Book Load (0.000000)  SELECT * FROM books WHERE (books.id = 9) LIMIT 1
SQL (0.000000)  SELECT count(*) AS count_all FROM books WHERE (books.publisher_i
  Join Table Columns (0.015000)  SHOW FIELDS FROM authors_books
Author Load (0.000000)  SELECT * FROM authors INNER JOIN authors_books ON author
 
SQL (0.329000)  ROLLBACK
——————————————–

As you can see from the first and last line in the sample output, each test is wrapped in a transaction, and changes done by the test are rolled back at the end of the test.

Adding a Fixture for the Many-to-Many Relationship

Next, we’ll add a fixture that contains the data needed in the authors_books join table. We will use the data in the next section when writing a unit test that tests the many-to-many mapping. Create a new file named test/fixtures/authors_books.yml and add the following code:

pro_rails_ecommerce_1:
  author_id: 1
  book_id: 1
pro_rails_ecommerce_2:
  author_id: 2
  book_id: 1

The fixture links the two authors defined in the authors fixture to a record found in the books fixture.

Unit Testing the Many-to-Many Mapping

Change the fixtures declaration in the app/test/unit/book_test.rb file to use the new fixture:

fixtures :publishers, :authors, :books, :authors_books

Next, implement a test that verifies that the many-to-many mapping works. This test will verify that you can access the list of authors of a specific book by calling book.authors , and that you, from the author’s perspective, are able to access the list of books an author has written by calling author.books . Open test/unit/book_test.rb and add the following method to the end of the file.

  def test_has_and_belongs_to_many_authors_mapping
   
book = Book.new(
     
:title => ‘Rails E-Commerce 3nd Edition’,
     
:publisher => Publisher.find_by_name(‘Apress’),
     
:authors => [Author.find_by_first_name_and_last_name(‘Christian’, ‘Hellsten’),
     
Author.find_by_first_name_and_last_name(‘Jarkko’, ‘Laine’)],
     
:published_at => Time.now,
     
:isbn => ‘123-123-123-x’,
     
:blurb => ‘E-Commerce on Rails’,
     
:page_count => 300,
     
:price => 30.5
    )

    assert book.save

    book.reload

    assert_equal 2, book.authors.size
    assert_equal 2, ➥ Author.find_by_first_name_and_last_name(‘Christian’, ‘Hellsten’).books.size
  end

The unit test performs the following steps:

  1. Create a new book and assign two authors and one publisher to it. 
     
  2. Reload the book from the database. 
     
  3. Verify that the book has two authors. 
     
  4. Verify that one of the authors has two books, of which one is created by the test and th e other by the fixture.

Now run the unit tests:

$ ruby test/unit/book_test.rb

——————————————–
Loaded suite test/unit/book_test
Started
..
Finished in 0.421 seconds.

4 tests, 16 assertions, 0 failures, 0 errors
——————————————–

You should see no errors.

Generating Book Administration Code with the Scaffolding Script

With both the database schema and ActiveRecord model in place, we are now ready to start implementing the front-end. The requirements for book administration include five user stories: Add Book, Upload Book Cover, View Book, Edit Book, and Delete Book.


Tip  It’s good practice to run all your tests—including unit, integration, and functional—after you make any big changes, as we have done in this chapter. This can be done by running the rake command without specifying any parameters. However, at this point, it will throw an error. You can fix this by adding the line config.active_record.schema_format = :sql to config/environment.rb .


As with the publisher administration interface, we use scaffolding to create the controller, model, and view files by executing the generate script:

$ script/generate scaffold Book ‘admin/book’

existsapp/controllers/admin
existsapp/helpers/admin
createapp/views/admin/book
existstest/functional/admin
dependencymodel
existsapp/models/
existstest/unit/
existstest/fixtures/
skipapp/models/book.rb
skiptest/unit/book_test.rb
skiptest/fixtures/books.yml
createapp/views/admin/book/_form.rhtml
createapp/views/admin/book/list.rhtml
createapp/views/admin/book/show.rhtml
createapp/views/admin/book/new.rhtml
createapp/views/admin/book/edit.rhtml
createapp/controllers/admin/book_controller.rb
createtest/functional/admin/book_controller_test.rb
createapp/helpers/admin/book_helper.rb
createapp/views/layouts/book.rhtml
createpublic/stylesheets/scaffold.css

You can delete the public/stylesheets/scaffold.css file, because you already have a style sheet. Note that the generated functional test will fail if you execute it. You can decide whether to keep it or

You can delete the public/stylesheets/scaffold.css file, because you already have a style sheet. Note that the generated functional test will fail if you execute it. You can decide whether to keep it or delete it, but we deleted it by executing the following command:

$ rm test/functional/admin/book_controller_test.rb

Now we’ll introduce you to integration tests, which we’ll use to test the front-end instead of functional tests.

Integration Testing

As we mentioned in Chapter 2, Ruby on Rails 1.1 introduced the concept of integration tests. Integration tests can be used to write tests that span multiple controllers and exercise the whole application, from the dispatcher to the database.

Suppose that we wanted to write a test for the whole Emporium administration interface. The test would need to simulate the actions of one or more administrators: logging in to the application; administering authors, publishers, and books; and logging out. Integration tests are a good way of simulating these actions, as they can be used to ensure that related functionality works as expected when multiple controllers and actions are called in sequence.

Another benefit of using integration tests is that they allow you to open multiple sessions, unlike functional tests, which use the same session for the whole test. Opening a new session is done by calling the open_session method, which returns a new session object. This opens up a whole new range of possibilities for testing your code. For example, you can simulate multiple users accessing the same application at the same time, and you can test for bugs that are related to the session. The open_session method also enables you to extend the session with your own methods. This technique can be used to write a domain-specific language (DSL).


Tip  It’s a good idea to try to create test cases that are based on the user story. Try to follow the same flow of actions and events as in the user story. Also test alternative use case flows, which might involve invalid user input, for example.


Jamis Buck, one of the Rails core team members, points out on his blog jamis.jamisbuck.org that one of the biggest benefits of using integration tests is that you can easily create DSLs. As discussed in Chapter 2, Rails itself uses DSLs for many tasks, such as ActiveRecord mappings and validations. A DSL, as its name implies, is a language you write in Ruby code (or other programming language) for a specific domain. In the context of the Emporium project, for example, one domain is the integration testing of our book administration interface. DSLs should support actions related to a domain.

The following is an example of a DSL. If you read it line by line, you can get a sense of what it does, even without knowing too much about DSLs.

require "#{File.dirname(__FILE__)}/../test_helper"

class DSLTest < ActionController::IntegrationTest

  def test_browse_book_stor e
    george = new_session
    bob = new_session

    george.add_book(…)
   
bob.view_book(…)
   
bob.add_book_to_cart(…)
 
end
    private

    module TestingDSL
      def add_book(…)
        …
      end

      def view_book(…)
        …
      end

      def add_book_to_cart(…)
        …
      end

      def new_session
       
open_session do |sess|
          sess.extend(TestingDSL)
          yield sess if block_given?
        end
      end
    end
end

You’ll see how to implement integration testing in the following sections, as we complete each of the book administration user stories for this sprint.

Completing the Add Book User Story

As you have noticed, throughout this chapter, we haven’t followed TDD very strictly. Instead, we first created the code using scaffolding. Although we can add, list, view, edit, and delete books, the functionality is not tested and we are not confident that it is working as George desires. We’ll have to talk to George to find out what exactly should be implemented.

We call George over to our cubicle, which happens to be the only cubicle in the office, reserved exclusively for consultants. George tells us that when adding a book to the system, he must be able to enter all details of the book, including title, price, ISBN, blurb, and so on. Furthermore, George tells us that the current system is difficult to use. Because of this, he has to consult his computer-literate nephew, who enters the details of new books into the system. The blurb text is what is causing him most troubles. When displayed on the website, the blurb text must be nicely styled with, for example, proper headings, bulleted lists, and italicized text, so that it looks as good as possible. On the Web, this requires HTML skills, and because George doesn’t know HTML, he can’t write the blurb himself. Luckily, there’s a simple answer to the problem called Textile.

Textile is a simple text markup language that can be used to write content for the Web without needing to know HTML. RedCloth is a Ruby module that adds Textile support to Rails applications. You’ll see how this works when we implement the View Book user story, later in the chapter.

Our first task is to create the integration test we will use to test the book administration implementation.

Creating an Integration Test

We’ll create a DSL that will closely match the actions performed in the book administration user stories. Create the integration test by executing the following command:

$ script/generate integration_test book

——————————————–
  exists  test/integration/
  create  test/integration/book_test.rb
——————————————–

As with unit tests, the integration test contains only a dummy test, so modify the test/integration/book_test.rb file as shown in Listing 3-2.


RUBY BLOCKS

A block is a piece of Ruby code that can be passed to a Ruby method. Unlike normal parameters, blocks can be passed to all Ruby methods without explicitly declaring that the method takes a block as a parameter. The method receiving the block, as a parameter, can evaluate the code, by calling the yield method.

The following example shows how Ruby blocks can be used for preprocessing and postprocessing by passing a block to the log method.

def log
  puts "before"
  yield
  puts "after"
end

log { puts "in between" } # block on one line

The following is the output of executing this example:

before
in between
after

The following syntax is preferred for blocks that span more than one line:

log do
  calculate_x
  calculate_y
end


Listing 3-2. First Version of Integration Test for the Book Administration Interface

require "#{File.dirname(__FILE__)}/../test_helper"

class BookTest < ActionController::IntegrationTest 
  fixtures :publishers, :authors
  def test_book_administration
   
publisher = Publisher.create(:name => ‘Books for Dummies’)
    author = Author.create(:first_name => ‘Bodo’, :last_name => ‘Bär’)

    george = new_session_as(:george)
    ruby_for_dummies = george.add_book :book => {
      :title => ‘Ruby for Dummies’,
      :publisher_id => publisher.id,
      :author_ids => [author.id],
      :published_at => Time.now,
      :isbn => ‘123-123-123-X’,
      :blurb => ‘The best book released since "Eating for Dummies"’,
      :page_count => 123,
      :price => 40.4
   
}
  end

  private

  module BookTestDSL
    attr_writer :name

    def add_book(parameters)
      post "/admin/book/create", parameters
      assert_response :redirect
      follow_redirect!
      assert_response :success
      assert_template "admin/book/list"
      assert_tag :tag => ‘td’, :content => parameters[:book][:title]
      return Book.find_by_title(parameters[:book][:title])
   
end
  end

  def new_session_as(name)
   
open_session do |session|
      session.extend(BookTestDSL)
      session.name = name
      yield session if block_given?
    end
  end

end

Note that the test_book_administration test will be used for verifying that the whole book administration works from end to end. The first step in doing this is implementing a test for the Add Book user story.

Also note that the method new_session_as(name) is used to open a new session for a virtual user. Inside the method, we use some Ruby magic to extend the new session object at runtime with our book-testing DSL. This is done with the extend method, which simply adds the instance methods in the BookTestDSL module to the session object.

We also save the name of the user in an instance variable inside the DSL module. This allows you to use it later, if required.

The line yield session if block_given? is used to pass the new session to a block, if a block has been specified.

The integration test performs the following actions, which verify that the Add Book user story works:

  1. Create a new author and publisher. 
     
  2. Open a new session as George. 
     
  3. Create a new book by calling the create action with valid parameters. 
     
  4. Verify that there is a redirection to the list books view, which should happen if the book was created successfully.

Run the integration test, and you should see that all tests pass:

$ ruby test/integration/book_test.rb

——————————————–
Loaded suite test/integration/book_test Started
.
Finished in 0.453 seconds.

1 tests, 4 assertions, 0 failures, 0 errors
——————————————–

Since the test didn’t fail, you could be tricked into believing that we have just finished the implementation of the Add Book user story, but you can see that this is not the case by opening a browser and going to http://localhost:3000/admin/book/new . You should see the front-end for the Add Book user story, as shown in Figure 3-6.

The page you see on the screen was created by the scaffolding script, and includes drop-down lists for the published_at , created_at , and updated_at fields. The values for created_at and updated_at are generated by Rails automatically, so George shouldn’t have to see them. There’s also no way of specifying the authors or publisher of the book.


Figure 3-6.  Testing the Add Book user story

Changing the Controller

The add book page should provide a way of selecting the publisher and one or more authors for the book. This will be done using a drop-down list of all available publishers and a multiple-selection list showing all authors. To be able to show the authors and publishers in the view, we must tell the controller to load all publishers and authors, and pass them to the view.

Open app/controllers/admin/book_controller.rb and add the lines shown in bold to the new and create actions.

  def new
   
load_data
   
@book = Book.new
  end

  def create
    @book = Book.new(params[:book])
    if @book.save
     
flash[:notice] = ‘Book was successfully created.’
      redirect_to :action => ‘list’
    else
     
load_data
     
render :action => ‘new’
    end
  end

Also, add the following method to the end of the controller:

private

def load_data
  @authors = Author.find(:all)
  @publishers = Publisher.find(:all)
end

Note that the new method load_data should be declared private, as it is used only internally by the controller. This method loads all authors and publishers from the database and stores them as instance variables, which allows you to access them from the view.

We also needed to change the create action, since it will render the new action’s view, which expects to find the authors and publishers, if there is a validation error on the Book object.

The form shown on the add book page is generated by the app/views/admin/book/_form.rhtml file. This file is shared by both the edit and add book pages.

Changing the View

Next, we will use the collection_select view helper to generate the drop-down list for publishers. The format for the collection_select helper is as follows:

<%= collection_select :book, :publisher_id, @publishers, :id, :name %>

The first and second parameters tell the helper to which model and attribute to bind the field. The third parameter is used to pass a list of publishers that should be shown in the drop-down list. The two last parameters, :id and :name , are used to specify that the value for the drop-down list should be the publisher’s id and that the label should be the publisher’s name.

select_tag is used for generating a list of authors from which George can select one or more authors. The format for this helper is as follows:

<%= select_tag ‘book[author_ids][]’,
      options_from_collection_for_select(@authors, :id, :name, ➥ @book.authors.collect{|author| author.id}),
        { :multiple => true, :size => 5 }
%>

select_tag has the following parameters:

  1. The first parameter, book[author_ids][] , specifies to which attribute the field should b e bound. Note that the parameter must end with [] so that Rails knows that the attribute it needs to bind the value to is an array. 
     
  2. The second parameter, options_from_collection_for_select , is used for generating the list of options that should be shown in the list. This method’s first parameter is a collection of authors. The method’s second and third parameters, :id and :name , specify the attributes that should be used as the value and label, respectively. The fourth parameter is used for preselecting the authors that have been assigned to the book. 
     
  3. The third parameter of the select_tag is used for specifying options. In this case, we specify that the list should support multiple-selections and that five authors should be shown.

Next, change the app/views/admin/book/_form.rhtml file as follows:

<%= error_messages_for ‘book’ %>

<p><label for="book_title">Title</label><br/>
<%= text_field ‘book’, ‘title’ %></p>

<p><label for="book_publisher">Publisher</label><br/>
<%= collection_select :book, :publisher_id, @publishers, :id, :name %></p>

<p><label for="book[author_ids][]">Authors</label><br/>
<%= select_tag ‘book[author_ids][]’,
      options_from_collection_for_select(@authors, :id, :name, ➥
@book.authors.collect{|author| author.id}),
        { :multiple => true, :size => 5 }
%>
</p>

<p><label for="book_published_at">Published at</label><br/>
<%= datetime_select ‘book’, ‘published_at’ %></p>

<p><label for="book_isbn">Isbn</label><br/>
<%= text_field ‘book’, ‘isbn’ %></p>

<p><label for="book_blurb">Blurb</label><br/>
<%= text_area ‘book’, ‘blurb’ %></p>

<p><label for="book_price">Price</label><br/>
<%= text_field ‘book’, ‘price’ %></p>

<p><label for="book_price">Page count</label><br/>
<%= text_field ‘book’, ‘page_count’ %></p>

Notice that we use the text_field, collection_select , select_tag , datetime_select , and text_area helpers for creating the fields. You can test the Add Book user story by first adding a couple of authors and publishers to the database, either through the user interface we created earlier or by executing the following from the console:

$ script/console

——————————————–
Loading development environment.
>> Publisher.create(:name => ‘Apress’)
>> Author.create(:first_name => ‘Salman’, :last_name => ‘Rushdie’)
>> Author.create(:first_name => ‘Joel’, :last_name => ‘Spolsky’)
——————————————–

Then, open http://localhost:3000/admin/book/new in your browser. You should see a page that looks similar to Figure 3-7.


Figure 3-7.  The add book page with publishers and authors

You should be able to add books to the system by entering all valid information. If you forget to enter something in a required field, you should see validation errors similar to those shown in Figure 3-8.


Figure 3-8.  Validation errors

Updating the Integration Test

As usual, we should change the integration test to reflect the changes we have made to the code. We should test that we can create a book and that the view contains what is expected. Next, change the add_book method in the DSL as shown in Listing 3-3.

Listing 3-3. The Updated Integration Test for the Add Book User Story

  def add_book(parameters)
   
author = Author.find(:all).first
   
publisher = Publisher.find(:all).first

    get "/admin/book/new"
    assert_response :success
   
assert_template "admin/book/new"

    assert_tag :tag => ‘option’, :attributes => { :value => publisher.id }
    assert_tag :tag => ‘select’, :attributes => {
      :id => ‘book[author_ids][]’}

    post "/admin/book/create", parameters
   
assert_response :redirect
   
follow_redirect!
   
assert_response :success
   
assert_template "admin/book/list"
   
assert_tag :tag => ‘td’, :content => parameters[:book][:title]
   
return Book.find_by_title(parameters[:book][:title])
  end

Instead of just calling the create action with valid parameters and verifying that the request was successful, we now test the complete user story, including that the form contains a list of publishers and authors. This is done with the assert_tag , which checks if the drop-down list and multiple-selection list are displayed on the screen.

Run the integration test again, and you should see all tests pass:

$ ruby test/integration/book_test.rb

——————————————–
Loaded suite test/integration/book_test Started
.
Finished in 0.532 seconds.

1 tests, 8 assertions, 0 failures, 0 errors
——————————————–

We have now finished the implementation of the Add Book user story.

Completing the Upload Book Cover User Story

The Upload Book Cover user story is performed by the administrator, George. When adding a book, George should be able to select an image and upload it to the Emporium site. This image is then shown to customers when they are viewing the details of a book.

Adding File Upload Functionality

We don’t have to reinvent the wheel to implement file upload functionality. Sebastian Kanthak has already implemented the file upload functionality we need and released it as the FileColumn plugin. The plugin contains view helpers and an extension to ActiveRecord that allows us to implement file upload easily.

Install the FileColumn plugin by executing the following command:

$ script/plugin install http://opensvn.csie.org/rails_file_column/ plugins/file_column/trunk/

This downloads the latest version of the plugin from the Internet and installs it in the vendor/plugins/trunk directory. After the installation has finished, rename the trunk directory to file_column . Note that you need to restart WEBrick to activate the plugin.


Tip  For more information about FileColumn, visit http://www.kanthak.net/opensource/ file_column/ . For example, you can discover how to configure FileColumn to resize the uploaded image with the RMagick image processing library.


The FileColumn plugin stores the path to the uploaded image in the database. The exact column where it should store the information is specified with a call to the file_column method. Currently, our database schema doesn’t contain a column that we can use for this purpose, which is why we’ll create it in the next section.

But first, add the file_column call to app/models/book.rb :

class Book < ActiveRecord::Base
  has_and_belongs_to_many :authors
  belongs_to :publisher

  file_column :cover_image
  validates_length_of :title, :in => 1..255

By calling file_column , we include the file upload functionality in our model and tell it to store the path to the uploaded image in the cover_image column.


Note  At the time of writing, the FileColumn plugin contained an annoying bug that runs a unit test located in the plugin’s lib directory every time you execute rake or a script. To fix this, delete vendor/plugins/file_column/lib/test_case.rb and remove the line require ‘test_case’ from the vendor/plugin/file_column/init.rb file.


Modifying the Database Schema

We’ll use an ActiveRecord migration to add the cover_image column to the books table. Create the migration with the following command:

$ script/generate migration add_book_cover_column

——————————————–
  exists  db/migrate
  create db/migrate/004_add_book_cover_column.rb
——————————————–

Add the following migration code to db/migrate/004_add_book_cover_column.rb .

class AddBookCoverColumn < ActiveRecord::Migration
  def self.up
    add_column :books, :cover_image, :string
  end

  def self.down
    remove_column :books, :cover_image
  end
end

The migration adds the cover_image column to the books table, and removes it if we are rolling back changes.

You can now execute the migration with rake migrate :

$ rake migrate

——————————————–
(in C:/projects/emporium)
== AddBookCoverColumn: migrating ============================================== — add_column(:books, :cover_image, :string)
   -> 0.5150s
== AddBookCoverColumn: migrated (0.5150s) ===========================================
——————————————–

Cloning the Changes

You should clone the changes to your test database, because we’ll create an integration test later in this chapter that tests the file upload functionality. You can clone the development database to test by executing the following command:

rake db:test:clone_structure

As usual, you could run rake without specifying any parameters.

Changing the Form

Next, we’ll change the form we created for the Add Book user story so that the user can select an image and upload it. Add the following code to the end of the view app/views/admin/book/_form.rhtml .

<p><label for="book_cover_image">Cover image</label><br/>
<%= file_column_field ‘book’, "cover_image" %></p>

Note that the file upload functionality requires that we change the form encoding to be multipart/form-data . This is done by changing the start_form_tag in app/views/admin/book/new.rhtml , as follows:

<%= start_form_tag( {:action => ‘create’}, :multipart => true ) %>

You can now test the file upload functionality in your browser by opening http://localhost:3000/admin/book/new and selecting a file for the Cover image field. As you can see after clicking the Create button, the path to the uploaded image is stored in the database.

When we implement the View Book user story, we will show you how to use the url_for_file_column method to extract the path and display the image on a page:

  <%= image_tag url_for_file_column(:book, :cover_image) %>


Tip  At the time of writing, we couldn’t test file uploading with integration tests because of a bug in Rails. But, when it is fixed, you can use the fixture_file_upload in your tests to create a valid HTTP parameter that can be used by the get and post methods; for example, :cover_image => fixture_file_upload (‘/book_cover.gif’, ‘image/png’) . Note that the book_cover.gif image should be in the fixtures directory.


Completing the List Books User Story

We already created a page with scaffolding that lists all the books in the system. This page can be accessed at http://localhost:3000/admin/book/list. We show it to George and he seems happy, except for two things: he can’t sort the list and he only needs to see the publisher’s name and the book’s title and ISBN.

Changing the View

To fix the book list page, first change the view as follows:

<table>
 
<tr>
   
<th><a href="?sort_by=publisher_id">Publisher</a></th>
    <th><a href="?sort_by=title">Title</a></th>
    <th><a href="?sort_by=isbn">ISBN</a></th>
    <th colspan="3"></th>

  </tr>

<% for book in @books %>
  <tr>
    <td><%=h book.publisher.name %></td>
    <td><%=h book.title %></td>
    <td><%=h book.isbn %></td>
    <td><%= link_to ‘Show’, :action => ‘show’, :id => book %></td>
    <td><%= link_to ‘Edit’, :action => ‘edit’, :id => book %></td>
    <td><%= link_to ‘Destroy’, { :action => ‘destroy’, :id => book }, ->
:confirm => ‘Are you sure?’, :post => true %></td>
 </tr>
<% end %>
</table>
<%= link_to ‘Previous page’, { :page => @book_pages.current.previous } ->
if @book_pages.current.previous %>
<%= link_to ‘Next page’, { :page => @book_pages.current.next } ➥
if @book_pages.current.next %>
<br/>
<%= link_to ‘New book’, :action => ‘new’ %>

The links we added allow George to sort the list when the Publisher, Title, or ISBN column is clicked.

Changing the Controller

The following code implements the sorting. Change the app/controllers/admin/book controller.rb file accordingly.

  def list
   
@page_title = ‘Listing books ’ 
    sort_by = params[:sort_by]

    @book_pages, @books = paginate :books, :order => sort_by, :per_page => 10
 
end

Note the sort order is specified with the sort_by parameter. This parameter is passed to the paginate method, which has built-in support for ordering the paginated list.

Adding an Integration Test

We’ll update our book administration DSL to include a method for testing the List Books user story. The new method performs a simple smoke test. It accesses the page and verifies that the server responds with an HTTP 200 status code, which means the request was successfully processed.

Change the BookTestDSL as follows, adding the code shown in bold.

  module BookTestDSL
    attr_writer :name

    def list_books
     
get "/admin/book/list"
     
assert_response :success
     
assert_template "admin/book/list"
    end

    def add_book(parameters)

Also add the row highlighted below to the end of the test_book_administration test. This method simulates George browsing to the book list page, right after he has added a new book.

  def test_book_administration
    .
    .
    george.list_books
 
end

The finished page can be accessed at http://localhost:3000/admin/book/list and should look like Figure 3-9.


Figure 3-9.  Testing the List Books user story

Completing the View Book User Story

The View Book user story also needs some cleaning up before George is happy. The code created by the scaffolding displays the values of all database columns directly to the user. This means, for example, that the publisher’s ID is shown instead of the publisher’s name. We’ll fix this and also add code that displays the authors of the book and the book cover.

Changing the View

First, change app/views/admin/book/show.rhtml as follows:

<dl>
  <dt>Title</dt>
  <dd><%= @book.title %></dd>  
  <dt>Publisher</dt>
  <dd><%= @book.publisher.name %></dd>
  <dt>Published at</dt>
  <dd><%= @book.published_at.strftime("%m/%d/%Y at %I:%M%p") %></dd>
  <dt>Authors</dt>
  <dd><%= @book.authors.collect{|author| author.name }.join(‘, ‘) %></dd>
  <dt>ISBN</dt>
  <dd><%= @book.isbn %></dd>
  <dt>Blurb</dt>
  <dd><%= textilize @book.blurb %></dd>
  <dt>Price</dt>
  <dd><%= @book.price %></dd>
  <dt>Page count</dt>
  <dd><%= @book.page_count %></dd>
  <dt>Cover image</dt>
 
<% if @book.cover_image.nil? %>
  <dd>N/A</dd>
 
<% else %>
  <dd><%= image_tag url_for_file_column(:book, :cover_image) %></dd>
 
<% end %>
</dl>

<p><%= link_to "Edit", :action => "edit", :id => @book %> |
<%= link_to "Back", :action => "list" %></p>

Note that we use image_tag and the method url_for_file_column to display the uploaded image of the book cover, but only if it exists. We also format the field published_at to use a standard format.

Recall that George wanted the Blurb field to be easy to edit. This is why we have used the Textile markup language in the Blurb field, instead of HTML. The Textile markup we entered in the Blurb field is passed through the textilize method in the view:

<%= textilize @book.blurb %>

This translates the Textile markup in the Blurb field to HTML. You’ll see this in action in the next section.


Note  The textilize method is resource-intensive and should be executed only once (when the object is saved). The resulting HTML should be stored in a database field, for example, blurb_html . The conversion can easily be done using a before_save filter in the Book model, and then changing the view to display the blurb_html column’s value, instead of running the conversion for each request.


Changing the Controller

There’s one more thing to fix. The view expects to find the instance variable page_title , which means you should change the controller’s show action, as follows:

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

    @page_title = #{@book.title}
  end

You can now access the book details page by clicking the Show link located next to a book on the books list page. Figure 3-10 shows the page after all the changes have been done. Note that the uploaded image is shown at the bottom of the page.


Figure 3-10.  Testing the View Book user story

Another thing to note about Figure 3-10 is that the Blurb field shows a heading, a bulleted, and a numbered list. In Figure 3-10, we entered the following into the Blurb field:

h1. This is a heading

* Item 1
## Item 1.1
 
* Step 2
## Step 2.1

Tip  See http://en.wikipedia.org/wiki/Textile_(markup_language) for more information about the Textile markup language.


Adding an Integration Test

We’ll also add an integration test for the View Book user story. This is a simple test that verifies that the page doesn’t throw an error. Add the following code to the DSL.

  def show_book(book)
    get "/admin/book/show/#{book.id}"
    assert_response :success
    assert_template "admin/book/show"
  end

The show_book method takes a book as a parameter, which it uses to call the show action.

Also add the highlighted line, shown in the following code, to the last line of the test_book_administration method, right after the line george.list_books :

  george.list_books
 
george.show_book ruby_for_dummies
end

This calls the test using the book we created earlier in the test. Run the integration test again to verify that it still passes:

$ test/integration/book_test.rb

——————————————–
Loaded suite test/integration/book_test Started
.
Finished in 0.531 seconds.

1 tests, 12 assertions, 0 failures, 0 errors
——————————————–

Completing the Edit Book User Story

One user story remains for us to implement, before we can call it a day. Luckily, most of the code was generated with scaffolding.

Open a browser and verify that the Edit Book user story works. Add a book to the system and click the Edit link next to the book in the list of books. You should see the following error message:

——————————————–
NoMethodError in Admin/book#edit
——————————————–

Rails is kind enough to tell us the error is around line 5, which is where we display the list of publishers. The test is failing because we haven’t loaded the publisher object, which the view expects to find. To fix this, we need to change the action so that it loads the publishers and authors in the same way we did for the Add Book user story. Since we already created the load_data method, we only need to add a call to it in the edit action, as follows:

  def edit
   
@page_title = ‘Editing book’
    load_data
   
@book = Book.find(params[:id])
  end

We also need to change the form to use multipart encoding, because we added the file upload functionality earlier in the chapter:

<%= start_form_tag( {:action => ‘update’, :id => @book}, :multipart => true )%>
 
<%= render :partial => ‘form’ %>
 
<%= submit_tag ‘Edit’ %>
<%= end_form_tag %>

<%= link_to ‘Show’, :action => ‘show’, :id => @book %> |
<%= link_to ‘Back’, :action => ‘list’ %>

It’s important that you test the edit functionality. Add a new method to the testing DSL:

def edit_book(book, parameters)
  get "/admin/book/edit/#{book.id}"
  assert_response :success
  assert_template "admin/book/edit"

  post "/admin/book/update/#{book.id}", parameters
  assert_response :redirect
  follow_redirect!
  assert_response :success
  assert_template "admin/book/show"
end

The new edit_book method takes an instance of a book as a parameter and the parameters hash. The parameters hash should contain the new attributes that the book should be updated to use.

Lastly, add a call to the edit_book method right after the george.show_book line in the test_book_administration method:

  george.show_book ruby_for_dummies

  george.edit_book(ruby_for_dummies, :book => {
    :title => ‘Ruby for Toddlers’,
    :publisher_id => publisher.id,
    :author_ids => [author.id],
    :published_at => Time.now,
    :isbn => ‘123-123-123-X’,
    :blurb => ‘The best book released since "Eating for Toddlers"’,
    :page_count => 123,
    :price => 40.4
 
})
end

Run the integration test by executing ruby test/integration/book_test.rb , and you should see no errors. Verify that you can edit a book by accessing the edit page in your browser.

Testing the Delete Book User Story

The last user story, Delete Book, is already complete. Scaffolding created the destroy action in the book controller, which is all we need. But, we can’t be sure it works until we have a test in place, so we’ll write an integration test for it.

Add the new delete_book method to the DSL:

  def delete_book(book)
   
post "/admin/book/destroy/#{book.id}"
   
assert_response :redirect
   
follow_redirect!
   
assert_template "admin/book/list"
  end

The new method simply calls the destroy action and verifies that we are redirected to the list books page.

We’ll allow another user, not George, to execute the test, to better illustrate how integration tests can be used. Add the two highlighted lines to the end of the test_book_administration method.

    :page_count => 123,
    :price => 40.4
  })

  bob = new_session_as(:bob)
  bob.delete_book ruby_for_dummies
end

Again, run the tests with rake test:integrations . You should see no errors, which means you have successfully implemented the book administration interface.

We quickly do an ad hoc usability test with George, by allowing him to add a couple of books and publishers to the system. He is delighted that everything works and that we could finish it so quickly. We decide to call it a day and head home.

Summary

In this chapter, we introduced you to scaffolding and showed you how to map one-to-many, many-to-one, and many-to-many relationships with ActiveRecord. We also showed you how to write integration tests and use a custom testing DSL for the whole book administration interface. Additionally, you saw how to implement file upload capabilities with the FileColumn plugin and how to use the Textile markup language to simplify content creation. At the end of the chapter, we had a working book inventory management system, with extensive tests that make us confident that we can handle future changes to the system without breaking it.

In the next chapter, we’ll implement the front-end for the book catalog functionality, which is what the customer will use.

[gp-comments width="770" linklove="off" ]

chat sex hikayeleri Ensest hikaye