Rails 4 Application Development HOTSHOT (2014)

Chapter 10. A Rails Engines-based E-Commerce Platform

Rails provides an effective way to extend the functionality of applications in a plug-and-play fashion. This is called Rails Engines. Earlier Rails versions had engines and plugins (which are located at app/vendor/plugins), but Rails 4 has completely deprecated the use of plugins in Rails apps. Compared to plugins, engines are cleaner in terms of their definition, have a proper testing structure, and can be more easily customized.

Mission briefing

In this section, we will create a Rails engine to generate an e-commerce application. Once ready, we will add the entire application as a gem and mount it on the main application. As soon as we do this, we will get all the basic features of a shopping cart application. This will allow users to maintain the application and maintain a collection of multiple modules. The application will have different moving parts that need to be upgraded on a frequent basis, as and when they are updated by their respective maintainers.

Why is it awesome?

There are several examples of successful, feature-packed Rails engines such as Devise, Spree Commerce, and LocomotiveCMS. These engines have given users an easy way to incorporate really advanced functionality such as an authentication system, a fully featured e-commerce engine, and a content management system tucked inside a Ruby gem.

The following screenshot shows us what the designed application looks like:

Why is it awesome?

At the end of these tasks, we will have a Rails engine that can be mounted on a Rails application.

Your Hotshot objectives

While building this application, we will go through the following tasks:

·        Creating a category and product listing

·        Creating a shopping cart and an add to cart feature

·        Packaging the engine as a gem

·        Mounting the engine on a blank Rails application

·        Customizing and overriding the default classes

Mission checklist

We need the following software installed on the system before we start with our mission:

·        Ruby 1.9.3 / Ruby 2.0.0

·        Rails 4.0+

·        MongoDB 2.4

·        Devise

·        Bootstrap 3.0

·        Git

·        jQuery

Creating a category and product listing

In the first task, we will deal with the creation of a Rails engine. We will create a product and category to list our products as we are creating an e-commerce engine. We will see how to add a carrierwave uploader for uploading product images inside the engine and add it as a dependency to our application. At the end of this, we will understand why we selected a mountable Rails engine instead of a full Rails engine.

Engage thrusters

We will first create the backbone of our Rails engine by performing the steps:

1.    We will generate a mountable engine as opposed to a full Rails engine.

2.  $rails plugin new ecom --mountable --O

3.        create        

4.        create  README.rdoc

5.        create  Rakefile

6.        create  ecom.gemspec

7.        create  MIT-LICENSE

8.        create  .gitignore

9.        create  Gemfile

10.      create  app

11.      create  app/controllers/ecom/application_controller.rb

12.      create  app/helpers/ecom/application_helper.rb

13.      create  app/mailers

14.      create  app/models

15.      create  app/views/layouts/ecom/application.html.erb

16.      create  app/assets/images/ecom

17.      create  app/assets/images/ecom/.keep

18.      create  config/routes.rb

19.      create  lib/ecom.rb

20.      create  lib/tasks/ecom_tasks.rake

21.      create  lib/ecom/version.rb

22.      create  lib/ecom/engine.rb

23.      create  app/assets/stylesheets/ecom/application.css

24.      create  app/assets/javascripts/ecom/application.js

25.      create  bin

26.      create  bin/rails

27.      create  test/test_helper.rb

28.      create  test/ecom_test.rb

29.      append  Rakefile

30.      create  test/integration/navigation_test.rb

31.  vendor_app  test/dummy

32.         run  bundle install

33.Fetching gem metadata from https://rubygems.org/...........

34.Fetching gem metadata from https://rubygems.org/..

35.Resolving dependencies...

36.Using rake (10.1.1)

37.Using i18n (0.6.9)

38.Using minitest (4.7.5)

39.Using multi_json (1.8.2)

40.Using atomic (1.1.14)

41.Using thread_safe (0.1.3)

42.Using tzinfo (0.3.38)

43.Using activesupport (4.0.2)

44.Using builder (3.1.4)

45.Using erubis (2.7.0)

46.Using rack (1.5.2)

47.Using rack-test (0.6.2)

48.Using actionpack (4.0.2)

49.Using mime-types (1.25.1)

50.Using polyglot (0.3.3)

51.Using treetop (1.4.15)

52.Using mail (2.5.4)

53.Using actionmailer (4.0.2)

54.Using activemodel (4.0.2)

55.Using activerecord-deprecated_finders (1.0.3)

56.Using arel (4.0.1)

57.Using activerecord (4.0.2)

58.Using bundler (1.3.5)

59.Using thor (0.18.1)

60.Using railties (4.0.2)

61.Using hike (1.2.3)

62.Using tilt (1.4.1)

63.Using sprockets (2.10.1)

64.Using sprockets-rails (2.0.1)

65.Using rails (4.0.2)

66.Using ecom (0.0.1) from source at /home/rwub/rails4-book/book/6294OS_Chapter_10/ecom

67.ecom at /home/rwub/rails4-book/book/6294OS_Chapter_10/ecom did not have a valid gemspec.

68.This prevents bundler from installing bins or native extensions, but that may not affect its functionality.

69.The validation message from Rubygems was:

70.  "FIXME" or "TODO" is not an author

71.Your bundle is complete!

Use `bundle show [gemname]` to see where a bundled gem is installed.

72.          As we have skipped ActiveRecord, we need an ORM, so, we will add Mongoid to our Gemfile and bundle install:

73.Gemfile

74. 

75.gem 'mongoid',  github: 'mongoid/mongoid'

76. 

ecom$ bundle install

77.          We will not run mongoid:config in the Rails engine; we will do this after this engine is installed in an application.

78.          In order to use mongoid to generate our models, we need to add it as a module dependency in our Rails binary. Currently, it looks like the following code:

79.bin/rails

80. 

81.#!/usr/bin/env ruby

82.# This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.

83. 

84.ENGINE_ROOT = File.expand_path('../..', __FILE__)

85.ENGINE_PATH = File.expand_path('../../lib/ecom/engine', __FILE__)

