Practical 4

0. Finish Practical Sheet 3. Run the tests (rake test) to make sure everything works fine. Today you will create some new tables for various types of objects, and connect them to other tables in the database.

1. More precisely, we need two additional tables in the data base, one for shopping carts, and one for items in shopping carts, so-called line items.

The purpose of a shopping cart object is to contain the products a buyer chooses for purchase. This concept is best modelled as a relationship between products and carts. Such a relationship sits in its own table, which here will be called line_items.

The cart object itself needs no components, and its scaffolding can be generated with a simple

rails generate scaffold cart

command. Then run

rake db:migrate

to actually make the table in the database.

2. Sessions. How does this work, a cart per user, with no user actually logged in? Rails maintains state between requests through the use of cookies, and one thing contained in the cookie is called the session. This is a ruby hash session that allows the application to recognise a returning user, and can furthermore be used to store session related information, such as this user’s shopping cart.

3. Concerns. A concern is a piece of code that can be shared between different controllers. It sits in a file in the dedicated folder app/controllers/concerns.

Here it will be convenient to declare a method set_cart() (which either finds this user’s cart, or creates a new one) as a concern. This is done by putting the following code into a file app/controllers/concerns/current_cart.rb.

module CurrentCart
  private

  def set_cart
    @cart = Cart.find(session[:cart_id])
  rescue ActiveRecord::RecordNotFound
    @cart = Cart.create
    session[:cart_id] = @cart.id
  end
end

This code contains a couple of new ruby features, like module, which we might ignore for the moment. Marking the current_cart method as private restricts access to it. The rescue clause is executed only in case session[:cart_id] is currently undefined (which will cause an ActiveRecord::RecordNotFound error to be raised).

3a. It might be a good idea to commit this incremental development step to the git repository. What commands would you have to type at the command line to achieve this?

4. Relationships. The purpose of a line_item is to connect a product to a cart. In the language of relational databases, the line_item table represents a many-to-many relationship between products and carts: a cart can contain many products, a product can be contained in many carts. In this particular form, the many-to-many relationship is the result of 1. a one-to-many relationship between line_items and products: each line item refers to one product and a product can be referred to by many line items; and 2. a one-to-many relationship between line_items and carts: … The scaffolding command

rails generate scaffold line_item product:references cart:references

will create the necessary files to set this up, including a migration. Run

rake db:migrate

to create the line_times table in the database.

Defining the type of both the product and the cart component of a line_item as references has various effects. In the database, it will make columns product_id and cart_id of type integer. In the LineItem class (which file?) it will add the declarations

belongs_to :product
belongs_to :cart

with the following effect. In addition to the attributes .product_id and .cart_id (which are supported by database columns), any line item now has two additional attributes .product and .cart which can be used to access the cart of a line item li simply as li.cart (rather than Cart.find(li.cart_id)).

5. Relationships work in two ways, backwards and forwards. It makes as much sense to talk about all the line items in a cart, as it does for the cart of a line item (but note the use of plural vs. singular: we are talking about a one-to-many relationship here). This opposite point of view is registered with the Cart model (which file?) by adding the following line to its definition:

has_many :line_items, dependent: :destroy

Now, we can access the line items of a cart cart as cart.line_items, and determine their number as cart.line_items.count if so desired.

(The dependent: :destroy part will have the effect that when a cart is deleted from the database, all line items associated with the cart will also be deleted. Makes sense, doesn’t it.)

6. Add a similar line (without dependent: :destroy) to the Product model (which file?). This will allow us to access all the line items that reference a product product as product.line_items.

7. Here, rather than silently deleting all line items referring to a product when that product is deleted, we want to ensure that a product is not removed from the database, as long as there are carts containing it to be processed, that is, as long as it is still referenced by a line item. Add the following line to the top of the Product class definition:

before_destroy :ensure_not_referenced_by_any_line_item

and the following text to its end:

private

