Building a Shopping Cart in Ruby on Rails (Part 2)

In the previous part I showed you how to create a cart, add products to it and how to keep products in user’s cart so they can leave the session and come back at a later time and their products will still be in their cart. In this part I will show you how to create an order and the admin panel. Let’s get started.

We finished off by creating the view carts/index.html.erb where we create an order.

<div class="total-price"><%= link_to 'Next', 
new_order_path, class: "btn btn-success" %></div>

Order

Obviously, we need a view that will render after clicking “Next” but before we create the view we need to create the model Order first. You can use rails generate model Order, which will also generate the migration and tests for you, but I like to do it manually.

class Order < ApplicationRecord
  belongs_to :cart
  validates :delivery_address, :delivery_type, :payment_type, presence: true

  DELIVERY_TYPES = ["Courier(DPD)", "Personal collection", "InPost"]
  PAYMENT_TYPES  = ["Cash On Delivery", "Bank Transfer", "Dotpay"]
end

delivery_types and payment_types are for the select options in the view.

rails generate migration CreateOrders delivery_address delivery_type payment_type

(Note the “underscore” is missing in delivery_type and payment_type as WordPress does not understand this syntax (sic!)
Note, that rails does not automatically add timestamps for you when using “manual approach” so I added it manually in the migration file.

class CreateOrders < ActiveRecord::Migration[5.0]
  def change
    create_table :orders do |t|
      t.string :delivery_address
      t.string :delivery_type
      t.string :payment_type
      t.timestamps
    end
  end
end

I want to be able to see what products were added to a particular order by a usr so I need to create associations between order, cart and user.

rails generate migration AddUsertoOrders
class AddColumnUsersToOrders < ActiveRecord::Migration[5.0]
  def change
    add_reference :orders, :user, foreign_key: true
  end
end
rails generate migration AddCartToOrders
class AddCartToOrders < ActiveRecord::Migration[5.0]
  def change
    add_reference :orders, :cart, foreign_key: true
  end
end

Now we can create the view.

<% provide(:title, 'Finalize') %>
<% provide(:button_text, 'Create order') %>
<h1>Please enter your details</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
    <%= render 'form' %></div>
</div>

Here I provide the title of the page, a class to the button and render the form, which looks like this:

<%= form_for(@order) do |f| %>
  <% if @order.errors.any? %>
<div id="error_explanation">
<div class="alert alert-danger">
      The form contains <%= pluralize(@order.errors.count, "error") %>.</div>
<ul>
    <% @order.errors.full_messages.each do |msg| %>
<li><%= msg %></li>

<% end %></ul>
</div>

<% end %>
  <%= f.label :delivery_address %>
  <%= f.text_area :delivery_address, rows: 3, cols: 25, class: 'form-control' %>
  <%= f.label :delivery_type %>
  <%= f.select :delivery_type, Order::DELIVERY_TYPES, prompt: 'Select a delivery method', class: 'form-control' %>
  <%= f.label :payment_type %>
  <%= f.select :payment_type, Order::PAYMENT_TYPES, prompt: 'Select a payment method', class: 'form-control' %>
  <%= f.hidden_field :user_id, value: current_user.id %>
  <%= f.hidden_field :cart_id, value: current_cart.id %>
  <%= f.submit yield(:button_text), class: "btn btn-primary" %>
<% end %>

Admin Panel

We want to give admin users the ability to add products, view and manage orders so we add the following to our routes.rb

 namespace :admin do
    resources :products
    resources :orders, only: [:index, :show]
    root 'products#index'
  end

This gives us routes under the path /admin/products and /admin/orders which are directed to Admin::ProductsController and Admin::OrdersController.

Controllers

There are a few things we need to keep in mind for our namespaced controller.

First, Rails expects this controller file to exist in an admin/ folder within app/controllers/ (i.e., at app/controllers/admin/orders_controller.rb).

Second, because the controller is named within the admin namespace, we use the scope resolution operator to define the controller class:

class Admin::OrdersController < ApplicationController
  # Your Methods Here
end

Views

Views that correspond to our Admin::CategoriesController actions are namespaced just like the controller is. When Rails needs these view files, it will look in the app/views/admin/ subfolder.

Additionally, we’ll need to point our path helpers to the right place for namespaced routes. For example, we’ll use paths like new_admin_category_path and form_for [:admin, @category] in our views.

Restricting Access To Admins

There are a couple of approaches here. We’ll start by adding a simple before_action to the Admin::OrdersController:

class Admin::OrdersController < ApplicationController
  before_action :require_admin

  # Your Methods Here

  def require_admin
    unless current_user.admin?
      redirect_to root_path
    end
  end
end

Refactoring

This approach works, but it’s not very flexible. If we add other admin controller and actions (say, for filtering products), we have to redefine this require_admin method in each namespaced controller.

One solution might be to define the require_admin method in our ApplicationController. That’s a step in the right direction, but we would have to remember to add the before_action to each admin controller. These controllers wouldn’t be secure by default.

The solution I prefer is to define a separate class (in app/controllers/) called AdminController that inherits from ApplicationController:

class AdminController < ApplicationController
  before_action :require_admin

  def require_admin
    unless current_user.admin?
      redirect_to root_path
    end
  end
end

Now, each namespaced admin controller can inherit directly (and quite appropriately) from AdminController. Since we’ve defined the before_action in AdminController, actions in any sub-classed controller will be restricted to admin users by default.

We no longer need to list the before_action or define a require_admin method in each of our namespaced controllers; they just inherit from AdminController:

class Admin::OrdersController < AdminController
  # Methods omitted
end
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s