86. 

87.# Set up gems listed in the Gemfile.

88. 

89.ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

90. 

91.require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])

92. 

93.require "rails/all"

require 'rails/engine/commands'

94.          We will modify "rails/all" to load all the modules separately and especially load mongoid. We will also load rubygems from the gemspec file directly onto the bin/rails file.

95.bin/rails

96. 

97.#!/usr/bin/env ruby

98.# This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.

99. 

100.   ENGINE_ROOT = File.expand_path('../..', __FILE__)

101.   ENGINE_PATH = File.expand_path('../../lib/ecom/engine', __FILE__)

102.    

103.   # Set up gems listed in the Gemfile.

104.    

105.   ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

106.    

107.   require 'rubygems'

108.    

109.   require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])

110.    

111.   require "action_controller/railtie"

112.   require "action_mailer/railtie"

113.   require "sprockets/railtie"

114.   require "rails/test_unit/railtie"

115.   require 'rails/engine/commands'

require "mongoid"

116.     In Rails 4, active_resource/railties is not required, so we will have to make sure the following line is not included:

require "active_resource/railtie"

117.     We will also add mongoid as a dependency in our gemspec file:

118.    ecom/ecom.gemspec

119.    

120.     s.add_dependency "rails", "~> 4.1.0.rc1"

  s.add_dependency "mongoid", "4.0.0.beta1"

121.     Generate rails scaffold for the products. This will create a model, view, and controller under the ecom namespace as shown in the following code:

122.   ecom$ rails g scaffold product name:string description:string base_price:float sku:string

123.         invoke  mongoid

124.         create    app/models/ecom/product.rb

125.         invoke    test_unit

126.         create      test/models/ecom/product_test.rb

127.         create      test/fixtures/ecom/products.yml

128.         invoke  resource_route

129.          route    resources :products

130.         invoke  scaffold_controller

131.         create    app/controllers/ecom/products_controller.rb

132.         invoke    erb

133.         create      app/views/ecom/products

134.         create      app/views/ecom/products/index.html.erb

135.         create      app/views/ecom/products/edit.html.erb

136.         create      app/views/ecom/products/show.html.erb

137.         create      app/views/ecom/products/new.html.erb

138.         create      app/views/ecom/products/_form.html.erb

139.         invoke    test_unit

140.         create      test/controllers/ecom/products_controller_test.rb

141.         invoke    helper

142.         create      app/helpers/ecom/products_helper.rb

143.         invoke      test_unit

144.         create        test/helpers/ecom/products_helper_test.rb

145.         invoke  assets

146.         invoke    js

147.         create      app/assets/javascripts/ecom/products.js

148.         invoke    css

149.         create      app/assets/stylesheets/ecom/products.css

150.         invoke  css

      create    app/assets/stylesheets/scaffold.css

151.     At this point, we will also set up a mechanism to create search-friendly URLs also known as slugs for our products:

152.   Gemfile

153.    

gem 'mongoid_slug', "3.2"

154.     In order to make it work on the product model, we will have to include the module for Mongoid::Slug. We will tell the module to use names to create the slug and enable the history feature in the URL.

155.   module Ecom

156.    

157.     class Product

158.    

159.       include Mongoid::Document

160.    

161.       include Mongoid::Slug

162.    

163.       field :name, type: String

164.    

165.       field :description, type: String

166.    

167.       field :base_price, type: Float

168.    

169.       field :sku, type: String

170.    

171.       slug :name, history: true

172.    

173.     end

end

174.     Likewise, we will create a model for categories too.

175.   ecom$ rails g model category title:string

176.         invoke  mongoid

177.         create    app/models/ecom/category.rb

178.         invoke    test_unit

179.         create      test/models/ecom/category_test.rb

      create      test/fixtures/ecom/categories.yml

180.     We will associate categories and products:

181.   app/models/ecom/category.rb

182.    

183.   module Ecom

184.     class Category

185.       include Mongoid::Document

186.       field :title, type: String

187.    

188.       has_many :products

189.     end

end

190.     We will also associate the product with the category.

191.   app/models/ecom/product.rb

192.    

193.   Module Ecom

194.     class Product

195.       include Mongoid::Document

196.       include Mongoid::Slug

197.    

198.       field :name, type: String

199.       field :description, type: String

200.       field :base_price, type: Float

201.       field :sku, type: String

202.       field :category_id, type: String

203.    

204.       slug :name, history: true

205.       belongs_to :category

206.     end

end

207.     We will follow the same steps to include the carrierwave uploader as we did in our previous projects. We will run the generator for carrierwave as follows:

208.   ecom$ rails g uploader image

    create  app/uploaders/ecom/image_uploader.rb

209.     Note that the uploader is created in the namespace for the ecom/image_uploader.rb plugin.

210.     In order to take our plugin for a test drive, we will directly navigate to the test/dummy folder, where a dummy application has been created for us when we generated a new mountable plugin.

211.     We will run bundle install and configure our database as per Mongoid:

ecom/test/dummy:~/rails g mongoid:config

212.     This will generate the mongoid config file. We will then start the server. However, we will receive the following error:

Unable to autoload constant Ecom::ImageUploader, expected /home/rwub/.rvm/gems/ruby-1.9.3-p327/bundler/gems/ecom-ed9e6082e731/app/uploaders/ecom/image_uploader.rb to define it

213.     This is because carrierwave, by default, creates the engine namespace folder and places the uploader file in it, but does not modify the uploader file with the module name.

214.   app/uploaders/ecom/image_uploader.rb

215.   # encoding: utf-8

216.   module Ecom

217.    class ImageUploader < CarrierWave::Uploader::Base

218.    

219.     # Choose what kind of storage to use for this uploader:

220.     storage :file

221.    

222.     # Override the directory where uploaded files will be stored.

223.     # This is a sensible default for uploaders that are meant to be mounted:

224.     def store_dir

225.       "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"

226.     end

227.    end

end

Objective complete – mini debriefing

In this task, we created a mountable Rails engine. There are two types of Rails engines available:

·        A full engine

