Scaffolding means purely and simply that a basic scaffold for an application is created with a generator. This scaffold not only contains the model but also a simple Web GUI (views) and of course a controller. The programming paradigm used for this is REST (Representational State Transfer).
You can find a definition of REST at wikipedia.org/wiki/Representational_state_transfer. My super short version: the inventor Roy Fielding described in 2000 how you can access data with a simple set of rules within the concept of CRUD and the specification of the Hypertext Transfer Protocol (HTTP). CRUD is the abbreviation for Create (SQL: INSERT), Read (SQL: SELECT), Update (SQL: UPDATE) and Delete (SQL: Delete). This created URLs that are easy to read for humans and have a certain logic. In this chapter, you will see examples showing the individual paths for the different CRUD functions.
I think the greatest frustration with Rails arises regularly from the fact that many beginners use scaffolding to get quick results without having proper basic knowledge of Ruby and without knowing what ActiveRecord is. They don’t know what to do next. Fortunately, you have worked your way through the chapters "Ruby Basics", "First Steps with Rails" and "ActiveRecord", so you will be able to understand and use scaffolding straight away.
Scaffolding uses redirects and flash messages. So we have to make a little detour first to understand scaffolding.
The name says it all, really: redirects are commands that you can use within the controller to skip, i.e. redirect, to other web pages.
Note
|
A redirect returns to the browser the response 302 Moved
with the new target. So each redirect does a roundtrip to
the browser and back.
|
Let’s create a new Rails project for a suitable example:
$ rails new redirect_example
[...]
$ cd redirect_example
Before we can redirect, we need a controller with at least two different methods. Off we go with a ping pong example:
$ rails generate controller Game ping pong
Running via Spring preloader in process 51759
create app/controllers/game_controller.rb
route get 'game/pong'
route get 'game/ping'
invoke erb
create app/views/game
create app/views/game/ping.html.erb
create app/views/game/pong.html.erb
invoke test_unit
create test/controllers/game_controller_test.rb
invoke helper
create app/helpers/game_helper.rb
invoke test_unit
invoke assets
invoke coffee
create app/assets/javascripts/game.coffee
invoke scss
create app/assets/stylesheets/game.scss
The controller app/controllers/game_controller.rb
has the following
content:
class GameController < ApplicationController
def ping
end
def pong
end
end
Now for the redirect: how can we achieve that we get immediately
redirected to the method pong when we go to
http://localhost:3000/game/ping? Easy, you will say, we just change the
route in config/routes.rb
. And you are right. So we don’t necessarily
need a redirect. But if we want to process something else in the method
ping before redirecting, then this is only possible by using a
redirect_to
in the controller app/controllers/game_controller.rb
:
class GameController < ApplicationController
def ping
logger.info '+++ Example +++'
redirect_to game_pong_path
end
def pong
end
end
But what is game_pong_path
? Let’s have a look a the routes generated
for this Rails application:
$ rails routes
Prefix Verb URI Pattern Controller#Action
game_ping GET /game/ping(.:format) game#ping
game_pong GET /game/pong(.:format) game#pong
Note
|
As you can see, the route to the action redirect_to :action => 'pong' |
I will explain the details and the individual options of the redirect later in the context of each specific case. For now, you just need to know that you can redirect not just to another method, but also to another controller or an entirely different web page.
When we try to go to http://localhost:3000/game/ping we are automatically redirected to http://localhost:3000/game/pong and in the log output we see this:
Started GET "/game/ping" for 127.0.0.1 at 2015-04-15 17:50:04 +0200
Processing by GameController#ping as HTML
+++ Example +++
Redirected to http://localhost:3000/game/pong
Completed 302 Found in 14ms (ActiveRecord: 0.0ms)
Started GET "/game/pong" for 127.0.0.1 at 2015-04-15 17:50:04 +0200
Processing by GameController#pong as HTML
Rendered game/pong.html.erb within layouts/application (2.1ms)
Completed 200 OK in 2128ms (Views: 2127.4ms | ActiveRecord: 0.0ms)
If you want to redirect the user of your web application to the page he
has just been you can use redirect_to :back
. This is very useful in a
scenario where your user first has to login to get access to a specific
page.
In my eyes, the term “flash messages” is somewhat misleading. Almost anyone would associate the term “Flash” with more or less colorful web pages that were implemented with the Adobe Shockwave Flash Plug-in. But in Ruby on Rails, flash messages are something completely different. They are messages that are displayed, for example on the new page after a redirect (see section Redirects).
Flash messages are good friends with redirects. The two often work together in a team to give the user feedback on an action he just carried out. A typical example of a flash message is the system feedback when a user has logged in. Often the user is redirected back to the original page and gets the message “You are now logged in.”
As an example, we are once more constructing the ping pong scenario from section "Redirects":
$ rails new pingpong
[...]
$ cd pingpong
$ rails generate controller Game ping pong
[...]
We fill the app/controllers/game_controller.rb
with the following
content:
class GameController < ApplicationController
def ping
redirect_to game_pong_path, notice: 'Ping-Pong!'
end
def pong
end
end
Now we start the Rails web server with rails server
and use the
browser to go to http://localhost:3000/game/ping. We are redirected from
ping to pong. But the flash message "Ping-Pong!" is nowhere to be seen.
We first need to expand app/views/layouts/application.html.erb
:
<!DOCTYPE html>
<html>
<head>
<title>RedirectExample</title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<% flash.each do |name, message| %>
<p><i><%= "#{name}: #{message}" %></i></p>
<% end %>
<%= yield %>
</body>
</html>
Now we see the flash message at the top of the page when we go to http://localhost:3000/game/ping in the browser:
If we go to http://localhost:3000/game/pong we still see the normal Pong page. But if we go to http://localhost:3000/game/ping we are redirected to the Pong page and then the flash message is displayed at the top.
Tip
|
If you do not see a flash message that you were expecting, first check in the view to see if the flash message is output there. |
Flash messages are automagically passed to the view in a hash. By
default, there are three different types: error
, warning
and
notice
. You can also invent your own category and then get it in the
view later.
You can set a flash message by writing the hash directly too:
flash[:notice] = 'Ping-Pong!'
Please have a look at the official documentation at http://guides.rubyonrails.org/action_controller_overview.html#the-flash for more information.
You may wonder why there are flash messages in the first place. Couldn’t you just build them yourself if you need them? Yes, indeed. But flash messages have the advantage that they offer a defined approach that is the same for any programmer. So you don’t need to start from scratch every single time you need one.
Let’s first use scaffolding to create a list of products for an online shop. First, we need to create a new Rails application:
$ rails new scaffold-shop
[...]
$ cd scaffold-shop
Let’s look at the scaffolding options:
$ rails generate scaffold
Usage:
rails generate scaffold NAME [field[:type][:index] field[:type][:index]]
[options]
[...]
Examples:
`rails generate scaffold post`
`rails generate scaffold post title body:text published:boolean`
`rails generate scaffold purchase amount:decimal tracking_id:integer:uniq`
`rails generate scaffold user email:uniq password:digest`
I’ll keep it short: for our current state of knowledge, we can use
rails generate scaffold
just like rails generate model
. Let’s create
the scaffold for the products:
$ rails generate scaffold product name 'price:decimal{7,2}'
Running via Spring preloader in process 52261
invoke active_record
create db/migrate/20170323161553_create_products.rb
create app/models/product.rb
invoke test_unit
create test/models/product_test.rb
create test/fixtures/products.yml
invoke resource_route
route resources :products
invoke scaffold_controller
create app/controllers/products_controller.rb
invoke erb
create app/views/products
create app/views/products/index.html.erb
create app/views/products/edit.html.erb
create app/views/products/show.html.erb
create app/views/products/new.html.erb
create app/views/products/_form.html.erb
invoke test_unit
create test/controllers/products_controller_test.rb
invoke helper
create app/helpers/products_helper.rb
invoke test_unit
invoke jbuilder
create app/views/products/index.json.jbuilder
create app/views/products/show.json.jbuilder
create app/views/products/_product.json.jbuilder
invoke test_unit
create test/system/products_test.rb
invoke assets
invoke coffee
create app/assets/javascripts/products.coffee
invoke scss
create app/assets/stylesheets/products.scss
invoke scss
create app/assets/stylesheets/scaffolds.scss
As you can see, rails generate scaffold
has already created the model.
So we can directly call rails db:migrate
:
$ rails db:migrate
== 20151218150127 CreateProducts: migrating ===================================
-- create_table(:products)
-> 0.0023s
== 20151218150127 CreateProducts: migrated (0.0024s) ==========================
Let’s create the first six products in the db/seeds.rb
.
Product.create(name: 'Apple', price: 1)
Product.create(name: 'Orange', price: 1)
Product.create(name: 'Pineapple', price: 2.4)
Product.create(name: 'Marble cake', price: 3)
Populate with the example data:
$ rails db:seed
rails generate scaffold
has created a route (more on this later in
the chapter "Routes"), a controller and several views for us.
We could also have done all of this manually. Scaffolding is merely an automatism that does the work for us for some basic things. This is assuming that you always want to view, create and delete records.
Without diving too deeply into the topic routes, let’s just have a quick
look at the available routes for our example. You need to run
rails routes
:
$ rails routes
Prefix Verb URI Pattern Controller#Action
products GET /products(.:format) products#index
POST /products(.:format) products#create
new_product GET /products/new(.:format) products#new
edit_product GET /products/:id/edit(.:format) products#edit
product GET /products/:id(.:format) products#show
PATCH /products/:id(.:format) products#update
PUT /products/:id(.:format) products#update
DELETE /products/:id(.:format) products#destroy
These are all the routes and consequently URLs available in this Rails
application. All routes invoke actions (in other words, methods) in the
ProductsController
.
Now it’s about time we had a look at the file
app/controllers/products_controller.rb
. Scaffold automatically creates
the methods index, show, new, create, update and destroy. These methods
or actions are called by the routes.
Here is the content of app/controllers/products_controller.rb
class ProductsController < ApplicationController
before_action :set_product, only: [:show, :edit, :update, :destroy]
# GET /products
# GET /products.json
def index
@products = Product.all
end
# GET /products/1
# GET /products/1.json
def show
end
# GET /products/new
def new
@product = Product.new
end
# GET /products/1/edit
def edit
end
# POST /products
# POST /products.json
def create
@product = Product.new(product_params)
respond_to do |format|
if @product.save
format.html { redirect_to @product, notice: 'Product was successfully created.' }
format.json { render :show, status: :created, location: @product }
else
format.html { render :new }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /products/1
# PATCH/PUT /products/1.json
def update
respond_to do |format|
if @product.update(product_params)
format.html { redirect_to @product, notice: 'Product was successfully updated.' }
format.json { render :show, status: :ok, location: @product }
else
format.html { render :edit }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
end
end
# DELETE /products/1
# DELETE /products/1.json
def destroy
@product.destroy
respond_to do |format|
format.html { redirect_to products_url, notice: 'Product was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_product
@product = Product.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def product_params
params.require(:product).permit(:name, :price)
end
end
Let us take a moment and go through this controller.
A before_action
calls a private method to set an instance variable
@product
for the actions :show, :edit, :update and :destroy. That DRYs
it up nicely:
before_action :set_product, only: [:show, :edit, :update, :destroy]
[...]
private
# Use callbacks to share common setup or constraints between actions.
def set_product
@product = Product.find(params[:id])
end
[...]
The index
method sets the instance variable @products
. It contains
the result of Product.all
.
# GET /products
# GET /products.json
def index
@products = Product.all
end
The show
method doesn’t do anything. set_product
before_action
already set the instance variable @product
. So there is not more to
do.
# GET /products/1
# GET /products/1.json
def show
end
The edit
method doesn’t do anything. the set_product
before_action
already set the instance variable @product
. So there is not more to
do.
# GET /products/1/edit
def edit
end
The create
method uses Product.new
to create a new instance of
Product and stores it in @product
. The private method product_params
is used to filter the trusted parameters with a white list. When
@product
was successfully saved a redirect
to the show
action is
initiated for html requests. If a validation error occurred the new
action will be rendered.
# POST /products
# POST /products.json
def create
@product = Product.new(product_params)
respond_to do |format|
if @product.save
format.html { redirect_to @product, notice: 'Product was successfully created.' }
format.json { render :show, status: :created, location: @product }
else
format.html { render :new }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
end
end
[...]
# Never trust parameters from the scary internet, only allow the white list through.
def product_params
params.require(:product).permit(:name, :price)
end
The update
method tries to update @product with the product_params
.
The private method product_params
is used to filter the trusted
parameters with a white list. When @product
was successfully updated a
redirect
to the show
action is initiated for html requests. If a
validation error occured the edit
action will be rendered.
# PATCH/PUT /products/1
# PATCH/PUT /products/1.json
def update
respond_to do |format|
if @product.update(product_params)
format.html { redirect_to @product, notice: 'Product was successfully updated.' }
format.json { render :show, status: :ok, location: @product }
else
format.html { render :edit }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
end
end
[...]
# Never trust parameters from the scary internet, only allow the white list through.
def product_params
params.require(:product).permit(:name, :price)
end
The destroy
method destroys @product
and redirects an html request
to the index
action.
# DELETE /products/1
# DELETE /products/1.json
def destroy
@product.destroy
respond_to do |format|
format.html { redirect_to products_url, notice: 'Product was successfully destroyed.' }
format.json { head :no_content }
end
end
Now we start the Rails web server:
$ rails server
=> Booting Puma
=> Rails 5.1.0 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.8.2 (ruby 2.4.0-p0), codename: Sassy Salamander
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
Now a little drum roll … dramatic suspense … launch the web browser and go to the URL http://localhost:3000/products. You can see the list of products as simple web page.
If you now click the link New Product, you will see an input form for a new record:
Use your browser’s Back button to go back and click on the Show link in the first line. You will then see the following page:
If you now click Edit, you will see the editing view for this record:
And if you click Destroy on the Index page, you can delete a record after confirming the message that pops up. Isn’t that cool?! Within less than 10 minutes, you have written a Web application that allows you to *c*reate, *r*ead/*r*etrieve, *u*pdate and *d*elete/*d*estroy records CRUD. That is the scaffolding magic. You can save a lot of time.
You can probably guess, but let’s have a look at the directory
app/views/products
anyway:
$ tree app/views/products/
app/views/products/
├── _form.html.erb
├── _product.json.jbuilder
├── edit.html.erb
├── index.html.erb
├── index.json.jbuilder
├── new.html.erb
├── show.html.erb
└── show.json.jbuilder
There are two different file extensions. The html.erb
is for HTML
requests and the json.jbuilder
is for JSON requests.
For index
, edit
, new
and show
the corresponding views are
located there. As new
and edit
both require a form for editing the
data, this is stored in the partial _form.html.erb
in accordance with
the principle of DRY (*D*on’t *R*epeat
*Y*ourself) and integrated in new.html.erb
and edit.html.erb
with a
<%= render 'form' %>
.
Let’s open the file app/views/products/index.html.erb
:
<p id="notice"><%= notice %></p>
<h1>Products</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @products.each do |product| %>
<tr>
<td><%= product.name %></td>
<td><%= product.price %></td>
<td><%= link_to 'Show', product %></td>
<td><%= link_to 'Edit', edit_product_path(product) %></td>
<td><%= link_to 'Destroy', product, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Product', new_product_path %>
You are now an old hand when it comes to ERB, so you’ll be able to read and understand the code without any problems.
In the views generated by the scaffold generator, you first came across
the helper link_to
. This creates <a hre …>
links. You can of
course also enter a link manually via <a href="…">
in the erb, but
for links within a Rails project, link_to
is more practical, because
you can use the names of the routes as a target. The code becomes much
easier to read. In the above example, there are the following routes:
$ rails routes
Prefix Verb URI Pattern Controller#Action
products GET /products(.:format) products#index
POST /products(.:format) products#create
new_product GET /products/new(.:format) products#new
edit_product GET /products/:id/edit(.:format) products#edit
product GET /products/:id(.:format) products#show
PATCH /products/:id(.:format) products#update
PUT /products/:id(.:format) products#update
DELETE /products/:id(.:format) products#destroy
The first part of this route is the name of the route. With a new call,
this is new_product
. A link to new_product
looks like this in the
erb code (you can see it at the end of the file
app/views/products/index.html.erb
):
<%= link_to 'New Product', new_product_path %>
In the HTML code of the generated page (http://localhost:3000/products) you can see the result:
<%= link_to 'New Product', new_product_path %>
With link_to
you can also link to resources within a RESTful resource.
Again, you can find examples for this in
app/views/products/index.html.erb
. In the table, a show
, an edit
and a destroy
link is rendered for each product
:
<tbody>
<% @products.each do |product| %>
<tr>
<td><%= product.name %></td>
<td><%= product.price %></td>
<td><%= link_to 'Show', product %></td>
<td><%= link_to 'Edit', edit_product_path(product) %></td>
<td><%= link_to 'Destroy', product, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
From the resource and the selected route, Rails automatically determines
the required URL and the required HTTP verb (in other words, whether it
is a POST, GET, PUT or DELETE). For index and show calls, you need to
observe the difference between singular and plural.
link_to 'Show', product
links to a single record and
link_to 'Show', products_path
links to the index view.
Whether the name of the route is used with or without the suffix _path
in link_to
depends on whether Rails can `derive'' the route from the
other specified information. If only one object is specified (in our
example, the variable `product
), then Rails automatically assumes that
it is a show route.
Examples:
ERD-Code | Explanation |
---|---|
|
Link to the first product. |
|
Link to the Web interface where a new product can be created. |
|
Link to the form where the first product can be edited. |
|
Link to deleting the first product. |
In the partial used by new
and edit
,
app/views/products/_form.html.erb
, you will find the following code
for the product form:
<%= form_with(model: product, local: true) do |f| %>
<% if product.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(product.errors.count, "error") %> prohibited this product from being saved:</h2>
<ul>
<% product.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="field">
<%= f.label :price %>
<%= f.text_field :price %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
In a block, the helper form_for
takes care of creating the HTML form
via which the user can enter the data for the record or edit it. If you
delete a complete <div class="field">
element here, this can no longer
be used for input in the web interface. I am not going to comment on all
possible form field variations at this point. The most frequently used
ones will appear in examples later on and be explained then (if they are
not self-explanatory).
Note
|
You can find an overview of all form helpers at http://guides.rubyonrails.org/form_helpers.html |
When using validations in the model, any validation errors that occur are displayed in the following code at the head of the form:
<% if product.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(product.errors.count, "error") %> prohibited this product from being saved:</h2>
<ul>
<% product.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
Let’s add a small validation to the app/models/product.rb
model:
class Product < ApplicationRecord
validates :name,
presence: true
end
When ever somebody wants to save a product which doesn’t have a name Rails will show this Flash Error:
By default, Rails’ scaffolding generates not just access via HTML for
human users, but also a direct interface for machines. The same methods
index
, show
, new
, create
, update
and destroy
can be called
via this interface, but in a format that is easier to read for machines.
As an example, we will demonstrate the index
action via which all data
can be read in one go. With the same idea, data can be removed
(destroy
) or edited (update
).
JSON (see wikipedia.org/wiki/Json) seems to be the new cool kid. So we use JSON.
If you do not require machine-readable access to data, you can remove
these lines in the file Gemfile
(followed by the command bundle
).
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5'
Of course you can delete the format.json
lines manually too. But
please don’t forget to delete the JSON view files too.
JSON as Default
Right at the beginning of app/controllers/products_controller.rb
you
will find the entry for the index action:
# GET /products
# GET /products.json
def index
@products = Product.all
end
The code is straightforward. In the instance variable @products
, all
products are saved. The view app/views/products/index.json.jbuilder
contains the following code to render the JSON:
json.array! @products, partial: 'products/product', as: :product
It renders the partial _product.json.jbuilder:
json.extract! product, :id, :name, :price, :created_at, :updated_at
json.url product_url(product, format: :json)
You can use your browser to fetch the JSON output. Just open http://localhost:3000/products.json and view the result. I installed a JSON view extension in my Chrome browser to get a nicer format.
If you do not want the JSON output, you need to delete the
json.jbuilder
files.
JSON and XML Together
If you ever need a JSON and XML interface in a Rails application, you
just need to specify both variants in the controller in the block
respond_to
. Here is an example with the
app/controllers/products_controller.rb
in the index
action:
# GET /products
# GET /products.json
# GET /products.xml
def index
@products = product.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: @products }
format.xml { render xml: @products }
end
end
You should never use scaffolding just for the sake of it. There are Rails developers who never use scaffolding and always build everything manually. I find scaffolding quite useful for quickly getting into a new project. But it is always just the beginning.
Let’s assume we need a web page quickly with which we can list products and represent them individually. But we do not require an editing or deleting function. In that case, a large part of the code created via scaffold would be useless and have to be deleted. Let’s try it out as follows:
$ rails new read-only-shop
[...]
$ cd read-only-shop
$ rails generate scaffold product name 'price:decimal{7,2}'
[...]
$ rails db:migrate
[...]
Now create the db/seeds.rb
with some demo products:
Product.create(name: 'Apple', price: 1)
Product.create(name: 'Orange', price: 1)
Product.create(name: 'Pineapple', price: 2.4)
Product.create(name: 'Marble cake', price: 3)
And populate it with this data:
$ rails db:seed
As we only need index
and show
, we should delete the not required
views:
$ rm app/views/products/_form.html.erb
$ rm app/views/products/new.html.erb
$ rm app/views/products/edit.html.erb
The json.jbuilder
views are not needed either:
$ rm app/views/products/*.json.jbuilder
The file app/controllers/products_controller.rb
can be simplified with
an editor. It should look like this:
class ProductsController < ApplicationController
before_action :set_product, only: [:show]
# GET /products
# GET /products.json
def index
@products = Product.all
end
# GET /products/1
# GET /products/1.json
def show
end
private
# Use callbacks to share common setup or constraints between actions.
def set_product
@product = Product.find(params[:id])
end
end
We only need the routes for index
and show
. Please open the file
config/routes.rb
and edit it as follows:
Rails.application.routes.draw do
resources :products, only: [:index, :show]
end
A rails routes
shows us that really only index
and show
are routed
now:
$ rails routes
Prefix Verb URI Pattern Controller#Action
products GET /products(.:format) products#index
product GET /products/:id(.:format) products#show
If we now start the server rails server
and go to the URL
http://localhost:3000/products, we get an error message.
The same message will be displayed in the log:
$ rails server
=> Booting Puma
=> Rails 5.1.0 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.8.2 (ruby 2.4.0-p0), codename: Sassy Salamander
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
Started GET "/products" for 127.0.0.1 at 2017-03-23 17:47:43 +0100
(0.2ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
Processing by ProductsController#index as HTML
Rendering products/index.html.erb within layouts/application
Product Load (0.2ms) SELECT "products".* FROM "products"
Rendered products/index.html.erb within layouts/application (126.3ms)
Completed 500 Internal Server Error in 149ms (ActiveRecord: 0.7ms)
ActionView::Template::Error (undefined method `edit_product_path' for #<#<Class:0x007f98eb1e8148>:0x007f98ea6620d0>
Did you mean? edit_polymorphic_path):
17: <td><%= product.name %></td>
18: <td><%= product.price %></td>
19: <td><%= link_to 'Show', product %></td>
20: <td><%= link_to 'Edit', edit_product_path(product) %></td>
21: <td><%= link_to 'Destroy', product, method: :delete, data: { confirm: 'Are you sure?' } %></td>
22: </tr>
23: <% end %>
app/views/products/index.html.erb:20:in `block in _app_views_products_index_html_erb___4554496912710881403_70147378203280'
app/views/products/index.html.erb:15:in `_app_views_products_index_html_erb___4554496912710881403_70147378203280'
The error message states that we call an undefined method
edit_product_path
in the view app/views/products/index.html.erb
. As
we only route index
and show
now, there are no more edit
,
destroy
or new
methods any more. So we need to adapt the file
app/views/products/index.html.erb
in the editor as follows:
<h1>Products</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th></th>
</tr>
</thead>
<tbody>
<% @products.each do |product| %>
<tr>
<td><%= product.name %></td>
<td><%= product.price %></td>
<td><%= link_to 'Show', product %></td>
</tr>
<% end %>
</tbody>
</table>
And while we are at it, we also edit the
app/views/products/show.html.erb
accordingly:
<p>
<strong>Name:</strong>
<%= @product.name %>
</p>
<p>
<strong>Price:</strong>
<%= @product.price %>
</p>
<%= link_to 'Back', products_path %>
Now our application is finished. Start the Rails server with
rails server
and open the URL http://localhost:3000/products in the
browser.
Note
|
In this example, I am not commenting on the required changes in the tests, as this is not an exercise for test driven development but meant to demonstrate a way of working with scaffolding. TDD developers will quickly be able to adapt the tests. |