Practical 7
0. Finish Practical Sheet 6.
1. Taking Orders.
In this practical, we add to our online store the functionality
that allows us to record the buyer’s details and thus turn a
shopping cart into an order. As a first step in that direction,
generate the scaffold for an Order
model with the following components:
Order |
|
---|---|
name |
string |
address |
text |
email |
string |
pay_type |
integer |
In order to relate the integer
payment types to human readable
names, add the following enum
declaration to the generated file that
contains the order model:
enum pay_type: {
"Cheque" => 0,
"Credit Card" => 1,
"Purchase Order" => 2
}
2.
Generate a migration to add a column order
of type references
to the line_items
table.
Write the name of the generated migration file into the comment box below, as evidence that you have attended this practical.
In this generated file, replace the add_reference
command by the two lines
add_reference :line_items, :order, null: true, foreign_key: true
change_column :line_items, :cart_id, :integer, null: true
3.
Connect the orders
table with the line_items
table by
adding suitable has_many
and belongs_to
declarations (to which files?)
Add a parameter optional: true
to each belongs_to
call, to allow
this field to be empty.
As in the cart
model,
add the parameter dependent: :destroy
to the has_many
call:
when an order is destroyed, all its line items must go as well.
Then run the migrations:
rake db:migrate
4.
Add a 'Checkout'
button next to the 'Empty Cart'
button (which file?):
<%= button_to "Checkout", new_order_path, method: :get, class: "checkout" %>
Then put both buttons inside a <div>
element with class
attribute
"actions"
.
5. There’s no point checking out an empty cart. In order to
decide this, the orders
controller needs access to the current cart.
For this, add
include CurrentCart
before_action :set_cart, only: [:new, :create]
before_action :ensure_cart_isnt_empty, only: :new
to the top of this controller (which file?).
6.
Then, after the private
declaration of this controller,
one can prevent empty orders from being placed by adding this code:
def ensure_cart_isnt_empty
if @cart.line_items.empty?
redirect_to store_url, notice: "Your cart is empty"
end
end
7. This expected behavior should be formulated as a test. In the
file orders_controller_test.rb
add:
test "requires item in cart" do
get new_order_url
assert_redirected_to store_path
assert_equal flash[:notice], 'Your cart is empty'
end
Also modify the "should get new"
test so that it reads:
post line_items_url, params: { product_id: products(:ruby).id }
get new_order_url
assert_response :success
8. In the new setup, a line item now belongs to a cart, or to an order. In other words, at a given point in time, a line item might not have a cart (or an order) it belongs to. This optionality needs to be registered with the line items model. Change the lines that define the model relationships to
belongs_to :order, optional: true
belongs_to :cart, optional: true
8a. If all works fine, commit the changes to your local git
repository, and push them to the github
cloud.
git add .
git commit -m "added checkout button"
git push
9. Forms.
Replace the contents of the order
’s new
view with:
<section class="depot_form">
<h1>Please Enter Your Details</h1>
<%= render 'form', order: @order %>
</section>
10.
In the form itself,
that is in the partial file _form.html.erb
,
restrict the name
and email
input fields to
length 40 (size: 40
), and the address to 3 rows and 40
columns (rows: 3, cols: 40
). Turn the pay_type
field into a selection
and add an id
attribute like so:
<%= form.select :pay_type, Order.pay_types.keys, id: :order_pay_type,
prompt: "Select a payment method" %>
and pass the argument "Place Order"
to the submit button.
Also, add the id
attributes :order_name
, :order_address
and :order_email
to the other three fields, e.g.,
<%= form.text_field :name, id: :order_name, size: 40 %>
11. Add these formatting rules to the end of the stylesheet
app/assets/stylesheets/application.scss
.
.depot_form {
padding: 0 1em;
h1 {
font-size: 1.99em;
line-height: 1.41em;
margin-bottom: 0.5em;
padding: 0;
}
.field, .actions {
margin-bottom: 0.5em;
padding: 0;
}
.actions {
text-align: right;
padding: 1em 0;
}
input, textarea, select, option {
border: solid thin #888;
box-sizing: border-box;
font-size: 1em;
padding: 0.5em;
width: 100%;
}
label {
padding: 0.5em 0;
}
input[type="submit"] {
background-color: #bfb;
border-radius: 0.354em;
border: solid thin #888;
color: black;
font-size: 1.41em;
font-weight: bold;
padding: 0.354em 1em;
}
input[type="submit"]:hover {
background-color: #9d9;
}
// Also, clean up the error styling
#error_explanation {
background-color: white;
border-radius: 1em;
border: solid thin red;
margin-bottom: 0.5em;
padding: 0.5em;
width: 100%;
h2 {
background: none;
color: red;
font-size: 1.41em;
line-height: 1.41em;
padding: 1em;
}
ul {
margin-top: 0;
li {
color: red;
font-size: 1em;
}
}
}
.field_with_errors {
background: none;
color: red;
width: 100%;
label {
font-weight: bold;
}
label::before {
content: "! ";
}
input,textarea {
background: pink;
}
}
}
Now try it out: put some items into your cart and proceed to the checkout! How does it look?
12. As this form is intended for the outside world, the application
cannot rely on all input always being valid.
Add validation (to which model?) to ensure that the fields name
, address
, email
are all present and filled at submit time.
Also make sure, the pay_type
is one of the choices in the dropdown menu:
validates :pay_type, inclusion: pay_types.keys
13. Modifying the validation rules almost always requires us to
adjust the fixtures so that they pass validation. In the fixtures file
orders.yml
modify fixture one
to be:
one:
name: Dave Thomas
address: MyText
email: dave@example.org
pay_type: Cheque
Then, in line_items.yml
, modify fixture two
to be:
two:
product: ruby
order: one
13a. Test the application. If all works fine, commit the changes to
your local git
repository.
14. Create Order.
Next comes the create
action in the orders
controller. It needs to
-
Get the values from the order form to populate a new
Order
model object. -
Add the line items from the cart to that order.
-
Validate and save the order object.
-
In case of failure, display informative error messages that help the user to fix the problems.
-
In case of success, delete the cart, redisplay the catalog page and a message confirming receipt of the order.
This is done as follows.
In the Order
model, define a method that moves items from a cart to this order:
def add_line_items_from_cart(cart)
cart.line_items.each do |item|
item.cart_id = nil
line_items << item
end
end
(Note that the append method <<
automatically sets
item.order_id
. Also note how item.cart_id
is set to nil
to prevent the item from disappearing when the cart will be destroyed.)
In the create
method of the orders controller,
add the line
@order.add_line_items_from_cart(@cart)
after the assignment @order = ...
,
and replace the format.html
line under if @order.save
by
Cart.destroy(session[:cart_id])
session[:cart_id] = nil
format.html {
redirect_to store_url, notice: 'Thank you for your order.'
}
The new redirect requires us to modify the corresponding assertion
in the "should create order"
test: replace its last line by
assert_redirected_to store_path
14a. Now try it out and run the tests. If all works fine, commit
the changes to your local git
repository, and push them to the
github
cloud.
15. Atom feed. Feeds are news broadcasting services. You can subscribe to a feed and automatically receive regular updates. Here the good news to be spread is that someone ordered some product. In order to enable this service for our online store, we need to set up a new action. Recall that an action consists of three things: a controller method, a view template, and a route. First add the following method to the products controller:
def who_bought
@product = Product.find(params[:id])
@latest_order = @product.orders.order(:updated_at).last
if stale?(@latest_order)
respond_to do |format|
format.atom
end
end
end
To make a product aware of all the orders it is contained in, add a line
has_many :orders, through: :line_items
to the product model (which file?)
16.
The command format.atom
expects to find a view template
who_bought.atom.builder
in the views/products
finder
(where builder
is a template preprocessor, much like erb
, but better suited for
producing XML, and per default associated to .atom
files).
Create such a file with the following content.
atom_feed do |feed|
feed.title "Who bought #{@product.title}"
feed.updated @latest_order.try(:updated_at)
@product.orders.each do |order|
feed.entry(order) do |entry|
entry.title "Order #{order.id}"
entry.summary type: 'xhtml' do |xhtml|
xhtml.p "Shipped to #{order.address}"
xhtml.table do
xhtml.tr do
xhtml.th 'Product'
xhtml.th 'Quantity'
xhtml.th 'Total Price'
end
order.line_items.each do |item|
xhtml.tr do
xhtml.td item.product.title
xhtml.td item.quantity
xhtml.td number_to_currency item.total_price
end
end
xhtml.tr do
xhtml.th 'total', colspan: 2
xhtml.th number_to_currency order.line_items.map(&:total_price).sum
end
end
xhtml.p "Paid by #{order.pay_type}"
end
entry.author do |author|
author.name order.name
author.email order.email
end
end
end
end
and briefly try and make sense of this code.
17. Finally, add a matching route by changing
resources :products
in the file config/routes.rb
to:
resources :products do
get :who_bought, on: :member
end
18. Subscribe to http://localhost:3000/products/3/who_bought.atom
in your favorite reader, order the product from the store page, and get the good news.
18a. If all works fine, commit the changes to your local git
repository, and push them to the github
cloud.