·        A mountable engine

A full engine is a tightly coupled application, which works as a direct augmentation to the existing Rails application. This happens because it shares the classes across the application once included due to the lack of a different namespace. As shown in the following code, a full Rails engine initializer has a regular engine initializer rule:

ecom/lib/ecom/engine.rb

module Ecom

  class Engine < ::Rails::Engine

  end

end

In our case, we are using a mountable engine so the initializer will have a method called isolate_namespace. This method will separate model, views, controllers, and all methods into a namespace called Ecom.

module Ecom

  class Engine < ::Rails::Engine

    isolate_namespace Ecom

  end

end

Everything we see here is included with that namespace.

ecom/app/controllers/ecom$ ls

application_controller.rb  categories_controller.rb  products_controller.rb

Because of the namespace, application_controller is added to the controller as a dependency before it is extended to make the ActionController::Base class available to all the controllers.

ecom/app/controllers/ecom/products_controller.rb

require_dependency "ecom/application_controller"

module Ecom

  class ProductsController < ApplicationController

  end

end

The main purpose of our engine is to augment an existing application and avoid conflicts with an application's existing model and controller classes. Hence, we decided to go for an isolate_namespace mountable engine.

While the plugin was being generated, we saw that a full application to test drive the engine was also created inside the test folder. We, however, need to add the database config files in order to run it.

In order to use mongoid inside the Rails engine, we had to manually include the mongoid module and hence the other Rails modules in it. This is because ActiveRecord is loaded in the rails/all module inclusion by default. Hence, we explicitly require specific railties that include mongoid. We also added a method to load rubygems inside our Rails bin file. We also added mongoid as a dependency to our Rails engine. Railtie is the core of the Rails framework. ActiveRecord, ActionController, and ActionMailer are all examples of Railtie and are responsible for initializing themselves. Railtie is essential when the component needs to communicate with the Rails framework at the time of boot or even after that.

We created a method to generate search-friendly URLs using the mongoid-slug gem. We defined the name field to create the slug and enabled history to retain the URLs even after they have been updated.

Creating a shopping cart and an Add to Cart feature

A shopping cart is the most important feature of an e-commerce application. We need to create a temporary session object in order to store the value of items in the cart. The standard terminology for products that have been added to the cart is line items. When a user successfully checks out, line items get transitioned into orders, and they generally live between sessions. They are also dependent on the completion of the transaction. Once the transaction is completed or the session is cleared, the line items are deleted.

Prepare for lift off

We will install devise and generate a model for the user as follows:

ecom$ rails g devise:install

      create  config/initializers/devise.rb

      create  config/locales/devise.en.yml

ecom$ rails g devise user

This will generate the following route in our engine's routes:

config/routes.rb

devise_for :users, :class_name => "Ecom::User"

Engage thrusters

We will create a checkout process in this task, as shown in the following steps:

1.    We will first add devise to the application. However, we need to modify a few things in order for it to function seamlessly inside an engine. First, modify the routes:

2.  app/config/routes.rb

3.   devise_for :users, {

4.      :class_name => "Ecom::User",

5.      module: :devise

  }

6.    Add a router name inside the devise initializer. We will also need to uncomment secret_key as follows:

7.  app/config/initializer/devise.rb

8.  Devise.setup do |config|

9.    config.secret_key =

10.'1c17867bf2d8e469ed713b1249eab0f87c918e0e09b265be6a0ed8bc01c8f0ebd192387418d60542c96ad42b61fdc8a167ec5843f6cd94e9d66ee39b33ede703'

11.  config.parent_controller = 'ActionController::Base'

12.  config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'

13.  require 'devise/orm/mongoid'

14.  config.case_insensitive_keys = [ :email ]

15.  config.strip_whitespace_keys = [ :email ]

16.  config.skip_session_storage = [:http_auth]

17.  config.stretches = Rails.env.test? ? 1 : 10

18.  config.reconfirmable = true

19.  config.password_length = 8..128

20.  config.reset_password_within = 6.hours

21.  config.sign_out_via = :delete

22.  config.router_name = :ecom

end

23.          Finally, add devise as a gem dependency as follows:

24.ecom/ecom.gemspec

25. 

26.  s.add_dependency "rails", "~> 4.1.0.rc1"

27. 

28.  s.add_dependency "mongoid", "4.0.0.beta1"

29. 

  s.add_dependency "devise"

30.          We will generate a model for line items as follows:

31.$ rails g model line_item product_id:string price:float

32.      invoke  mongoid

33.      create    app/models/ecom/line_item.rb

34.      invoke    test_unit

35.      create      test/models/ecom/line_item_test.rb

      create      test/fixtures/ecom/line_items.yml

36.          We will also generate a model called purchases as shown in the following code. This model stores the value of orders that are generated as soon as the transaction is complete:

37.rails g model purchase user_id:string checked_out_at:time total_price:float

38.      invoke  mongoid

39.      create    app/models/ecom/purchase.rb

40.      invoke    test_unit

41.      create      test/models/ecom/purchase_test.rb

      create      test/fixtures/ecom/purchases.yml

42.          First, we will create two associations: the first one between line items and purchases, and the second one between line items and products. This is because a product data is imported into a line item, and upon a successful checkout, the line item is then transformed into a purchase. So, a line item belongs to product and a purchase has many line items.

43.app/model/ecom/line_item.rb

44.module Ecom

45.  class LineItem

46.    include Mongoid::Document

47.    include Mongoid::Timestamps

48.   

49.    field :purchase_id, type: String

50.    field :product_id, type: String

51.    field :price, type: Float

52. 

53.    belongs_to :purchase

54.    belongs_to :product

55.  end

56.end

57. 

58.app/model/ecom/product.rb

59. 

60.module Ecom

61.  class Product

62.    include Mongoid::Document

63.    include Mongoid::Slug

64. 

65.    field :name, type: String

66.    field :description, type: String

67.    field :base_price, type: Float

68.    field :sku, type: String

69. 

70.    slug :name, history: true

71.    belongs_to :category

