The Simplified Go Food Web App - Iteration 4

What We Learned So Far

In the previous lesson, you have learned to:

  1. Write and use partial templates
  2. Write feature to empty your cart
  3. 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:

  1. Enable updates on foods' price to be broadcasted to every store index page currently opened in users' browser
  2. 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:

  1. 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
  2. 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

You can learn more about Enum here and here.

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:

  1. Add "checkout" button to your cart
  2. When clicked, it will create a new Order
  3. 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
  4. User will then fill his/her name, address, email, and payment type to a new Order form
  5. 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