# ensure that there are no line items referencing this product
def ensure_not_referenced_by_any_line_item
  unless line_items.empty?
    errors.add(:base, 'Line Items present')
    throw :abort
  end
end

What does this code accomplish? We can add a test to products_controller_test.rb that ensures that a product in a cart cannot be deleted:

test "can't delete product in cart" do
  assert_difference('Product.count', 0) do
    delete product_url(products(:two))
  end

  assert_redirected_to products_url
end

Also change the line product: one to

product: two

in the line_items fixture file (where?), so that product two is contained in both line items when testing. Do a

rake test

to see (or not) the effect.

7a. If all works fine, commit the changes to your local git repository, and push them to the github cloud.

git add .
git commit -m "added cart and line item models."
git push

8. Buttons. Buttons (in Rails) by default send a HTTP POST request (as opposed to the HTTP GET request that is sent by a link). The create action in the line item controller (which file?) expects a HTTP POST request, doesn’t it? That’s why a button in the user interface is a perfect match for the wish to create a line item (i.e., a buyer’s desire to purchase a product). The purchase decision in the form of a clickable button is implemented as

<%= button_to 'Add to Cart', line_items_path(product_id: product) %>

in the right place (inside the <div> with class="price") on the catalog page (store/index.html.erb).

9. The button looks better with some additional styling:

form, div {
  display: inline;
}

input[type="submit"] {
  background-color: #282;
  border-radius: 0.354em;
  border: solid thin #141;
  color: white;
  font-size: 1em;
  padding: 0.354em 1em;
}

input[type="submit"]:hover {
  background-color: #141;
}

to be inserted just after the CSS rule for .price near the end of the stylesheet store.scss.

10. Next, we need to inform the line items controller (which file?) how to actually create a new line item, taking into account the product whose button has been pressed, and the cart to put it into. For the cart, we have earlier prepared the CurrentCart concern. In order to use it now, add the following two lines to the top of the definition of the line items controller.

include CurrentCart
before_action :set_cart, only: [:create]

This includes the CurrentCart module and arranges that the set_cart() method is invoked before every create() action. The effect of this will be that a suitable cart is assigned to the instance variable @cart.

11. Then, to actually build the line item, replace the first line of the create() action by:

product = Product.find(params[:product_id])
@line_item = @cart.line_items.build(product: product)

and in the format.html part, replace redirect_to @line_item by

redirect_to @line_item.cart

What effect will that have?

12. Now, since the controller method has been modified, we need to update the corresponding functional test. Run the tests now and see one failing. In that failing test ("should create line item", which file?), replace the post line by

post line_items_url, params:  { product_id: products(:ruby).id }

and the assert_redirected_to line by

follow_redirect!

assert_select 'h2', 'Your Pragmatic Cart'
assert_select 'li', 'Programming Ruby 1.9 & 2.0'

Then run the tests again. And test the Add to Cart button in your application …

13. The effect of clicking the button could be a bit more informative. Firstly, the cart page could list the items currently in the cart. For this, replace the contents of carts/show.html.erb with

<% if notice %>
  <aside id="notice"><%= notice %></aside>
<% end %>

<h2>Your Pragmatic Cart</h2>
<ul>
  <% @cart.line_items.each do |item| %>
    <li><%= item.product.title %></li>
  <% end %>
</ul>

And secondly, the notice that a line item was successfully created could look less out of place. Add the following to app/assets/stylesheets/application.scss, before the .content { } bracket:

.notice, #notice {
  background: #ffb;
  border-radius: 0.5em;
  border: solid 0.177em #882;
  color: #882;
  font-weight: bold;
  margin-bottom: 1em;
  padding: 1em 1.414em;
  text-align: center;
}

And then: go shopping in your own online store!

13a. If all works fine, commit the changes to your local git repository, and push them to the github cloud. Surely, there are many questions left open. Formulate one as a comment below, as evidence that you attended this practical.

Written on November 4, 2020 by CS424.