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: true3.
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: :newto 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
end7. 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'
endAlso 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 :success8. 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: true8a. 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.keys13. 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: ChequeThen, in line_items.yml, modify fixture two to be:
two:
product: ruby
order: one13a. 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
Ordermodel 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_path14a. 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
endTo make a product aware of all the orders it is contained in, add a line
has_many :orders, through: :line_itemsto 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
endand 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
end18. 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.