The Simplified Go Food Web App - Iteration 4
What We Learned So Far
In the previous lesson, you have learned to:
- Write and use partial templates
- Write feature to empty your cart
- Write and use Ajax to update part of your app without reloading theĀ whole page
Action Cable
New Requirements
As usual, your product manager comes with new requests. This time he asks you to write a feature that:
- Enable updates on foods' price to be broadcasted to every store index page currently opened in users' browser
- The broadcast does not update foods' price in users' cart
Generating Channel
To enable updates to be broadcasted, we use Rails' feature called "channel".
rails generate channel foods
Fill the newly generated "app/channels/foods_channel.rb".
class FoodsChannel < ApplicationCable::Channel
def subscribed
stream_from "foods"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Changing Foods Controller
Unfortunately (or fortunately?) for us, RSpec currently does not support testing for ActionCable yet. So we will skip unit testing for this feature and jump right to the code.
class FoodsController < ApplicationController
# -cut-
# PATCH/PUT /foods/1
# PATCH/PUT /foods/1.json
def update
respond_to do |format|
if @food.update(food_params)
format.html { redirect_to @food, notice: 'Food was successfully updated.' }
format.json { render :show, status: :ok, location: @food }
@foods = Food.all
ActionCable.server.broadcast 'foods', html: render_to_string('store/index', layout: false)
else
format.html { render :edit }
format.json { render json: @food.errors, status: :unprocessable_entity }
end
end
end
# -cut-
end
Modifying Foods' Javascript
Lastly, we modify our script for Foods in "app/assets/javascrips/channels/foods.coffee"
App.foods = App.cable.subscriptions.create "FoodsChannel",
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
$(".store #main").html(data.html)
Now whenever you update your foods' price in one window, any open window that shows your Store index page will automatically update the foods' price without having to reload.
Commit!
Now is a good time to commit your progress.
Capturing Order
The Requirements
Now we are going to finish our cart and convert it to an order. Here are the initial requirements:
- You are going to implement Order feature. An Order contains four information: the name, address, and email of the buyer, and the payment type of the order
- Order's payment type is stored as number, with number 0 means cash payment, 1 means Go Pay payment, and 2 means credit card payment
Write The Spec!
Based on the requirements above, write the spec and factory for our Order model.
Order Model Spec (1)
This is how your Order model spec should look like.
require 'rails_helper'
describe Order do
it "has a valid factory" do
expect(build(:order)).to be_valid
end
it "is valid with a name, address, email, and payment_type" do
expect(build(:order)).to be_valid
end
it "is invalid without a name" do
order = build(:order, name: nil)
order.valid?
expect(order.errors[:name]).to include("can't be blank")
end
it "is invalid without an address" do
order = build(:order, address: nil)
order.valid?
expect(order.errors[:address]).to include("can't be blank")
end
# -cut-
end
Order Model Spec (2)
This is how your Order model spec should look like.
require 'rails_helper'
describe Order do
# -cut-
it "is invalid without an email" do
order = build(:order, email: nil)
order.valid?
expect(order.errors[:email]).to include("can't be blank")
end
it "is invalid with email not in valid email format" do
order = build(:order, email: "email")
order.valid?
expect(order.errors[:email]).to include("must be in valid email format")
end
it "is invalid without a payment_type" do
order = build(:order, payment_type: nil)
order.valid?
expect(order.errors[:payment_type]).to include("can't be blank")
end
end
Order Factory
This is how your Order factory should look like.
FactoryGirl.define do
factory :order do
name { Faker::Name.name }
address { Faker::Address.street_address }
email { Faker::Internet.email }
payment_type { Faker::Number.between(0, 2) }
end
factory :invalid_order, parent: :order do
name nil
address nil
email nil
payment_type nil
end
end
Generate Order Model
When you run your rspec for Order model now, you will be told that Order is an uninitialized constant. Let's create our Order model then.
rails generate model Order name address:text email payment_type:integer
When prompted with overwriting specs and factories, answer with "n". Also notice that we did not specify the type of name and email when generating our model. Whenever we do this, Rails will automatically assume the type is string.
Validating Order Model (1)
After you run db:migrate and rspec command, you will see that your validation tests do not work as we intend to yet. Write validations in your Order model to make it pass.
Validating Order Model (2)
Your Order model validation should look like this.
class Order < ApplicationRecord
validates :name, :address, :email, :payment_type, presence: true
validates :email, format: {
with: /.+@.+\..+/i,
message: 'must be in valid email format'
}
end
Enumerating Payment Type
Storing payment type in integer is an efficient way. However, we may need to keep track of which values are used for which payment type. Rails provides us with "enum" to do this.
class Order < ApplicationRecord
enum payment_type: {
"Cash" => 0,
"Go Pay" => 1,
"Credit Card" => 2
}
# -cut-
validates :payment_type, inclusion: payment_types.keys
end
Modifying Order Model Spec
Using enum will change our Order model spec and its factory.
describe Order do
# -cut-
it "is invalid with wrong payment_type" do
expect{ build(:order, payment_type: "Grab Pay") }.to raise_error(ArgumentError)
end
end
And this would be your modified factory.
FactoryGirl.define do
factory :order do
name { Faker::Name.name }
address { Faker::Address.street_address }
email { Faker::Internet.email }
payment_type "Cash"
end
# -cut-
end
More Requirements
These are the requirements for your Orders controller:
- Add "checkout" button to your cart
- When clicked, it will create a new Order
- When a new Order is created, it will ensure that the Cart is not empty,
- if Cart is empty, app will redirect user to Store index
- if Cart is not empty, app will proceed the order
- User will then fill his/her name, address, email, and payment type to a new Order form
- When the form is submitted, app will add LineItems from the Cart to the Order and then destroy current session's Cart
Write The Spec!
As usual, we deal with our view later. First, based on previous requirements, we will write the spec for Order model and Orders controller.
Order Model Spec
Your Order model spec should look like this.
describe Order do
# -cut-
describe "adding line_items from cart" do
before :each do
@cart = create(:cart)
@line_item = create(:line_item, cart: @cart)
@order = build(:order)
end
it "add line_items to order" do
expect{
@order.add_line_items(@cart)
@order.save
}.to change(@order.line_items, :count).by(1)
end
it "removes line_items from cart" do
expect{
@order.add_line_items(@cart)
@order.save
}.to change(@cart.line_items, :count).by(-1)
end
end
end
Make It Pass!
When we run our Order model spec, it will ask us to define a method called "add_line_items". Write this method!
Add Lines from Cart
This is how your Order model should look like.
class Order < ApplicationRecord
has_many :line_items, dependent: :destroy
# -cut-
def add_line_items(cart)
cart.line_items.each do |item|
item.cart_id = nil
line_items << item
end
end
end
Modifying LineItem Model
When modifying your Order model, you should realize that your LineItem now should belongs to Order too. Also note that when a LineItem is associated with a Cart, it does not associate with Order, and vice versa.
class LineItem < ApplicationRecord
belongs_to :food
belongs_to :cart, optional: true
belongs_to :order, optional: true
def total_price
quantity * food.price
end
end
Adding Order ID to LineItem
And of course don't forget to add Order ID to LineItem.
rails generate migration AddOrderIdToLineItem order:references
Now run rails db:migrate and your rspec again, your model specs should all pass.
Commit!
Now is a good time to commit your progress.
Orders Controller Spec (1)
require 'rails_helper'
describe OrdersController do
it "includes CurrentCart" do
expect(OrdersController.ancestors.include? CurrentCart).to eq(true)
end
# -cut-
end
Orders Controller Spec (2)
describe OrdersController do
describe 'GET #index' do
it "populates an array of all orders" do
order1 = create(:order, name: "Buyer 1")
order2 = create(:order, name: "Buyer 2")
get :index
expect(assigns(:orders)).to match_array([order1, order2])
end
it "renders the :index template" do
get :index
expect(response).to render_template :index
end
end
describe 'GET #show' do
it "assigns the requested order to @order" do
order = create(:order)
get :show, params: { id: order }
expect(assigns(:order)).to eq order
end
it "renders the :show template" do
order = create(:order)
get :show, params: { id: order }
expect(response).to render_template :show
end
end
# -cut-
end
Orders Controller Spec (3)
require 'rails_helper'
describe OrdersController do
# -cut-
describe 'GET #new' do
context "with non-empty cart" do
before :each do
@cart = create(:cart)
session[:cart_id] = @cart.id
@line_item = create(:line_item, cart: @cart)
end
it "assigns a new Order to @order" do
get :new
expect(assigns(:order)).to be_a_new(Order)
end
it "renders the :new template" do
get :new
expect(:response).to render_template :new
end
end
# -cut-
end
# -cut-
end
Orders Controller Spec (4)
require 'rails_helper'
describe OrdersController do
# -cut-
describe 'GET #new' do
# -cut-
context "with empty cart" do
before :each do
@cart = create(:cart)
session[:cart_id] = @cart.id
end
it "redirects to the store index page" do
get :new
expect(:response).to redirect_to store_index_url
end
end
end
# -cut-
end
Orders Controller Spec (5)
require 'rails_helper'
describe OrdersController do
# -cut-
describe 'GET #edit' do
it "assigns the requested order to @order" do
order = create(:order)
get :edit, params: { id: order }
expect(assigns(:order)).to eq order
end
it "renders the :edit template" do
order = create(:order)
get :edit, params: { id: order }
expect(response).to render_template :edit
end
end
# -cut-
end
Orders Controller Spec (6)
describe OrdersController do
# -cut-
describe 'POST #create' do
context "with valid attributes" do
it "saves the new order in the database" do
expect{
post :create, params: { order: attributes_for(:order) }
}.to change(Order, :count).by(1)
end
it "destroys session's cart" do
expect{
post :create, params: { order: attributes_for(:order) }
}.to change(Cart, :count).by(-1)
end
it "removes the cart from session's params" do
post :create, params: { order: attributes_for(:order) }
expect(session[:cart_id]).to eq(nil)
end
it "redirects to store index page" do
post :create, params: { order: attributes_for(:order) }
expect(response).to redirect_to store_index_url
end
end
# -cut-
end
# -cut-
end
Orders Controller Spec (7)
describe OrdersController do
# -cut-
describe 'POST #create' do
# -cut-
context "with invalid attributes" do
it "does not save the new order in the database" do
expect{
post :create, params: { order: attributes_for(:invalid_order) }
}.not_to change(Order, :count)
end
it "re-renders the :new template" do
post :create, params: { order: attributes_for(:invalid_order) }
expect(response).to render_template :new
end
end
end
# -cut-
end
Orders Controller Spec (8)
describe OrdersController do
# -cut-
describe 'PATCH #update' do
before :each do
@order = create(:order)
end
context "with valid attributes" do
it "locates the requested @order" do
patch :update, params: { id: @order, order: attributes_for(:order) }
expect(assigns(:order)).to eq @order
end
it "changes @order's attributes" do
patch :update, params: { id: @order, order: attributes_for(:order, name: 'Nasi Uduk') }
@order.reload
expect(@order.name).to eq('Nasi Uduk')
end
it "redirects to the order" do
patch :update, params: { id: @order, order: attributes_for(:order) }
expect(response).to redirect_to @order
end
end
# -cut-
end
# -cut-
end
Orders Controller Spec (9)
require 'rails_helper'
describe OrdersController do
# -cut-
describe 'PATCH #update' do
# -cut-
context "with invalid attributes" do
it "does not update the order in the database" do
patch :update, params: { id: @order, order: attributes_for(:order, name: 'Nasi Uduk', description: nil) }
@order.reload
expect(@order.name).not_to eq('Nasi Uduk')
end
it "re-renders the :edit template" do
patch :update, params: { id: @order, order: attributes_for(:invalid_order) }
expect(response).to render_template :edit
end
end
end
# -cut-
end
Orders Controller Spec (10)
require 'rails_helper'
describe OrdersController do
# -cut-
describe 'DELETE #destroy' do
before :each do
@order = create(:order)
end
it "deletes the order from the database" do
expect{
delete :destroy, params: { id: @order }
}.to change(Order, :count).by(-1)
end
it "redirects to orders#index" do
delete :destroy, params: { id: @order }
expect(response).to redirect_to orders_url
end
end
end
Solutions - The Simplified Go Food Web App - Iteration 4
By qblfrb
Solutions - The Simplified Go Food Web App - Iteration 4
- 281