Yesterday you have developed:
Let's start by putting "add to cart" button to our view.
<!-- cut -->
<% @foods.each do |food| %>
<div class="entry">
<%= image_tag(food.image_url) %>
<h3><%= food.name %></h3>
<%= sanitize(food.description) %>
<div class="price_line">
<span class="price">
<%= number_to_currency(food.price, unit: "Rp ", delimiter: ".", separator: ",") %>
<%= button_to 'Add to Cart', line_items_path(food_id: food) %>
</span>
</div>
</div>
<% end %>
Above we use a helper method called "button_to".
.store {
/* cut */
.entry {
/* cut */
p, div.price_line {
margin-left: 100px;
margin-top: 0.5em;
margin-bottom: 0.8em;
form, div {
display: inline
}
}
/* cut */
}
}
We are going to create our LineItems controller and modify our Carts controller. Let's start with the requirements:
Based on these requirements, write (or modify) only necessary controller specs for LineItems and Carts controller.
This is how your LineItems controller specs should look like. We only write specs for "create" method because that's all we need for the time being.
require 'rails_helper'
describe LineItemsController do
describe 'POST #create' do
it "includes CurrentCart"
context "with existing cart" do
it "does not create a new cart before saving a new line_item"
end
context "without existing cart" do
it "creates a new cart before saving new line_item"
end
it "saves the new line_item in the database"
it "redirects to carts#show"
end
end
When we run rspec for our LineItems controller spec, it will tell us that LineItemsController does not exist. Let's create it.
class LineItemsController < ApplicationController
end
Rails.application.routes.draw do
# -cut-
resources :line_items
# -cut-
end
When creating a controller, don't forget to add its route too.
If we run our spec for LineItems controller, now it says that all examples are pending. Let's implement it one by one.
# -cut-
describe LineItemsController do
describe 'POST #create' do
before :each do
@food = create(:food)
end
# -cut-
end
end
First, we create a class variable @food that we will use for our entire specs for 'POST #create'
Next, we write detailed example for including CurrentCart module.
# -cut-
describe LineItemsController do
describe 'POST #create' do
# -cut-
it "includes CurrentCart" do
expect(LineItemsController.ancestors.include? CurrentCart).to eq(true)
end
# -cut-
end
end
Here, we just make sure that our LineItemsController has CurrentCart as one of its ancestors.
Next, we write detailed example for a scenario when there is already a cart in the browser session.
# -cut-
describe LineItemsController do
describe 'POST #create' do
# -cut-
context "with existing cart" do
it "does not create a new cart before saving new line_item" do
cart = create(:cart)
session[:cart_id] = cart.id
expect{
post :create, params: { food_id: @food.id }
}.not_to change(Cart, :count)
end
end
# -cut-
end
end
Here, we create our cart with FactoryGirl and assign its id to session[:cart_id]. We expect that when a new line_item is created, no new cart is created hence the total number of Cart does not change.
Next, we write detailed example for a scenario when there is no cart already in the browser session.
# -cut-
describe LineItemsController do
describe 'POST #create' do
# -cut-
context "without existing cart" do
it "creates a new cart before saving new line_item" do
expect{
post :create, params: { food_id: @food.id }
}.to change(Cart, :count).by(1)
end
end
# -cut-
end
end
Here, we just make sure that a new cart is created, indicated by the change in the number of Cart.
Next, we write detailed example for saving the new line_item to the database.
# -cut-
describe LineItemsController do
describe 'POST #create' do
# -cut-
it "saves the new line_item in the database" do
expect{
post :create, params: { food_id: @food.id }
}.to change(LineItem, :count).by(1)
end
# -cut-
end
end
Here, we just make sure that a new line_item is created, indicated by the change in the number of LineItem.
Also note that we do not use attributes_for because our "add to cart" button only pass one parameter that is "product_id".
Next, we write detailed example for the redirect process after the saving process finished.
# -cut-
describe LineItemsController do
describe 'POST #create' do
# -cut-
it "redirects to carts#show" do
post :create, params: { food_id: @food.id }
expect(response).to redirect_to(cart_path(assigns(:line_item).cart))
end
end
end
Different from our specs for Foods controller, here we do not redirect our response to line_item show page. We redirect it to the cart page instead.
We use "assigns(:line_item)" to get the newly created line_item object.
RSpec now shows a lot of errors when we run it. Let's try to make the red turn to green one by one!
For our first example, RSpec tells us that CurrentCart is not included yet in our LineItems controller. Include it.
class LineItemsController < ApplicationController
include CurrentCart
end
Our first example still do not pass after this addition. It tells us that there is no "create" method. Let's make one.
For our first example, RSpec tells us that CurrentCart is not included yet in our LineItems controller. Include it.
class LineItemsController < ApplicationController
include CurrentCart
def create
food = Food.find(params[:food_id])
@line_item = @cart.line_items.build(food: food)
respond_to do |format|
if @line_item.save
format.html { redirect_to @line_item.cart, notice: 'Line item was successfully created.' }
format.json { render :show, status: :created, location: @line_item }
else
format.html { render :new }
format.json { render json: @line_item.errors, status: :unprocessable_entity }
end
end
end
end
Explained:
After the latest addition to your LineItems controller, it still does not go green. Instance variable "@cart" still returns nil. That's because we have not really called CurrentCart's "set_cart". We need to call this method whenever we are about to create a line_item.
class LineItemsController < ApplicationController
include CurrentCart
before_action :set_cart, only: [:create]
# -cut-
end
Above, we use one of Action Controller's filter called "before_action" to call "set_cart" before we execute "create" method in LineItems controller.
To find out what other filters you can use in your controllers, take a look at this page.
As per requirements, our cart's show page should display all line_item(s) inserted to it. Since we don't cover unit testing for view, let's jump to the code
<p id="notice"><%= notice %></p>
<h2>Your Cart</h2>
<ul>
<% @cart.line_items.each do |item| %>
<li><%= item.food.name %></li>
<% end %>
</ul>
Now if you run your rspec again, you will get all green. You can try your current progress in the browser. Afterward, don't forget to commit your code.
Your product manager is happy with your progress so far. However, he wants to an improvement to your app. Here is his requirements:
First, we change our Cart model spec to accommodate the new requirements.
describe Cart do
# -cut-
it "does not change the number of line_item if the same food is added" do
cart = create(:cart)
food = create(:food, name: "Nasi Uduk")
line_item = create(:line_item, food: food, cart: cart)
expect { cart.add_food(food) }.not_to change(LineItem, :count)
end
it "increments the quantity of line_item if the same food is added" do
cart = create(:cart)
food = create(:food, name: "Nasi Uduk")
line_item = create(:line_item, food: food, cart: cart)
expect { cart.add_food(food) }.to change { line_item.quantity }.by(1)
end
end
RSpec tells us that we don't have a method named "add_food" in our Cart model. Let's write this method.
class Cart < ApplicationRecord
has_many :line_items, dependent: :destroy
def add_food(food)
current_item = line_items.find_by(food_id: food.id)
if current_item
current_item.quantity += 1
else
current_item = line_items.build(food_id: food.id)
end
current_item
end
end
RSpec then tells us that we don't have a method "quantity" in our LineItem. Since quantity is something that we want to store in our database, we need to change our line_items table. To do this, we use migration.
Run the following command in your console.
rails generate migration add_quantity_to_line_items quantity:integer
Take a look at the generated file, and you should see a migration file like this:
class AddQuantityToLineItems < ActiveRecord::Migration[5.0]
def change
add_column :line_items, :quantity, :integer
end
end
You can read what else we can do with ActiveRecord migration in this page. To apply this migration, run "rails db:migrate".
When you run the rspec command now, you will see a message saying "undefined method '+' for nil:NilClass". That's because when we create a new "line_item", its quantity is nil.
We need to change this. We need to make sure that everytime a new "line_item" is newly created, its quantity is set to 1.
To do that, we need to rollback our migration first.
rails db:rollback
Then we change our migration file to make sure the default value of line_item's quantity is 1.
class AddQuantityToLineItems < ActiveRecord::Migration[5.0]
def change
add_column :line_items, :quantity, :integer, null: false, default: 1
end
end
Execute the command "rails db:migrate" again after that.
Now if you run your rspec again, you will get all green. Don't forget to commit your code.
We also need to change our LineItems controller spec to follow the new requirements. Can you write the spec modification?
describe LineItemsController do
describe 'POST #create' do
# -cut-
context "with existing line_item with the same food" do
before :each do
cart = create(:cart)
session[:cart_id] = cart.id
line_item = create(:line_item, food: @food, cart: cart)
end
it "does not save the new line_item in the database" do
expect{
post :create, params: { food_id: @food.id }
}.not_to change(LineItem, :count)
end
it "increments the quantity of line_item with the same food" do
post :create, params: { food_id: @food.id }
expect(assigns(:line_item).quantity).to eq(2)
end
end
context "without existing line_item with the same food" do
it "saves the new line_item in the database" do
expect{
post :create, params: { food_id: @food.id }
}.to change(LineItem, :count).by(1)
end
end
# -cut-
end
# -cut-
end
We need just a slight modification in our LineItems controller:
class LineItemsController < ApplicationController
include CurrentCart
before_action :set_cart, only: [:create]
def create
food = Food.find(params[:food_id])
@line_item = @cart.add_food(food)
respond_to do |format|
if @line_item.save
format.html { redirect_to @line_item.cart, notice: 'Line item was successfully created.' }
format.json { render :show, status: :created, location: @line_item }
else
format.html { render :new }
format.json { render json: @line_item.errors, status: :unprocessable_entity }
end
end
end
end
Slight modification to our view:
<p id="notice"><%= notice %></p>
<h2>Your Cart</h2>
<table>
<% total_price = 0.0 %>
<% @cart.line_items.each do |item| %>
<tr>
<td><%= item.quantity %> ×</td>
<td><%= item.food.name %></td>
<td class="item_price"><%= number_to_currency(item.quantity * item.food.price, unit: "Rp ", delimiter: ".", separator: ",") %></td>
</tr>
<% total_price += (item.quantity * item.food.price) %>
<% end %>
<tr class="total_line">
<td colspan="2">Total</td>
<td class="total_cell"><%= number_to_currency(total_price, unit: "Rp ", delimiter: ".", separator: ",") %></td>
</tr>
</table>
Fill in a stylesheet for your carts:
.carts {
.item_price, .total_line {
text-align: right;
}
.total_line, .total_cell {
font-weight: bold;
border-top: 1px solid #595;
}
}
Congratulations! You have developed a working cart. Play with your cart a little bit, afterward don't forget to commit your code.
In this exercise, you always have to write the spec first before you write your code.