72.    has_many :line_items

73.    mount_uploader :image, ImageUploader

74.  end

75.end

76. 

77.app/models/ecom/purchase.rb

78. 

79.module Ecom

80.  class Purchase

81.    include Mongoid::Document

82.    include Mongoid::MultiParameterAttributes

83.    include Mongoid::Timestamps

84. 

85.    field :user_id, type: String

86.    field :checked_out_at, type: DateTime  

87.    field :total_price,type: Float

88. 

89.    has_many :line_items, :dependent => :destroy

90.    belongs_to :user

91.  end

end

92.          Finally, we will add an association between the user and the purchase:

93.app/models/ecom/user.rb

94. 

95.module Ecom

96.  class User

97.  include Mongoid::Document

98.  include Mongoid::Timestamps

99. 

100.     # Include default devise modules. Others available are:

101.     # :confirmable, :lockable, :timeoutable and :omniauthable

102.     devise :database_authenticatable, :registerable,

103.            :recoverable, :rememberable, :trackable, :validatable

104.    

105.     ## Database authenticatable

106.     field :email,              :type => String, :default => ""

107.     field :encrypted_password, :type => String, :default => ""

108.    

109.     ## Recoverable

110.     field :reset_password_token,   :type => String

111.     field :reset_password_sent_at, :type => Time

112.    

113.     ## Rememberable

114.     field :remember_created_at, :type => Time

115.    

116.     ## Trackable

117.     field :sign_in_count,      :type => Integer, :default => 0

118.     field :current_sign_in_at, :type => Time

119.     field :last_sign_in_at,    :type => Time

120.     field :current_sign_in_ip, :type => String

121.     field :last_sign_in_ip,    :type => String

122.    

123.     ## Confirmable

124.     # field :confirmation_token,   :type => String

125.     # field :confirmed_at,         :type => Time

126.     # field :confirmation_sent_at, :type => Time

127.     # field :unconfirmed_email,    :type => String # Only if using reconfirmable

128.    

129.     ## Lockable

130.     # field :failed_attempts, :type => Integer, :default => 0 # Only if lock strategy is :failed_attempts

131.     # field :unlock_token,    :type => String # Only if unlock strategy is :email or :both

132.     # field :locked_at,       :type => Time

133.    

134.       has_many :purchases, :dependent => :destroy

135.     end

end

136.     We will now create a cart controller to display the cart and carry out certain functions such as checkout:

137.   $ rails g controller cart show

138.         create  app/controllers/ecom/cart_controller.rb

139.          route  get "cart/show"

140.         invoke  erb

141.         create    app/views/ecom/cart

142.         create    app/views/ecom/cart/show.html.erb

143.         invoke  test_unit

144.         create    test/controllers/ecom/cart_controller_test.rb

145.         invoke  helper

146.         create    app/helpers/ecom/cart_helper.rb

147.         invoke    test_unit

148.         create      test/helpers/ecom/cart_helper_test.rb

149.         invoke  assets

150.         invoke    js

151.         create      app/assets/javascripts/ecom/cart.js

152.         invoke    css

      create      app/assets/stylesheets/ecom/cart.css

153.     In order to create the line item, we will add a class method in our line_item model as follows:

154.   app/models/line_item.rb

155.    

156.     def self.make_items(purchase_id, product_id, price)

157.    

158.         LineItem.create(purchase_id: purchase_id, product_id: product_id, price: price)

159.    

    end

160.     While shopping, a user can add multiple products to the cart. Every time the user adds an item, the price is recalculated, as shown in the following code:

161.   app/models/ecom/purchase.rb

162.    

163.   def recalculate_price!

164.    

165.      self.total_price = line_items.inject(0.0){|sum, line_item| sum += line_item.price }

166.    

167.      save!

168.    

 end

169.     We will now create methods to add and remove line items from the cart and add a way to pass the objects to the checkout page:

170.   app/controllers/ecom/cart_controller.rb

171.   require_dependency "ecom/application_controller"

172.    

173.   module Ecom

174.     class CartController < ApplicationController

175.       before_filter :authenticate_user!

176.       before_action :get_cart_value

177.    

178.     def add

179.       @cart.save

180.       session[:cart_id] = @cart.id

181.       product = Product.find(params[:id])

182.       item = LineItem.new

183.       item.make_items(@cart.id, product.id, product.base_price)

184.       @cart.recalculate_price!

185.       flash[:notice] = "Product Added to Cart"

186.       redirect_to cart_path

187.     end

188.    

189.     def remove

190.       item = @cart.line_items.find(params[:id])

191.       item.destroy

192.       @cart.recalculate_price!

193.       flash[:notice] = "Product Deleted from Cart"

194.       redirect_to cart_path

195.     end

196.    

197.     protected

198.    

199.     def get_cart_value

200.       if session[:cart_id].nil?

201.        @cart = Purchase.create

202.        session[:cart_id] = @cart.id

203.        @cart

204.       else

205.        @cart = Purchase.find(session[:cart_id])

206.       end

207.     end

208.    

209.     end

end

210.     We will display all the items in the cart on the cart page:

211.   app/views/ecom/cart/show.html.erb

212.   <h1>Shopping Cart</h1>

213.    

214.   <% unless @cart.line_items.any? %>

215.     <p>You don't have any items in your cart. <%= link_to "Go Add Some", products_path %>

216.   <% end %>

217.    

218.   <table width="100%">

219.     <tr>

220.       <th>Product</th>

221.       <th>Price</th>

222.     </tr>

223.     <% for line_item in @cart.line_items %>

224.       <tr>

225.         <td><%= line_item.product.name %></td>

226.         <td><%= number_to_currency line_item.price %></td>

227.         <td><%= link_to "Remove", remove_from_cart_path(line_item), :method => :post %></td>

228.       </tr>

229.     <% end %>

230.     <tr>

231.       <td>Total:</td>

232.       <td><%= number_to_currency @cart.total_price %></td>

233.     </tr>

234.   </table>

235.    

236.   <hr />

