[Solutions]

The Simplified Go Food

Web App - Iteration 2

Fixing Cart Model Spec (1)

To ensure consistent test results, apply this fix to your Cart model spec:

describe Cart do
  # -cut-
  context "with existing line_item with the same food" do
    before :each do
      @cart = create(:cart)
      @food = create(:food)
      @line_item = create(:line_item, food: @food, cart: @cart)
    end
    
    it "does not save the new line_item in the database" do
      expect { @cart.add_food(@food).save }.not_to change(LineItem, :count)
    end

    it "increments the quantity of line_item with the same food" do
      expect { @cart.add_food(@food).save }.to change {
        @cart.line_items.find_by(food_id: @food.id).quantity
      }.by(1)
    end
  end
  # -cut-
end

Fixing Cart Model Spec (2)

To ensure consistent test results, apply this fix to your Cart model spec:

describe Cart do
  # -cut-
  context "without existing line_item with the same food" do
    it "saves the new line_item in the database" do
      cart = create(:cart)
      food = create(:food)
      expect { cart.add_food(food).save }.to change(LineItem, :count).by(1)
    end
  end
  # -cut-
end

Fixing LineItems Controller Spec (1)

To ensure consistent test results, apply this fix to your LineItems controller spec:

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
        expect {
          post :create, params: { food_id: @food.id }
        }.to change {
          @cart.line_items.find_by(food_id: @food.id).quantity
        }.by(1)
      end
    end
    # -cut-
  end
end

Fixing LineItems Controller Spec (2)

To ensure consistent test results, apply this fix to your LineItems controller spec:

describe LineItemsController do
  describe 'POST #create' do
    # -cut
    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
end

Spec for LineItem's Total Price

This is how LineItem's total_price spec should look like.

describe LineItem do
  # -cut-
  it "can calculate total_price" do
    cart = create(:cart)
    food = create(:food, price: 10000.0)
    line_item = create(:line_item, quantity: 3, food: food, cart: cart)

    expect(line_item.total_price).to eq(30000.0)
  end
end

Spec for Cart's Total Price

This is how Cart's total_price spec should look like.

describe Cart do
  # -cut-
  it "can calculate total_price" do
    cart = create(:cart)
    food1 = create(:food, name: "Food 1", price: 10000.0)
    food2 = create(:food, name: "Food 2", price: 15000.0)
    line_item1 = create(:line_item, quantity: 3, food: food1, cart: cart)
    line_item2 = create(:line_item, quantity: 1, food: food2, cart: cart)

    expect(cart.total_price).to eq(45000.0)
  end
end

Spec for Category Model

This is how Category model spec should look like.

# -cut-
describe Category do
  it "has a valid factory" do
    expect(build(:category)).to be_valid
  end

  it "is valid with a name" do
    expect(build(:category)).to be_valid
  end

  it "is invalid without a name" do
    category = build(:category, name: nil)
    category.valid?
    expect(category.errors[:name]).to include("can't be blank")
  end

  it "is invalid with a duplicate name" do
    category1 = create(:category, name: "Dessert")
    category2 = build(:category, name: "Dessert")

    category2.valid?
    expect(category2.errors[:name]).to include("has already been taken")
  end
end

Factory for Category

This is how Category factory should look like.

FactoryGirl.define do
  factory :category do
    name { Faker::Dessert.variety } 
    # This could be any faker, though
  end

  factory :invalid_category, parent: :category do
    name nil
  end
end

Factory for Food

This is how Food factory should look like.

FactoryGirl.define do
  factory :food do
    name { Faker::Food.dish }
    description { Faker::Food.ingredient }
    image_url "Food.jpg"
    price 10000.0

    association :category
  end
  # -cut-
end

Spec for Categories Controller (1)

This is how Categories controller spec should look like.

require 'rails_helper'

describe CategoriesController do
  describe 'GET #index' do
    it "populates an array of all categories" do 
      dessert = create(:category, name: "Dessert")
      main_course = create(:category, name: "Main Course")
      get :index
      expect(assigns(:categories)).to match_array([dessert, main_course])
    end

    it "renders the :index template" do
      get :index
      expect(response).to render_template :index
    end
  end
  # -cut-
end

Spec for Categories Controller (2)

This is how Categories controller spec should look like.

require 'rails_helper'

describe CategoriesController do
  # -cut-
  describe 'GET #show' do
    it "assigns the requested category to @category" do
      category = create(:category)
      get :show, params: { id: category }
      expect(assigns(:category)).to eq category
    end

    it "populates a list of all foods in the category" do
      category = create(:category)
      food1 = create(:food, category: category)
      food2 = create(:food, category: category)
      get :show, params: { id: category }
      expect(assigns(:category).foods).to match_array([food1, food2])
    end

    it "renders the :show template" do
      category = create(:category)
      get :show, params: { id: category }
      expect(response).to render_template :show
    end
  end
  # -cut-
end

Spec for Categories Controller (3)

This is how Categories controller spec should look like.

require 'rails_helper'

