0.1 Check which version of Ruby is installed:
ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580)
Better yet, use which because it gives us the full path of our Ruby interpreter (and indicates whether we’re using rvm):
which ruby
~/.rvm/rubies/ruby-2.6.3/bin/ruby
0.2 What version of Rails is installed?
rails -v
Rails 5.2.3
Or:
which rails
~/.rvm/gems/ruby-2.6.3/bin/rails
0.3 Is Postgres running?
If you have the Postgres OS X app installed, simply check the menu-bar application to check.
There are multiple ways to check the status of postgres from the command line, including pg-ctl but we won’t cover that now.
0.4 Is yarn installed?
Because we’re using the webpacker gem, we’ll need yarn (the Javascript dependency manager).
brew install yarn
Then install the Javascript dependencies:
yarn install
Due to reasons, you may also need to manually install some of the Javascript dependencies:
yarn add bootstrap jquery popper.js1.1 What does the product page need to do?
- Load an instance of
Teddyfrom the database 2. Render a template showing:TeddyimageTeddydescription- an action (i.e. button that
POSTs form-data, creating a newOrder)
1.2 What does order summary page need to do?
- Load the newly-created instance of
Order2. Render a template showing:TeddyimageTeddydescriptionTeddyprice- show the computed price information (tax, shipping, total)
- provide an action (i.e. button that
POSTs form-data to apayments controllerwhich creates a new Stripe payment, attaches the result to correspondingOrder, and then redirects to the order, showing completion)
Use rails new to create a new Ruby on Rails application.
rails new \
--webpack \
--database postgresql \
-m https://raw.githubusercontent.com/lewagon/rails-templates/master/devise.rb \
teddies_shop
Provided you’ve setup postgres correctly (and it’s running), rails new will automatically create a development and test database for you (default environments that every new default Rails app ships with). rails new leaves the production environment configuration blank for you to complete later.
Note that we’re also using a devise template:
-m, [--template=TEMPLATE] # Path to some application template (can be a filesystem path or URL)
After rails new has done its work, let’s check that our new application works properly.
rails c
Running via Spring preloader in process 60169
Loading development environment (Rails 5.2.3)
[1] pry(main)>
Note that while rails c will tell us if the core rails app is configured correctly for the development environment (i.e. mostly if it’s correctly connected to the database) it will tell us nothing about whether webpacker is working correctly, which is only initialised when we boot the development server via. rails s:
rails s
=> Booting Puma
=> Rails 5.2.3 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.1 (ruby 2.6.3-p62), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:3000
3.1 Generating top-level categories for our products
rails g model Category name:string
name we’ll have categories like kids and geek
3.2 Generating the Teddy model
rails g model Teddy sku:string name:string category:references photo_url:string
sku means stock-keeping unit and is the unique, canonical identifier for each product.
name is the customer-facing title of the product, (i.e. what actually appears on-screen for the user as they browse the inventory)
category is a reference to the parent product category (e.g. Octocat belongs to the geek category)
Finally, migrate the database:
rake db:migrate
== 20190527004750 CreateCategories: migrating =================================
-- create_table(:categories)
-> 0.0101s
== 20190527004750 CreateCategories: migrated (0.0103s) ========================
== 20190527005542 CreateTeddies: migrating ====================================
-- create_table(:teddies)
-> 0.0183s
== 20190527005542 CreateTeddies: migrated (0.0185s) ===========================
Now, let’s check that our Category and Teddy models work properly within the rails console by interactively creating a new Category:
rails c
[1] pry(main)> newCategory = Category.new
=> #<Category:0x00007fd60a29dcf8 id: nil, name: nil, created_at: nil, updated_at: nil>
Now, this model exists in memory, but it hasn’t been saved to the postgres database yet:
pry(main)> newCategory.save
(0.3ms) BEGIN
Category Create (3.2ms) INSERT INTO "categories" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id" [["created_at", "2019-05-27 01:09:49.053303"], ["updated_at", "2019-05-27 01:09:49.053303"]]
(0.7ms) COMMIT
=> true
pry(main)> newCategory
=> #<Category:0x00007fd60a29dcf8
id: 1,
name: nil,
created_at: Mon, 27 May 2019 01:09:49 UTC +00:00,
updated_at: Mon, 27 May 2019 01:09:49 UTC +00:00>
If the return value is => true our model has been saved to the database.
Lets make sure by loading our models straight from the database:
pry(main)> Category.all
Category Load (0.6ms) SELECT "categories".* FROM "categories"
=> [#<Category:0x00007fd6099f8378
id: 1,
name: nil,
created_at: Mon, 27 May 2019 01:16:01 UTC +00:00,
updated_at: Mon, 27 May 2019 01:16:01 UTC +00:00>]
Ok, but what’s the problem with this? The new category has no title because the model doesn’t have an appropriate NOT NULL constraint!
First, let’s clean up our newly created category:
newCategory.delete
Category Load (0.4ms) SELECT "categories".* FROM "categories" WHERE "categories"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]]
Category Destroy (5.8ms) DELETE FROM "categories" WHERE "categories"."id" = $1 [["id", 2]]
=> #<Category:0x00007fd60d2b48d8
id: 2,
name: nil,
created_at: Mon, 27 May 2019 01:16:01 UTC +00:00,
updated_at: Mon, 27 May 2019 01:16:01 UTC +00:00>
Break out of the rails console:
pry(main)> exit
Lets generate a new migration:
rails generate migration AddNotNullToCategoryName
Open the new migration and use change_column_null to add a constraint:
class AddNotNullToCategoryName < ActiveRecord::Migration[5.2]
def change
change_column_null :categories, :name, false
end
end
Save the migration, and run the new migration:
rake db:migrate
== 20190527012308 AddNotNullToCategoryName: migrating =========================
-- change_column_null(:categories, :name, false)
-> 0.0029s
== 20190527012308 AddNotNullToCategoryName: migrated (0.0032s) ================
From within rails console create a new Category and try saving it:
pry(main)> kidsCategory = Category.new
=> #<Category:0x00007fd60d1269f8 id: nil, name: nil, created_at: nil, updated_at: nil>
[2] pry(main)> kidsCategory.save
(0.3ms) BEGIN
Category Create (2.1ms) INSERT INTO "categories" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id" [["created_at", "2019-05-27 01:26:45.199308"], ["updated_at", "2019-05-27 01:26:45.199308"]]
(0.4ms) ROLLBACK
ActiveRecord::NotNullViolation: PG::NotNullViolation: ERROR: null value in column "name" violates not-null constraint
That’s better! Let’s give our Category a valid name and save it:
pry(main)> kidsCategory.name = "kids"
=> "kids"
[4] pry(main)> newCategory.save
(0.2ms) BEGIN
Category Create (0.6ms) INSERT INTO "categories" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["name", "kids"], ["created_at", "2019-05-27 01:26:45.199308"], ["updated_at", "2019-05-27 01:26:45.199308"]]
(1.4ms) COMMIT
=> true
Now, let’s create a Teddy with an association to the “kids” Category:
pry(main)> kidsTeddy = Teddy.new
=> #<Teddy:0x00007fd60dad1728 id: nil, sku: nil, name: nil, category_id: nil, photo_url: nil, created_at: nil, updated_at: nil>
pry(main)> kidsTeddy.category = kidsCategory
=> #<Category:0x00007fd60d1deda0
id: 5,
name: "kids",
created_at: Mon, 27 May 2019 01:31:20 UTC +00:00,
updated_at: Mon, 27 May 2019 01:31:20 UTC +00:00>
pry(main)> kidsTeddy.name = 'Professor Crumbling'
=> "Professor Crumbling"
pry(main)> kidsTeddy.sku = 'professor-crumbling'
=> "professor-crumbling"
pry(main)> kidsTeddy.save
(0.3ms) BEGIN
Teddy Create (1.6ms) INSERT INTO "teddies" ("sku", "name", "category_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["sku", "professor-crumbling"], ["name", "Professor Crumbling"], ["category_id", 5], ["created_at", "2019-05-27 01:35:18.020719"], ["updated_at", "2019-05-27 01:35:18.020719"]]
(0.6ms) COMMIT
=> true
Now, when we inspect the newly created Teddy we should see all the fields have been saved:
pry(main)> kidsTeddy
=> #<Teddy:0x00007fd60dad1728
id: 1,
sku: "professor-crumbling",
name: "Professor Crumbling",
category_id: 5,
photo_url: nil,
created_at: Mon, 27 May 2019 01:35:18 UTC +00:00,
updated_at: Mon, 27 May 2019 01:35:18 UTC +00:00>
Working with models in pry is a crucial skill, because testing the functionality of this layer directly (without having to use controllers and their various actions) will save you heaps of time, both in terms of designing and debugging.
However, what we really want is default test data so that when someone pulls down our application they can work with it immediately without having to go through the extra step of creating their own, or fetching or loading it from somewhere else. We’ll use seeds for that.
First, let’s clean up our database. Break our of rails c and run:
rake db:reset
Open db/seeds and enter the following:
puts 'Cleaning database...'
Teddy.destroy_all
Category.destroy_all
puts 'Creating categories...'
geek = Category.create!(name: 'geek')
kids = Category.create!(name: 'kids')
puts 'Creating teddies...'
Teddy.create!(sku: 'original-teddy-bear', name: 'Teddy bear', category: kids, photo_url: 'http://onehdwallpaper.com/wp-content/uploads/2015/07/Teddy-Bears-HD-Images.jpg')
Teddy.create!(sku: 'jean-mimi', name: 'Jean-Michel - Le Wagon', category: geek, photo_url: 'https://pbs.twimg.com/media/B_AUcKeU4AE6ZcG.jpg:large')
Teddy.create!(sku: 'octocat', name: 'Octocat - GitHub', category: geek, photo_url: 'https://cdn-ak.f.st-hatena.com/images/fotolife/s/suzumidokoro/20160413/20160413220730.jpg')
puts 'Finished!'Now run rails db:seed to create our test data:
rails db:seed
Cleaning database...
Creating categories...
Creating teddies...
Finished!
Now, if you re-enter rails console and run Category.all you will see our newly created test data:
rails c
Running via Spring preloader in process 68408
Loading development environment (Rails 5.2.3)
pry(main)> Category.all
Category Load (1.2ms) SELECT "categories".* FROM "categories"
=> [#<Category:0x00007fd6090e4200
id: 1,
name: "geek",
created_at: Mon, 27 May 2019 01:47:51 UTC +00:00,
updated_at: Mon, 27 May 2019 01:47:51 UTC +00:00>,
#<Category:0x00007fd60d98ef50
id: 2,
name: "kids",
created_at: Mon, 27 May 2019 01:47:51 UTC +00:00,
updated_at: Mon, 27 May 2019 01:47:51 UTC +00:00>]
Fantastic! Our basic model layer is up-and-running.
Now that we’ve created our model layer, we’re ready to start rigging up the screens that make up our simple ecommerce app. We’ll start with the simplest part: listing all our teddies.
Let’s use rails generate to create a new controller.
rails g controller teddies
Running via Spring preloader in process 69178
create app/controllers/teddies_controller.rb
invoke erb
create app/views/teddies
invoke test_unit
create test/controllers/teddies_controller_test.rb
This creates:
- teddies_controller.rb which implements
TeddiesController: the handler forGET /teddiesandGET /teddies/<id>. - An empty folder at
app/views/teddieswhere the template files go - teddies_controller_test.rb which implements
TeddiesControllerTest: which implements integration tests forTeddiesConstroller
We’re going to skip the integration tests for now, however, if we were doing this the proper way, we would start with the integration tests. So-called “Test-Driven Development” (TDD) isn’t just sound practice, it’s also a way to sketch out, at a high-level, how everything in your application should work. It’s quicker task than implementation. and helps you stay on track because the tests serve as a sort-of development checklist, presenting the specific features to develop, and the most sensible order to implement them in.
Ok, so we have our TeddiesController which will handle GET /teddies and GET /teddies/<id> , but it won’t work until we alter our routing configuration. Open config/routes.rb:
Rails.application.routes.draw do
devise_for :users
root 'teddies#index'
resources :teddies, only: [:index, :show]
endReplace:
root to: 'pages#home'with:
root 'teddies#index'Here, we are using the root keyword to plug GET / into TeddiesContoller#index .
Next, add:
resources :teddies, only: [:index, :show]This plugs:
GET /teddiesintoTeddiesController#indexGET /teddies/:idintoTeddiesController#show
Now that we’ve configured our router, lets implement the route handlers. Open teddies_controller.rb:
# app/controllers/teddies_controller.rb
class TeddiesController < ApplicationController
endRight now, when our router passes GET /teddies & GET /teddies/:id to TeddiesController… ain’t nothing gonna happen:
GET /teddies
AbstractController::ActionNotFound (The action 'index' could not be found for TeddiesController):
We told our router to pass those requests to #index and #show, so lets implement those:
# app/controllers/teddies_controller.rb
skip_before_action :authenticate_user!
def index
@teddies = Teddy.all
end
def show
@teddy = Teddy.find(params[:id])
endIn the index handler we’re fetching all instances of Teddy and loading them into a template (which we will create shortly).
In show we’re reading the :id parameter (e.g. for the request GET /teddies/1 params[:id] would yield 1) as the argument to Teddy.find. This will return one Teddy or throw an exception if a Teddy with that id cannot be found.
By default, Rails will attempt to load a view for each action, using the following pattern:
app/view/:controller/:action.html.erb
# app/view/teddies/index.html.erb
# app/view/teddies/show.html.erbJust one thing… those don’t exist yet! Create them with touch or with your text editor, starting with app/view/teddies/index.html.erb:
<div class="container">
<div class="row">
<h1>Teddies</h1>
<% @teddies.each do |teddy| %>
<div class="col-sm-6 col-md-4">
<div class="thumbnail">
<%= link_to image_tag(teddy.photo_url), teddy_path(teddy) %>
<div class="caption">
<h3><%= link_to teddy.name, teddy_path(teddy) %></h3>
<p><%= teddy.category.name %></p>
<p>$$$</p>
</div>
</div>
</div>
<% end %>
</div>
</div>Next, open your browser and head to http://localhost:3000/teddies.
If we click one of the <a> tags created in our template via:
<h3><%= link_to teddy.name, teddy_path(teddy) %></h3>But, when our application passes GET /teddies/:id to TeddiesController#show we get an error, because we haven’t created the view yet:
ActionController::UnknownFormat (TeddiesController#show is missing a template for this request format and variant.
So, let’s overcome this error by creating app/view/teddies/index.html.erb:
<div class="container">
<div class="row">
<div class="col-sm-12">
<h1><%= @teddy.name %></h1>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-6">
<%= image_tag(@teddy.photo_url, width: '100%') %>
</div>
<div class="col-sm-12 col-md-6">
<p>Some awesome description of our amazing teddy.</p>
<p>$$$</p>
</div>
</div>
</div>Now, when we follow the link GET /teddies/:id our application responds with the rendered template!
For an e-commerce site to make any sense, the products should have prices. Let’s add them.
First, in our gemfile we’ll add a new dependency: money-rails
# Gemfile
gem 'money-rails'money-rails provides Rails an interface to ruby-money which will come in handy shortly.
Install the new dependencies:
bundle
money-rails provides an initialiser interface where we can configure how Money should work (e.g. setting the default currency.
Create config/initializers/money.rb:
# config/initializers/money.rb
MoneyRails.configure do |config|
config.default_currency = :aud # or :gbp, :usd, etc.
config.locale_backend = nil
end
Note: we’re going to add config.locale_backend = nil otherwise we’ll get a deprecation warning.
Now, let’s add a price column to our Teddies model. With rails generate run:
rails g migration AddPriceToTeddies
Open the newly created migration:
class AddPriceToTeddies < ActiveRecord::Migration[5.2]
def change
end
endAdd:
add_monetize :teddies, :price, currency: { present: false }Then run rake db:migrate:
rake db:migrate
== 20190527032121 AddPriceToTeddies: migrating ================================
-- add_column(:teddies, "price_cents", :integer, {:null=>false, :default=>0})
-> 0.0034s
== 20190527032121 AddPriceToTeddies: migrated (0.0035s) =======================
Finally, enable money on our Teddy model by opening app/models/teddy.rb and adding:
class Teddy < ApplicationRecord
...
monetize :price_cents
endFinally, let’s update our seeds file, so that our Teddies don’t cost NULL cents.
Teddy.create!(price: 100, ...)Next, lets re-load our test data:
rake db:seed
Cleaning database...
Creating categories...
Creating teddies...
Finished!We can check the data using pry… or we could just say a little prayer and hope it works.
Something to note about money and monetize is that setting the price field as 100 would yield the stored value 10000 cents. So yeah, just beware of that.
Finally, we’re ready to update our views to show the prices.
In app/views/teddies/index.html.erb add:
<p>Amount: <%= humanized_money_with_symbol(teddy.price) %></p>And in app/views/teddies/index.html.erb add:
<p>Amount: <%= humanized_money_with_symbol(@teddy.price) %></p>