237.   <%= form_tag checkout_path, :style => "text-align: right" do |f| %>

238.     <%= link_to "Continue Shopping", root_path %>

239.     or

240.     <%= submit_tag "Checkout" %>

<% end %>

241.     To tie it all together, we will add routes for our cart controller:

242.   config/routes.rb

243.     get "cart" => "cart#show"

244.     post "cart/add/:id" => "cart#add", :as => :add_to_cart

  post "cart/remove/:id" => "cart#remove", :as => :remove_from_cart

245.     In our product page, we will add a button for adding items to the cart:

246.   app/views/ecom/products/index.html.erb 

247.   <div class="row">

248.       <% @products.each do |product| %>

249.           <div class="col-lg-4">

250.             <h3><%=link_to product.name, product %></h3>

251.             <p><%= image_tag product.image.url %></p>

252.             <p><b>Price:</b> <%= product.base_price %></p>

253.             <p><%= product.description %></p>

254.             <p><%=link_to( image_tag("ecom/add-to-cart-button.png"), add_to_cart_path(product.id)) %></p>

255.           </div>

256.       <%end%>

257.    

258.   app/views/ecom/products/show.html.erb

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

260.    

261.   <p>

262.     <h2><%= @product.name %></h2>

263.   </p>

264.    

265.   <p>

266.     <strong>Sku: </strong>

267.     <%= @product.sku %>

268.   </p>

269.    

270.   <p>

271.     <strong>Price: </strong>

272.     <%= @product.base_price %>

273.   </p>

274.    

275.   <br/>

276.   <p>

277.     <%=image_tag @product.image.url %>

278.   </p>

279.    

<p><%=link_to( image_tag("ecom/add-to-cart-button.png"), add_to_cart_path(@product.id)) %></p>

Objective complete – mini debriefing

In this task, we created a very basic cart function. The logic behind a cart function is that it should be valid throughout the user session. That way the user has the flexibility to add and remove products at will while shopping. In addition, we added two more models:line_item and purchase. While in the cart, we need to keep a track of the details of the products that are in the cart; we used the line item to do this.

We first made devise aware of the namespace of our model through our routes:

Ecom/config/routes.rb

App/config/routes.rb

 devise_for :users, {

    :class_name => "Ecom::User",

    module: :devise

  }

We want to check whether devise is bundled with the engine or not, so we will add it as a dependency in our gemspec file:

ecom/ecom.gemspec

 s.add_dependency "devise"

In order to stick with a specific version to retain its compatibility, we can define the version of the dependency:

s.add_dependency "devise", "3.2.3"

We added a protected method to check if the session variable for cart_id has a value or not. If the value is not present, we will create a new object for the order, as shown in the following code:

ecom/controllers/ecom/cart_controller.rb

 before_action :get_cart_value

protected

  def get_cart_value

    if session[:cart_id].nil?

     @cart = Purchase.create

     session[:cart_id] = @cart.id

     @cart

    else

     @cart = Purchase.find(session[:cart_id])

    end

  end

Furthermore, we created an add method in cart_controller.rb. We will persist cart_id in the session. The purchase or order is the collection of products that a user is purchasing. So, all those values will be associated to session[:cart_id].

def add

    @cart.save

    session[:cart_id] = @cart.id

    product = Product.find(params[:id])

    item = LineItem.new

    item.make_items(@cart.id, product.id, product.base_price)

    @cart.recalculate_price!

    flash[:notice] = "Product Added to Cart"

    redirect_to cart_path

end

We also added a method to call the line_item model, and call this model on the item object in cart_controller. Mongoid's create method allows us to directly pass the parameters and create a record, as shown in the following code:

   def self.make_items(purchase_id, product_id, price)

      LineItem.create(purchase_id: purchase_id, product_id: product_id, price: price)

   end

Every time a product is added to the cart, we need to recalculate the total price of the order. We created line_items.inject and recursively added the product prices to calculate the total price. The inject method accepts an array (line_items) as the input. It reads the entire array element by element (line_item) and accepts a block (sum). So, the inject method will load the entire line_items array and initiate a block called sum with a value 0.0. When the first line_item array is read, the sum function is encountered and the value is added to the sum block. When the inject method traverses the next line_item array, the value is added to the last updated value in the sum block, as shown in the following code:

ecom/model/ecom/purchase.rb

    def recalculate_price!

      self.total_price = line_items.inject(0.0){|sum, line_item| sum += line_item.price }

      save!

    end

Packaging the engine as a gem

GitHub and RubyGems are the best way to host our Gems. Rubygems hosts the gem server from where people can directly install it. GitHub can be used to host the source code of the gem. We will first edit our gem and make it ready for packaging. Then, we will pack and upload it on the rubygems website.

Prepare for lift off

In order to perform this task you need to have a rubygems account and need to set it up on your local machine, as mentioned in the following steps:

1.    First sign up for your account at http://rubygems.org/sign_up.

Prepare for lift off

2.    Then, you need to set it up on your machine through your console. Please make sure you put your handle in place of <handle> in the following code:

3.  $ curl -u <handle>

4.  https://rubygems.org/api/v1/api_key.yaml >

5.  ~/.gem/credentials; chmod 0600 ~/.gem/credentials

6.   

Enter host password for user '<handle>':

Engage thrusters

We will pack our newly created gem in this task:

1.    In order to run the Rails engine, we need to add its base route to the routes.rb file of our Rails application. However, instead of asking the user to do this manually, we will create a generators folder inside the lib folder:

2.  Ecom/lib$ mkdir generators

3.  Ecom/lib$ cd generators

4.  ecom/lib/generators$ mkdir ecom

ecom/lib/generators$ mkdir templates

5.    Inside ecom/lib/generators, we will create our install generator, as shown in the following code:

6.  class Ecom::InstallGenerator < ::Rails::Generators::Base

7.    include Rails::Generators::Migration

8.    source_root File.expand_path('../templates', __FILE__)

9.    desc "Installs Ecom Store"

10. 

end

11.          We will create an install method to add a line to the routes.rb file of our application and copy our locales to the application's locales folder:

