The Simplified Go Food

Web App - Iteration 2, Part 1

The Store

Previously you have finished features to manage food catalogue for your app. Happy with your work, your product manager decides that it's time to let the customer to actually order food from your app.

For this purpose, your product manager asks you to build a new customer-facing page that acts as a store for your foods. In that page, customers can add foods to cart and subsequently process with the order.

The Store Controller

Based on this new requirement, we need a new controller that will only serves purchasing process of customers, not the CRUD of the food. We shall name this controller as StoreController. Now, let's scaffold it.

rails generate controller Store index

Rails will then generate needed skeletons for us, including...

The Store Controller Spec (1)

As mentioned earlier, from this point on we will create our app with TDD in mind. That is, we will write the tests first and write the code later.


Given this requirement, can you write the spec?

- Store's index page will show all the foods, ordered alphabetically by their name

The Store Controller Spec (2)

require 'rails_helper'

describe StoreController do
  describe "GET index" do
    it "returns http success" do
      get :index
      expect(response).to have_http_status(:success)
    end

    it "returns a list of foods, ordered by name" do
      nasi_uduk = create(:food, name: "Nasi Uduk")
      kerak_telor = create(:food, name: "Kelar Telor")
      get :index
      expect(assigns(:foods)).to eq([kerak_telor, nasi_uduk])
    end
  end
end

Rails has provided us with a default spec for our Store controller. We will just modify it a little bit.

Afterward, if you run rspec, it will shows one failed example and that is indeed what we expect.

Modifying The Store Controller

Now we need to modify our StoreController to make it pass the test. This should be easy.

class StoreController < ApplicationController
  def index
    @foods = Food.order(:name)
  end
end

Here, we use ActiveRecord's method "order" to sort our Food model by name. You can see what other options to use for sorting ActiveRecord in this page.

Once you finished with this slight modification, your rspec should be all green again.

Layout and Style

We have passed our unit testing for the StoreController. Now it's time to play a little bit with the look and feel.

Actually, Rails has a built in unit test for view as well. However, in this class we are skipping unit test for view because it's not really the focus of our curriculum. So for this one, let's jump to the code!

Modifying Index

Modify your store's view page for index method as follow:

<p id="notice"><%= notice %></p>
<h1>Your Food Catalog</h1>

<% @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"><%= food.price %></span>
    </div>
  </div>
<% end %>

A little highlight: we use "sanitize" method to safely protect us from cross-site scripting when adding HTML tags in our food's description.

Modifying Routes

Slight modification to our routes:

Rails.application.routes.draw do
  root 'store#index', as: 'store_index'
  resources :foods
  get 'home/hello'
end

Now when you access your http://localhost:3000 it will directly load our store's index page

Modifying Stylesheet

// Place all the styles related to the Store controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.store {
  h1 {
    margin: 0;
    padding-bottom: 0.5em;
    font: 150% sans-serif;
    color: #226;
    border-bottom: 3px dotted #77d;
  }

  /* An entry in our store catalogue */
  .entry {
    overflow: auto;
    margin-top: 1em;
    border-bottom: 1px dotted #77d;
    min-height: 100px;

    img {
      width: 80px;
      margin-right: 5px;
      margin-bottom: 5px;
      position: absolute;
    }

    h3 {
      font-size: 120%;
      font-family: sans-serif;
      margin-left: 100px;
      margin-top: 0;
      margin-bottom: 2px;
      color: #227;
    }

    p, div.price_line {
      margin-left: 100px;
      margin-top: 0.5em;
      margin-bottom: 0.8em;
    }

    .price {
      color: #44a;
      font-weight: bold;
      margin-right: 3em;
    }
  }
}

Application Layout

You may have noticed that our app does not have a proper navigation menu. Therefore we need to create a simple menu that can be seen from every page. To do this we need to change our application layout.

Modifying Application Layout

Change your "app/views/layouts/application.html.erb"

<!DOCTYPE html>
<html>
  <head>
    <title>The Simplified Go Food Web App</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body class="<%= controller.controller_name %>">
    <div id="banner">
      <%= image_tag 'go-food.jpg', alt: "Simplified Go Food" %>
      <span class="title"><%= @page_title %></span>
    </div>
    <div id="columns">
      <div id="side">
        <ul>
          <li><a href="#">Home</a></li>
          <li><a href="#">My Account</a></li>
          <li><a href="#">My Orders</a></li>
        </ul>
      </div>
      <div id="main">
        <%= yield %>
      </div>
    </div>
  </body>
