The Simplified Go Food
Web App - Iteration 2, Part 2
Where Were We...
Yesterday you have developed:
- StoreController without using scaffold
- CartController
- CurrentCart concern
- LineItem model without using scaffold
- LineItemsController by using TDD from scratch
Adding The Button
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 %>
Modifying The Stylesheet
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 */
}
}
Requirements
We are going to create our LineItems controller and modify our Carts controller. Let's start with the requirements:
- There should be a "add to cart" button in every food
- When clicked
- a cart is created if there is no cart already
- selected food is then added to the cart
- Afterward, user is redirected to the cart page
- In the cart page, user can see list of items already inserted to the cart
Based on these requirements, write (or modify) only necessary controller specs for LineItems and Carts controller.
LineItems Controller Specs
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
Creating LineItems Controller
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.
Detailed Specs (1)
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'
Detailed Specs (2)
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.
Detailed Specs (3)
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.
Detailed Specs (4)
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.
Detailed Specs (5)
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".
Detailed Specs (6)
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.
Make Them Pass
RSpec now shows a lot of errors when we run it. Let's try to make the red turn to green one by one!
Including Concern
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.
"Create" Method (1)
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
"Create" Method (2)
Explained:
- We find food object with the same id as the one sent via "params[:food_id]"
- Since the "add to cart" button only sends "params[:food_id]", we need to build our line_items with the line "@cart.line_items.build(food: food)"
- If saving "@line_item" is successful, we redirect it to cart's show page
Before Action (1)
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
Before Action (2)
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.
Modifying Show Cart View
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>
Commit!
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.
Smarter Cart
Requirements
Your product manager is happy with your progress so far. However, he wants to an improvement to your app. Here is his requirements:
- Currently, if we add a food that is already in our cart, it will be displayed more than once in the cart page. We want to change this by displaying the quantity of items for every food in the cart instead.
- The cart page should also display the price of each item in it and the total price of all items.
- Implement an "empty cart" button that will erase every items currently in the cart.
Modifying Cart Model Spec
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
Modifying Cart Model
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
Migrating LineItem (1)
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
Migrating LineItem (2)
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".
Rolling Back
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
Changing The Migration
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.
Commit!
Now if you run your rspec again, you will get all green. Don't forget to commit your code.
Modifying LineItems Controller Spec
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
Modifying LineItems Controler
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
Modifying The Views
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>
Modifying The Stylesheet
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;
}
}
Commit!
Congratulations! You have developed a working cart. Play with your cart a little bit, afterward don't forget to commit your code.
Exercise
In this exercise, you always have to write the spec first before you write your code.
- Create a new method called "total_price" in LineItem model, it will return the calculation of LineItem's quantity times its Product's price
- Create a new method called "total_price" in Cart model, it will return the sum of the total price of its LineItem's
- Create a functioning MVC called "Category" with only one attribute "name", do it with TDD approach and without a scaffold
- Create "empty cart" button, it will use Carts controller's "destroy" method with slight modification: it redirects to Store controller's index after it finishes
The Simplified Go Food Web App - Iteration 2, Part 2
By qblfrb
The Simplified Go Food Web App - Iteration 2, Part 2
- 348