12.Ecom/lib/generators/ecom/install_generator.rb

13. 

14.class Ecom::InstallGenerator < ::Rails::Generators::Base

15.  include Rails::Generators::Migration

16.  source_root File.expand_path('../templates', __FILE__)

17.  desc "Installs Ecom Store"

18. 

19.  def install

20.    route 'mount Ecom::Engine => "/store"'

21.    copy_file "../../../../config/locales/en.yml", "config/locales/ecom.en.yml"

22.  end

end

23.          We will edit the ecom.gemspec file to add details. Make sure you add all the dependencies for the application here. Without these dependencies, the gem will not work.

24.$:.push File.expand_path("../lib", __FILE__)

25. 

26.# Maintain your gem's version:

27.require "ecom/version"

28. 

29.# Describe your gem and declare its dependencies:

30.Gem::Specification.new do |s|

31.  s.name        = "ecom"

32.  s.version     = Ecom::VERSION

33.  s.authors     = ["Saurabh Bhatia"]

34.  s.email       = ["saurabh.a.bhatia@gmail.com"]

35.  s.homepage    = "http://fedible.org"

36.  s.summary     = "A Complete Ecommerce Application"

37.  s.description = "A Rails plugin to create an Ecommerce Application"

38. 

39.  s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"]

40.  s.test_files = Dir["test/**/*"]

41. 

42.  s.add_dependency "rails", "~> 4.1.0.rc1"

43. 

44.  s.add_dependency "mongoid", "4.0.0.beta1"

45. 

46.  s.add_dependency "mongoid_slug", "3.2"

47. 

48.  s.add_dependency "carrierwave", "0.10.0"

49. 

50.  s.add_dependency "devise"

end

51.          After the gemspec file is defined clearly, build the gem using gemspec:

52.ecom$ gem build ecom.gemspec

53.  Successfully built RubyGem

54.  Name: ecom

55.  Version: 0.0.1

  File: ecom-0.0.1.gem

56.          In order to upload your gem on the rubygems server, you first need to sign up for it. If your rubygems account is correctly set up on your system, just push the gem:

57.$ gem push ecom-0.0.1.gem

58.Pushing gem to https://rubygems.org...

Successfully registered gem: ecom (0.0.1)

59.          You will have a page created on the rubygems server for your gem, which is shown as follows:

Engage thrusters

60.          We will now install a gem from our remote gem server:

61. $ gem install ecom -v 0.0.1

62.1 gem installed

63.Installing ri documentation for ecom-0.0.1...

Building YARD (yri) index for ecom-0.0.1...

Objective complete – mini debriefing

In this task, we prepared our application for show time by packaging it as a gem. We first began by creating a generator in our engine. This generator copies the locale files to their path and also inserts a route in the routes.rb file of our application. In our task, we created the generator manually. However, we can also use a generator to create a generator, shown as follows:

ecom$ rails g generator install

      create  lib/generators/install

      create  lib/generators/install/install_generator.rb

      create  lib/generators/install/USAGE

      create  lib/generators/install/templates

Then, we can add the description and tasks that the generator needs to perform. In a lot of Rails engines such as devise, the generator is used extensively to generate a user model, perform migrations, add routes to routes.rb, and copy locale files to the path. As you might have already seen, generators have a folder called templates. This folder contains templates of files that need to be copied to a particular path. For example, we need to generate a model. The generator will accept the name of the file as a command-line argument like the following code:

rails g model User

This command will copy the model for the user in the templates folder to the specified path and will rename it as User.

Rubygems has been the primary way to package and distribute Ruby programs from the beginning, be it Sinatra, only Ruby-based, or Rails engines. Rails gives us a lot of freedom to distribute a Rails engine. In case we use rubygems to distribute the engine, we will need to package the gem using the gem build command, as we saw in the previous task. We will then need an account on rubygems.org and will need to push the gem to the remote gem host. Within less than a minute, our gem is ready to be downloaded and installed. Rubygems also give some stats with the download, such as total downloads and how many downloads per day. The other way to distribute your Rails engine is directly via GitHub. If you think creating a gem is not something you want, you can host your code on GitHub and directly bundle it from there in your Gemfile.

The following is the screenshot of what the GitHub repository of our ecom engine looks like:

Objective complete – mini debriefing

In the Gemfile, we will need to add something like the following code:

gem 'ecom', github: 'saurabhbhatia/ecom'

We can also bundle a specific version, branch, tag, or a commit as follows:

gem 'ecom', '0.0.1', github: 'saurabhbhatia/ecom'

gem 'ecom', github: 'saurabhbhatia/ecom', :branch => 'rails4'

gem 'ecom', github: 'saurabhbhatia/ecom', :tag => '0.0.1rc2'

gem 'ecom', github: 'saurabhbhatia/ecom', :ref => '151e0516'

Any part of the previous code can be used to bundle the gem directly from GitHub. However, we need to be sure that all values are correctly entered in gemspec so that it does not throw an invalid gem spec error during installation.

Mounting the engine on a blank Rails application

We have created a Rails engine with a product and cart function and even packaged it as a gem. Now, we need to take the engine for a test drive. In order to do this, we will mount it onto a blank Rails application. In this task, we will prepare and install the engine in a Rails application. We will then generate a blank Rails application and mount it onto the application.

Engage thrusters

In this task, we will mount and run a Rails engine in a Rails app. Once this is done, we will generate a blank Rails application called Storezilla and add our engine to the Gemfile by performing the following steps:

1.    After adding our engine to the Gemfile, we will need to run the bundle install.

2.  Gemfile

 gem 'ecom', github: 'saurabhbhatia/ecom'

3.    We will then run the generator we just created as follows:

4.  $ rails g ecom:install

5.         route  mount Ecom::Engine => "/store"

      create  config/locales/ecom.en.yml

6.    We can now open our routes.rb file and see the newly created entry as follows:

7.  Storezilla::Application.routes.draw do

8.    get "home/index"

9.    mount Ecom::Engine => "/store"

10. 