describe CategoriesController do
  # -cut-
  describe 'GET #new' do
    it "assigns a new Category to @category" do
      get :new
      expect(assigns(:category)).to be_a_new(Category)
    end

    it "renders the :new template" do
      get :new
      expect(:response).to render_template :new
    end
  end
  # -cut-
end

Spec for Categories Controller (4)

This is how Categories controller spec should look like.

require 'rails_helper'

describe CategoriesController do
  # -cut-
  describe 'GET #edit' do
    it "assigns the requested category to @category" do
      category = create(:category)
      get :edit, params: { id: category }
      expect(assigns(:category)).to eq category
    end

    it "renders the :edit template" do
      category = create(:category)
      get :edit, params: { id: category }
      expect(response).to render_template :edit
    end
  end
  # -cut-
end

Spec for Categories Controller (5)

# -cut-
describe CategoriesController do
  # -cut-
  describe 'POST #create' do
    context "with valid attributes" do
      it "saves the new category in the database" do
        expect{
          post :create, params: { category: attributes_for(:category) }
        }.to change(Category, :count).by(1)
      end

      it "redirects to categories#show" do
        post :create, params: { category: attributes_for(:category) }
        expect(response).to redirect_to(category_path(assigns[:category]))
      end
    end

    context "with invalid attributes" do
      it "does not save the new category in the database" do
        expect{
          post :create, params: { category: attributes_for(:invalid_category) }
        }.not_to change(Category, :count)
      end

      it "re-renders the :new template" do
        post :create, params: { category: attributes_for(:invalid_category) }
        expect(response).to render_template :new
      end
    end
  end
  # -cut-
end

Spec for Categories Controller (6)

# -cut-
describe CategoriesController do
  # -cut-
  describe 'PATCH #update' do
    before :each do
      @category = create(:category)
    end

    context "with valid attributes" do
      it "locates the requested @category" do
        patch :update, params: { id: @category, category: attributes_for(:category) }
        expect(assigns(:category)).to eq @category
      end

      it "changes @category's attributes" do
        patch :update, params: { id: @category, category: attributes_for(:category, name: 'Dessert') }
        @category.reload
        expect(@category.name).to eq('Dessert')
      end

      it "redirects to the category" do
        patch :update, params: { id: @category, category: attributes_for(:category) }
        expect(response).to redirect_to @category
      end
    end
    # -cut-
  end
  # -cut-
end

Spec for Categories Controller (7)

# -cut-
describe CategoriesController do
  # -cut-
  describe 'PATCH #update' do
    # -cut-
    context "with invalid attributes" do
      it "does not update the category in the database" do
        patch :update, params: { id: @category, category: attributes_for(:category, name: nil) }
        @category.reload
        expect(@category.name).not_to eq(nil)
      end

      it "re-renders the :edit template" do
        patch :update, params: { id: @category, category: attributes_for(:invalid_category) }
        expect(response).to render_template :edit
      end
    end
  end
  # -cut-
end

Spec for Categories Controller (8)

# -cut-
describe CategoriesController do
  # -cut-
  describe 'DELETE #destroy' do
    before :each do
      @category = create(:category)
    end

    context "with associated foods" do
      it "does not delete the category from the database" do
        food = create(:food, category: @category)
        expect{
          delete :destroy, params: { id: @category }
        }.not_to change(Category, :count)
      end
    end

    context "without associated foods" do
      it "deletes the category from the database" do
        expect{
          delete :destroy, params: { id: @category }
        }.to change(Category, :count).by(-1)
      end

      it "redirects to categories#index" do
        delete :destroy, params: { id: @category }
        expect(response).to redirect_to categories_url
      end
    end
  end
  # -cut-
end

Generating Model

You can use Rails to generate your Category model.

rails generate model Category name:string

Remember to not overwrite your Category model spec. And remember to delete categories spec generated by Rails.

Adding Column to Food

Don't forget to modify our Food table to include category_id.

rails generate migration AddCategoryIdToFood category_id:integer

Then run "rails db:migrate".

Filling Category Model

Adding association, validation, and callback to Category model.

class Category < ApplicationRecord
  has_many :foods
  validates :name, presence: true, uniqueness: true

  before_destroy :ensure_not_referenced_by_any_food

  private
    def ensure_not_referenced_by_any_food
      unless foods.empty?
        errors.add(:base, 'Foods present')
        throw :abort
      end
    end
end

Modifying Food Model

Adding association with Category to Food model.

class Food < ApplicationRecord
  belongs_to :category
  # -cut-
end

You Shall Pass

Run your model specs and see it pass.

Creating Categories Controller (1)

This is how your Categories controller should look like.

class CategoriesController < ApplicationController
  before_action :set_category, only: [:show, :edit, :update, :destroy]

  # GET /categories
  # GET /categories.json
  def index
    @categories = Category.all
  end

  # GET /categories/1
  # GET /categories/1.json
  def show
  end

  # GET /categories/new
  def new
    @category = Category.new
  end

  # GET /categories/1/edit
  def edit
  end

  # -cut-
end

Creating Categories Controller (2)