</html>

Modifying Application Stylesheet

We also need to change our application-wide stylesheet.

/*
 *
 *= require_tree .
 *= require_self
 */

body, body > p, body > ol, body > ul, body > td {margin: 8px !important}

#banner {
  position: relative;
  min-height: 40px;
  background: #9c9;
  padding: 10px;
  border-bottom: 2px solid;
  font: small-caps 40px/40px "Times New Roman", serif;
  color: #282;
  text-align: center;

  img {
    position: absolute;
    top: 0;
    left: 0;
    height: 60px;
    width: 192px;
  }
}

#notice:empty {
  display: none;
}

#columns {
  background: #141;
  display: flex;

  #main {
    padding: 1em;
    background: white;
    flex: 1;
  }

  #side {
    padding: 1em 2em;
    background: #141;

    ul {
      padding: 0;

      li {
        list-style: none;

        a {
          color: #bfb;
          font-size: small;
        }
      }
    }
  }
}

@media all and (max-width: 800px) {
  #columns {
    flex-direction: column-reverse;
  }
}

@media all and (max-width: 500px) {
  #banner {
    height: 1em;
  }

  #banner .title {
    display: none;
  }
}

Using Helper to Format Price

Next, we want to change how prices are displayed to provide better experience for our users.

<p id="notice"><%= notice %></p>
<h1>Your Food Catalog</h1>

<% @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: ",") %>    
      </span>
    </div>
  </div>
<% end %>

You can take a look at this page to see what other options available for "number_to_currency" method.

Commit!

Now is a good time to commit and take a look at your progress in your app.

Creating The Cart

Now that we have a proper catalogue, we can start selling the foods on our app. In order to sell things, we need to provide users with cart.

 

In this exercise, a cart is just a placeholder to temporarily store all items that a user wants to buy. A cart, however, has a unique attribute: it should be easy to recover whenever a user open it with the same session.

 

Now, let's try to build it!

Scaffolding The Cart

Since it's just a placeholder with an id and no other attributes, we don't have much to do with our Cart MVC. Therefore, we just scaffold it and then migrate the database.

rails generate scaffold Cart
rails db:migrate

Concerning The Cart (1)

We mentioned about making our cart easy to recover from the same browser session. To do this, we need to ensure for every browser session, there is exactly only one cart assigned. We are going to implement this using Rails' "concerns".

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

Store it in "app/controllers/concerns/current_cart.rb"

Concerning The Cart (2)

Concern is a tool provided by Rails that enables us to include modules in our model or controller classes.

 

In our "current_cart" concern, we define just one method: "set_cart". Its job is to find a cart in current browser session. In the case that no cart is found, it creates a new cart and store its id in "session[:cart_id]" parameter.

Adding Foods to Cart

Once we have our cart set, we are ready to add foods to our cart. To do this, we will create a functioning MVC for a model named "LineItem". Let's try to build it from scratch in TDD way.

 

First, we define LineItem's requirements:

  1. Every LineItem stores data about a Food and a Cart
  2. If a Cart is deleted, all LineItem in it will be deleted too
  3. While LineItem is in a Cart, the Food it's referencing to can't be deleted

Model Specs for LineItem

We start from a basic spec:

require 'rails_helper'

describe LineItem do
  it "has a valid factory" do
    expect(build(:line_item)).to be_valid
  end
end

Run "rspec spec/models/line_item_spec.rb" in your console and watch it fails.

Follow The Message

Our failed test tells us one thing though: we haven't created a class named LineItem. Let's create an empty LineItem model. By now you should already know where to put this file.

class LineItem < ApplicationRecord
end

When we run "rspec spec/models/line_item_spec.rb" again, now it tells us that we don't have a factory for line_item yet.

LineItem Factory

Write the factory. This time, though, you should notice something different:

FactoryGirl.define do
  factory :line_item do
    association :food
    association :cart
  end
end

We use "association" to tell FactoryGirl to create a new Food and Cart on the fly for this LineItem to belongs to.