11.  root 'home#index'

end

12.          We will now run rake routes to check what routes have been created already, as follows:

13.$ rake routes

14.    Prefix Verb URI Pattern           Controller#Action

15.home_index GET /home/index(.:format) home#index

16.      ecom     /store                Ecom::Engine

17.      root GET /                     home#index

18. 

19.Routes for Ecom::Engine:

20.              categories GET    /categories(.:format)          ecom/categories#index

21.                         POST   /categories(.:format)          ecom/categories#create

22.            new_category GET    /categories/new(.:format)      ecom/categories#new

23.           edit_category GET    /categories/:id/edit(.:format) ecom/categories#edit

24.                category GET    /categories/:id(.:format) ecom/categories#show

25.                         PATCH  /categories/:id(.:format)      ecom/categories#update

26.                         PUT    /categories/:id(.:format)      ecom/categories#update

27.                         DELETE /categories/:id(.:format)      ecom/categories#destroy

28.        new_user_session GET    /users/sign_in(.:format)       devise/sessions#new

29.            user_session POST   /users/sign_in(.:format)       devise/sessions#create

30.    destroy_user_session DELETE /users/sign_out(.:format)      devise/sessions#destroy

31.           user_password POST   /users/password(.:format)      devise/passwords#create

32.       new_user_password GET    /users/password/new(.:format)  devise/passwords#new

33.      edit_user_password GET    /users/password/edit(.:format) devise/passwords#edit

34.                         PATCH  /users/password(.:format)      devise/passwords#update

35.                         PUT    /users/password(.:format)      devise/passwords#update

36.cancel_user_registration GET    /users/cancel(.:format)        devise/registrations#cancel

37.       user_registration POST   /users(.:format)               devise/registrations#create

38.   new_user_registration GET    /users/sign_up(.:format)       devise/registrations#new                  

39.  edit_user_registration GET    /users/edit(.:format)          devise/registrations#edit                  

40.                         PATCH  /users(.:format)               devise/registrations#update                

41.                         PUT    /users(.:format)               devise/registrations#update                 

42.                         DELETE /users(.:format)               devise/registrations#destroy             

43.                products GET    /products(.:format)            ecom/products#index                       

44.                         POST   /products(.:format)            ecom/products#create                      

45.             new_product GET    /products/new(.:format)        ecom/products#new                     

46.            edit_product GET    /products/:id/edit(.:format)   ecom/products#edit                   

47.                 product GET    /products/:id(.:format)        ecom/products#show                          

48.                         PATCH  /products/:id(.:format)        ecom/products#update                   

49.                         PUT    /products/:id(.:format)        ecom/products#update                                

50.                         DELETE /products/:id(.:format)        ecom/products#destroy               

51.                    root GET    /                              ecom/products#index             

52.                    cart GET    /cart(.:format)                ecom/cart#show                     

53.             add_to_cart GET    /cart/add/:id(.:format)        ecom/cart#add                                 

54.        remove_from_cart POST   /cart/remove/:id(.:format)     ecom/cart#remove                             

                checkout POST   /cart/checkout(.:format)       ecom/cart#checkout

55.          We will now navigate to the URL where we mounted our Rails engine, that is, localhost:3000/store. As shown in the following screenshot, we will see a blank store page:

Engage thrusters

56.          As we can see the store is empty, we will fill it with some products.

57.          The Products page, after we created some products, can be browsed at http://localhost:3000/store/products/do-androids-dream-of-electric-sheep-philip-k-dick as shown in the following screenshot:

Engage thrusters

58.          We will also see our cart page, which when empty looks like what is shown in the following screenshot:

Engage thrusters

Objective complete – mini debriefing

This task deals with the mounting of the engine in our application.

The first thing we did in this task was we added our engine to the Gemfile and bundle install. Then, we ran generator to install our ecom engine. This created route in our application where all the engine's routes will be mounted, as shown in the following code:

  mount Ecom::Engine => "/store"

This route will generate all routes with the namespace ecom but will mount at /store. Also, to query the models from the Rails console within the application, we will have to prefix the namespace to the model name.

storezilla$ rails c

Loading development environment (Rails 4.0.2)

1.9.3-p327 :001 > ecom = Ecom::Product.new

 => #<Ecom::Product _id: 52d970207277751d36000000, name: nil, description: nil, base_price: nil, sku: nil, _slugs: [], category_id: nil, image: nil>

If users add some products to the cart and want to view the current items in it, they can browse to /store/cart, as shown in the following screenshot:

Objective complete – mini debriefing

Customizing and overriding the default classes

Until now, we have seen how to create a Rails engine, how to prepare and package it, and finally, how to mount it onto an application and use it. There are times when you need to add some custom code to the existing application. The engine code is not really seen in the folder. So, what do we do if we need to add new methods inside our engine classes? This task will deal with the customization of classes inside the engine. We will first create a state machine-based checkout system, without which our cart functionality is incomplete.

Engage thrusters

We will finally customize our methods by performing the following steps in order to add a checkout process:

1.    First, we will create a namespace in the way we created our engine, as follows:

2.  Storezilla/app/models$mkdir ecom

     Inside this namespace we will create our own model with the same name – purchase.rb .

3.    In order to create a simple checkout process, we will need a state machine. We will use the state_machine library to add the following code:

4.  Gemfile

gem 'state_machine', github: 'pluginaweek/state_machine'

5.    In order to use state_machine with Rails 4.1, we need to monkey patch our state_machine library. We will place this inside our config/initializers folder, as follows:

6.  config/initiailizers/state_machine_patch.rb

7.   

8.  module StateMachine

9.   

10.  module Integrations

11. 

12.     module ActiveModel

13. 

14.        public :around_validation

15. 

16.     end

17. 

18.  end

19. 

end

20.          To override our model, we will use a decorator in Rails. We will first have to modify our engine to read the decorator directory:

21.lib/ecom/engine.rb

22. 

23.module Ecom

24. 

25.  class Engine < ::Rails::Engine

26. 

27.    isolate_namespace Ecom

28. 