class CategoriesController < ApplicationController
  # -cut-

  # POST /categories
  # POST /categories.json
  def create
    @category = Category.new(category_params)

    respond_to do |format|
      if @category.save
        format.html { redirect_to @category, notice: 'Category was successfully created.' }
        format.json { render :show, status: :created, location: @category }
      else
        format.html { render :new }
        format.json { render json: @category.errors, status: :unprocessable_entity }
      end
    end
  end
  # -cut-
end

Creating Categories Controller (3)

This is how your Categories controller should look like.

class CategoriesController < ApplicationController
  # -cut-

  # PATCH/PUT /categories/1
  # PATCH/PUT /categories/1.json
  def update
    respond_to do |format|
      if @category.update(category_params)
        format.html { redirect_to @category, notice: 'Category was successfully updated.' }
        format.json { render :show, status: :ok, location: @category }
      else
        format.html { render :edit }
        format.json { render json: @category.errors, status: :unprocessable_entity }
      end
    end
  end

  # -cut-
end

Creating Categories Controller (4)

This is how your Categories controller should look like.

class CategoriesController < ApplicationController
  # -cut-

  # DELETE /categories/1
  # DELETE /categories/1.json
  def destroy
    @category.destroy
    respond_to do |format|
      format.html { redirect_to categories_url, notice: 'Category was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_category
      @category = Category.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def category_params
      params.require(:category).permit(:name)
    end
end

Creating Categories Controller (4)

This is how your Categories controller should look like.

class CategoriesController < ApplicationController
  # -cut-

  # DELETE /categories/1
  # DELETE /categories/1.json
  def destroy
    @category.destroy
    respond_to do |format|
      format.html { redirect_to categories_url, notice: 'Category was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_category
      @category = Category.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def category_params
      params.require(:category).permit(:name)
    end
end

You Shall Pass

Run your controller specs and see it pass.

Creating Categories View (1)

_category.json.builder

json.extract! category, :id, :name, :created_at, :updated_at
json.url food_url(category, format: :json)

Creating Categories View (2)

_form.html.erb

<%= form_for(category) do |f| %>
  <% if category.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(category.errors.count, "error") %> prohibited this category from being saved:</h2>

      <ul>
      <% category.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Creating Categories View (3)

_edit.html.erb

<h1>Editing Category</h1>

<%= render 'form', category: @category %>

<%= link_to 'Show', @category %> |
<%= link_to 'Back', categories_path %>

Creating Categories View (4)

index.html.erb

<p id="notice"><%= notice %></p>

<h1>Categories</h1>

<table>
  <% @categories.each do |category| %>
    <tr class="<%= cycle('list_line_odd', 'list_line_even') %>">
      <td><%= category.name %></td>
      <td>
        <%= link_to 'Show', category %>  
        <%= link_to 'Edit', edit_category_path(category) %>  
        <%= link_to 'Destroy', category, method: :delete, data: { confirm: 'Are you sure?' } %>
      </td>
    </tr>
  <% end %>
</table>

<br/>

<%= link_to 'New Category', new_category_path %>

Creating Categories View (5)

index.json.builder

json.array! @categories, partial: 'categories/category', as: :category

Creating Categories View (6)

new.html.erb

<h1>New Category</h1>

<%= render 'form', category: @category %>

<%= link_to 'Back', categories_path %>

Creating Categories View (7)

show.html.erb

<p id="notice"><%= notice %></p>

<p>
  <strong>Name:</strong>
  <%= @category.name %>
</p>

<p>
  <strong>Foods:</strong>
  <% @category.foods.each do |food| %>
    <%= food.name %>
  <% end %>
</p>


<%= link_to 'Edit', edit_category_path(@category) %> |
<%= link_to 'Back', categories_path %>

Creating Categories View (8)

index.json.builder

json.partial! "categories/category", category: @category

Play Around

Now you can play around with your Category feature.

Modifying Foods Controller

We need to modify our Foods controller.

class FoodsController < ApplicationController
  # -cut-
  private
    # -cut-
    # Never trust parameters from the scary internet, only allow the white list through.
    def food_params
      params.require(:food).permit(:name, :description, :image_url, :price, :category_id)
    end
end

Modifying Foods View (1)

_form.html.erb

<%= form_for(food) do |f| %>
  <!-- cut -->
  <div class="field">
    <%= f.label :category %>
    <%= f.collection_select :category_id, Category.order(:name), :id, :name, include_blank: true %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Modifying Foods View (2)

index.html.erb

<!-- cut -->
<table>
  <% @foods.each do |food| %>
    <tr class="<%= cycle('list_line_odd', 'list_line_even') %>">
      <!-- cut -->
      <td class="list_description">
        <dl>
          <dt><%= food.name %></dt>
          <dd><%= truncate(strip_tags(food.description), length: 80) %></dd>
          <dd>Category: <%= food.category.try(:name) %></dd>
        </dl>
      </td>
      <!-- cut -->
    </tr>
  <% end %>
</table>
<!-- cut -->

Commit!

Now play around with your app and commit it afterward.

Solutions - The Simplified Go Food Web App - Iteration 2

By qblfrb

Solutions - The Simplified Go Food Web App - Iteration 2

  • 311