Migration

When we run rspec again, it tells us that there is no table named "line_items". We need to create the migration file.

rails generate migration CreateLineItem food:references cart:belongs_to
# Please note that "references" and "belongs_to" are actually the same thing
# We use both in this example to introduce you to both to enrich your vocabulary

Take a look at the generated migration file and then execute "rails db:migrate" command. After you finish, take a look at your "db/schema.rb" file to see the result.

Belongs to

When we run rspec again, it tells us that there is no method named "food=" in our LineItem model. We need to add this in our model:

class LineItem < ApplicationRecord
  belongs_to :food
  belongs_to :cart
end

After this little modification, our first spec for LineItem model should get the green light.

Commit!

Now is a good time to commit.

Modifying Model Specs for Cart

To comply with our requirements, we need to add new spec to our Cart model: when it is deleted, all LineItem(s) associated with it should also be deleted.

require 'rails_helper'

describe Cart do
  it "has a valid factory" do
    expect(build(:cart)).to be_valid
  end

  it "deletes line_items when deleted" do
    cart = create(:cart)
    food1 = create(:food)
    food2 = create(:food)
    
    line_item1 = create(:line_item, cart: cart, food: food1)
    line_item2 = create(:line_item, cart: cart, food: food2)
    cart.line_items << line_item1
    cart.line_items << line_item2


    expect { cart.destroy }.to change { LineItem.count }.by(-2)
  end
end

Modifying Cart

Now when we run "rspec spec/models/cart_spec.rb", we will get an error message "undefined method 'line_items' for Cart". We need to modify our Cart model.

class Cart < ApplicationRecord
  has_many :line_items, dependent: :destroy
end

Adding "has_many :line_items" directive will tell enable us to call "line_items" from our Cart, while parameter "dependent: :destroy" is a way to tell Cart model to destroy all line_items that are associated with it if it is destroyed.

Your command "rspec spec/models/cart_spec.rb" should gives you green light now.

Modifying Model Specs for Food (1)

To comply with our requirements, we need to add new spec to our Food model: when a LineItem is in cart, associated Food can't be deleted.

 

Based on our spec for Cart earlier, can you try to write this new spec?

Modifying Model Specs for Food (2)

This is how your additional spec for Food look like:

require 'rails_helper'

describe Food do
  # -cut-
  it "can't be destroyed while it has line_item(s)" do
    cart = create(:cart)
    food = create(:food)
    
    line_item = create(:line_item, cart: cart, food: food)
    food.line_items << line_item

    expect { food.destroy }.not_to change(Food, :count)
  end
end

Run "rspec spec/models/food_spec.rb" and watch it fails again, but this time for something else.

Modifying Food

As with our test with Cart before, RSpec tells us that there is no method called "line_items" in our Food model. Therefore we will modify our Food model accordingly.

class Food < ApplicationRecord
  has_many :line_items
end

When we run "rspec spec/models/food_spec.rb" it will still fail, but for another reason.

Callback

You are now to be introduced with a "callback". In Rails, a callback allows you to trigger logic before or after an alteration of an object's state.

Here, we are going to write a callback that check if a Food still has any LineItem(s) whenever it is about to be destroyed. If it does, the callback prevent the action from being executed.

class Food < ApplicationRecord
  # -cut-
  before_destroy :ensure_not_referenced_by_any_line_item

  # -cut-
  private
    def ensure_not_referenced_by_any_line_item
      unless line_items.empty?
        errors.add(:base, 'Line Items present')
        throw :abort
      end
    end
end

Commit!

Now you have made all your model specs turn green. It is a good time to commit.

What We Just Learned

  1. Generating a controller without scaffolding
  2. Modifying a controller's views
  3. Modifying application layout
  4. Creating a concern
  5. Generating a model without scaffolding
  6. Creating factory with association
  7. Following TDD to construct a controller and a model from scratch to finally pass all the required tests

What We Will Learn Next

Unfortunately though, we still can not see our cart in action because we have not finished all the required pieces. But you do not need to worry because tomorrow we will finish our cart and make it works!

The Simplified Go Food Web App - Iteration 2, Part 1

By qblfrb

The Simplified Go Food Web App - Iteration 2, Part 1

  • 357