29. 

30.    config.to_prepare do

31. 

32.      Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|

33. 

34.        require_dependency(c)

35. 

36.      end

37. 

38.    end

39. 

40.  end

41. 

end

42.          We will have to create an appropriate directory in our app folder, as follows:

43.$ storezilla/app~/$mkdir decorators

44.$ storezilla/app~/$ cd decorators

45.$ storezilla/app/decorators~/$ mkdir models

46.$ storezilla/app/decorators~/$ cd models

47.$ storezilla/app/decorators/models~/$ mkdir ecom

48.$ storezilla/app/decorators/models~/$ cd ecom

$storezilla/app/decoratos/models/ecom~/$ touch purchase_decorator.rb

49.          Once the gem is bundled, we will define states in our purchase model as follows:

50.app/decorators/model/ecom/purchase.rb

51. 

52.state_machine :initial => :cart_in_progress do

53. event :transaction_successful do

54.  transition :cart_in_progress => :order_placed

55. end

end

56.          We will open the Rails console and check how the state transition works with the following code:

57.1.9.3-p327 :004 > purchase = Ecom::Purchase.new

 => #<Ecom::Purchase _id: 52dfdb787277752b8d010000, created_at: nil, updated_at: nil, user_id: nil, checked_out_at: nil, total_price: nil, state: "cart_in_progress">

58.          We will check for state transition and see if it works as desired or not:

59.1.9.3-p327 :005 > purchase.transaction_successful!

60. => true

61.1.9.3-p327 :006 > purchase

 => #<Ecom::Purchase _id: 52dfdb787277752b8d010000, created_at: 2014-01-22 14:56:04 UTC, updated_at: 2014-01-22 14:56:04 UTC, user_id: nil, checked_out_at: nil, total_price: nil, state: "order_placed">

62.          We will put the state toggle inside an instance method in our model as follows:

63.app/decorators/model/ecom/purchase_decorator.rb  

64.def checkout!

65.  self.transaction_successful!

end

66.          Now, we need a controller method to fire this state transition. So, we need to create a controller called cart and extend it from our existing CartController, as follows:

67.app/controllers/cart_controller.rb

68.class CartController < Ecom::CartController

69. 

70. def checkout

71.    @cart.checkout!

72.    session.delete(:cart_id)

73.    flash[:notice] = "Thank your for the Order! We will e-mail you with the shipping info."

74.    redirect_to root_path

75.  end

end

76.          We will add a custom route for the checkout method as follows:

 post "cart/checkout" => "cart#checkout", :as => :checkout

77.          We will try to check out and inspect the output of our checkout method as follows:

Engage thrusters

78.          Now that we have the status of the product, we can create a simple filter to see which orders have been completed. For this, we will add a scope to our purchase model as follows:

79.app/models/ecom/purchase.rb

    scope :order_complete, -> {where(state: "order_placed")}

80.          We will now see the result of this scope in the Rails console as follows:

81.1.9.3-p327 :001 > purchase = Ecom::Purchase.order_complete

82. => #<Mongoid::Criteria

83.  selector: {"state"=>"order_placed"}

84.  options:  {}

85.  class:    Ecom::Purchase

86.  embedded: false>

87.

88.1.9.3-p327 :002 > purchase = Ecom::Purchase.order_complete.last

 => #<Ecom::Purchase _id: 52dfcff772777526cf000000, created_at: 2014-01-22 14:04:46 UTC, updated_at: 2014-01-22 14:04:46 UTC, user_id: 52d5d19c72777517c8000000,  total_price: 10.0, state: "order_placed">

Objective complete – mini debriefing

In this task, we used a pattern in Rails called decorators. Decorators are used to extend the engine model and the controller functionality. In our case, we enhanced our model's functionality by creating a decorator for that model. One of the ways to write a decorator is using a class_eval function, shown as follows:

Ecom::Purchase.class_eval do

end

This function will look for the model class inside the ecom engine and decorate it with the methods in the decorator. In the decorator, we defined the methods for state_machine. We have seen state machines and state transition in Project 2Conference and Event RSVP Management. At the time of this writing, state_machine is not maintained as required; hence, it is recommended that you use other similar libraries with the same purposes such as aasm and workflow.

In order to override the controller, we created a controller called cart in our controllers. We extended cart_controller in our application from our engine's cart controller. This will retain all the methods just as they are, and we can write more methods inside the controller. Also, if we want to override a specific method, we will define only that method (just as we defined the checkout in our case) and the other methods will remain intact, which is shown as follows:

class CartController < Ecom::CartController

end

Then, we created our controller method for the checkout. We called the checkout method in our model to toggle the state of the cart. We deleted the cart ID from the session variable once our transaction was complete after the state toggled successfully, using the following code:

 @cart.checkout!

    session.delete(:cart_id)

Lastly, we created scope in our model. Scope in mongoid has a slightly different syntax than the scope in ActiveRecord.

scope :order_complete, -> {where(state: "order_placed")}

We created a scope called order_complete, which fetches all the purchases whose state is order_placed. The where query is written in braces, preceded by an arrow. This is a Ruby 2 project and is the updated syntax for both mongoid and ActiveRecord.

Mission accomplished

We have successfully created a Rails engine. Good work! We managed to package quite a lot of features in our shopping cart engine. In this project, we worked on the following aspects:

·        We created a mountable Rails engine without ActiveRecord

·        We modified the engine to work with mongoid

·        We generated the models for a product and categories

·        We created a shopping cart with an add to cart functionality

·        We also added the remove from cart and line items functions

·        We prepared the package for gem and uploaded it to rubygems

·        We loaded the gem in the Gemfile and mounted the engine onto the Rails application

·        We customized the controller and model to add the functions that we needed

Hotshot challenges

We created a cool shopping cart project. You can enhance it with a lot of features:

·        Create an administrator area and separate the devise login based on the roles

·        Add product variants as nested attributes for a product

·        Create a scope filter based on the categories

·        Add checkout forms and customize the user sign up form

·        Add another state of cart in progress and cart failure to checkout