File 106 drop table if exists users; drop table if exists line_items; drop table if exists orders; drop table if exists products; create table products image_url varchar200 not null, da
Trang 1Source Code This appendix contains three things.
• Full listings for the files we created, and the generated files that we modified, for the final Depot application.
• The source for an e-mail exception notifier starts on page 511
• A cross-reference listing for all the code samples in the book starts
on page 512 All code is available for download from our website at http://pragmaticprogrammer.com/titles/railscode.html
C.1 The Full Depot Application
Trang 2File 106 drop table if exists users;
drop table if exists line_items;
drop table if exists orders;
drop table if exists products;
create table products (
image_url varchar(200) not null,
date_available datetime not null,
primary key (id)
);
create table orders (
name varchar(100) not null,
primary key (id)
);
create table line_items (
unit_price decimal(10,2) not null,
constraint fk_items_product foreign key (product_id) references products(id),
constraint fk_items_order foreign key (order_id) references orders(id),
primary key (id)
);
create table users (
name varchar(100) not null,
hashed_password char(40) null,
primary key (id)
# Also establishes Cart, LineItem, and User as models This
# is necessary because these classes appear in sessions and
# hence have to be preloaded
class ApplicationController < ActionController::Base
Trang 3# to the current controller's +index+ action
def redirect_to_index(msg = nil) #:doc:
flash[:notice] = msg if msgredirect_to(:action => 'index')
end
# The #authorize method is used as a <tt>before_hook</tt> in
# controllers that contain administration actions If the
# session does not contain a valid user, the method
# redirects to the LoginController.login.
unless session[:user_id]
flash[:notice] = "Please log in"
redirect_to(:controller => "login", :action => "login")
end end
end
depot_final/app/controllers/admin_controller.rb:
File 83 # The administration functions allow authorized users
# to add, delete, list, and edit products The class
# was initially generated from a scaffold but has since been
# modified, so do not regenerate.
#
# Only logged-in administrators can use the actions here See
# Application.authorize for details.
#
# See also: Product
class AdminController < ApplicationController
before_filter :authorize
# An alias for #list, listing all current products.
def index
listrender_action 'list'
# Initiate the creation of a new product.
# The work is completed in #create.
def new
@product = Product.new
end
# Get information on a new product and
# attempt to create a row in the database.
def create
@product = Product.new(@params[:product])
if @product.saveflash['notice'] = 'Product was successfully created.'
redirect_to :action => 'list'
else
render_action 'new'
end end
# Initiate the editing of an existing product.
Trang 4# The work is completed in #update.
def edit
@product = Product.find(@params[:id])
end
# Update an existing product based on values
# from the form.
def update
@product = Product.find(@params[:id])
if @product.update_attributes(@params[:product])
flash['notice'] = 'Product was successfully updated.'
redirect_to :action => 'show', :id => @product
# Ship a number of products This action normally dispatches
# back to itself Each time it first looks for orders that
# the user has marked to be shipped and ships them It then
# displays an updated list of orders still awaiting shipping.
#
# The view contains a checkbox for each pending order If the
# user selects the checkbox to ship the product with id 123, then
# this method will see <tt>things_to_ship[123]</tt> set to "yes".
count_text = pluralize(count, "order")
flash.now[:notice] = "#{count_text} marked as shipped"
when 0: "No #{noun.pluralize}"
when 1: "One #{noun}"
else "#{count} #{noun.pluralize}"
end
end
end
Trang 5File 85 # This controller performs double duty It contains the
# #login action, which is used to log in administrative users.
#
# It also contains the #add_user, #list_users, and #delete_user
# actions, used to maintain the users table in the database.
#
# The LoginController shares a layout with AdminController
#
# See also: User
class LoginController < ApplicationController
layout "admin"
# You must be logged in to use all functions except #login
before_filter :authorize, :except => :login
# The default action displays a status page.
def index
@total_orders = Order.count
@pending_orders = Order.count_pending
end
# Display the login form and wait for user to
# enter a name and password We then validate
# these, adding the user object to the session
if logged_in_usersession[:user_id] = logged_in_user.id
redirect_to(:action => "index")
else
flash[:notice] = "Invalid user/password combination"
end end end
# Add a new user to the database.
# Delete the user with the given ID from the database.
# The model raises an exception if we attempt to delete
# the last user.
Trang 6flash[:notice] = "Can't delete that user"
end end
# Log out by clearing the user entry in the session We then
# redirect to the #login action.
# [#display_cart] Show the contents of the cart
class StoreController < ApplicationController
before_filter :find_cart, :except => :index
# Display the catalog, a list of all salable products.
rescue
logger.error("Attempt to access invalid product #{params[:id]}")
redirect_to_index('Invalid product')
end
# Display the contents of the cart If the cart is
# empty, display a notice and return to the
end end
# Remove all items from the cart
def empty_cart
@cart.empty!
redirect_to_index('Your cart is now empty')
end
Trang 7# Prompt the user for their contact details and payment method,
# The checkout procedure is completed by the #save_order method.
# Called from checkout view, we convert a cart into an order
# and save it in the database.
private
# Save a cart object in the @cart variable If we already
# have one cached in the session, use it, otherwise create
# a new one and add it to the session
File 88 # A Cart consists of a list of LineItem objects and a current
# total price Adding a product to the cart will either add a
# new entry to the list or increase the quantity of an existing
# item in the list In both cases the total price will
# be updated.
#
# Class Cart is a model but does not represent information
# stored in the database It therefore does not inherit from
# Add a product to our list of items If an item already
# exists for that product, increase the quantity
# for that item rather than adding a new item.
def add_product(product)
item = @items.find {|i| i.product_id == product.id}
Trang 8if itemitem.quantity += 1
# Empty the cart by resetting the list of items
# and zeroing the current total price.
File 89 # Line items tie products to orders (and before that, to carts).
# Because the price of a product may change after an order is placed,
# the line item contains a copy of the product price at the time
# it was created.
class LineItem < ActiveRecord::Base
belongs_to :product
belongs_to :order
# Return a new LineItem given a Product.
def self.for_product(product)
item = self.newitem.quantity = 1item.product = productitem.unit_price = product.priceitem
end
end
depot_final/app/models/order.rb:
File 90 # An Order contains details of the purchaser and
# has a set of child LineItem rows.
class Order < ActiveRecord::Base
has_many :line_items
# A list of the types of payments we accept The key is
# the text displayed in the selection list, and the
# value is the string that goes into the database.
PAYMENT_TYPES = [
[ "Check", "check" ], [ "Credit Card", "cc" ],
[ "Purchase Order", "po" ]].freeze
validates_presence_of :name, :email, :address, :pay_type
# Return a count of all orders pending shipping.
def self.count_pending
count("shipped_at is null")
end
# Return all orders pending shipping.
def self.pending_shipping
find(:all, :conditions => "shipped_at is null")
end
# The shipped_at column is +NULL+ for
Trang 9# unshipped orders, the dtm of shipment otherwise.
File 91 # A Product is something we can sell (but only if
class Product < ActiveRecord::Base
:message => "must be a URL for a GIF, JPG, or PNG image"
# Return a list of products we can sell (which means they have to be
# available) Show the most recently available first.
def self.salable_items
find(:all,
:conditions => "date_available <= now()",
:order => "date_available desc")
end
protected
# Validate that the product price is a positive Float.
def validate #:doc:
errors.add(:price, "should be positive") unless price.nil? || price > 0.0
end
end
depot_final/app/models/user.rb:
File 92 require "digest/sha1"
# A User is used to validate administrative staff The class is
# complicated by the fact that on the application side it
# deals with plain-text passwords, but in the database it uses
# SHA1-hashed passwords.
class User < ActiveRecord::Base
# The plain-text password, which is not stored
# in the database
attr_accessor :password
# We never allow the hashed password to be
# set from a form
attr_accessible :name, :password
validates_uniqueness_of :name
validates_presence_of :name, :password
# Return the User with the given name and
Trang 10# Log in if the name and password (after hashing)
# match the database, or if the name matches
# an entry in the database with no password
def try_to_login
User.login(self.name, self.password) ||
User.find_by_name_and_hashed_password(name, "")
end
# When a new User is created, it initially has a
# plain-text password We convert this to an SHA1 hash
# before saving the user in the database.
# saved this row This stops it being made available
# in the session
def after_create
@password = nil end
<title>ADMINISTER Pragprog Books Online Store</title>
<%= stylesheet_link_tag "scaffold", "depot", "admin", :media => "all" %>
Trang 11<title>Pragprog Books Online Store</title>
<%= stylesheet_link_tag "scaffold", "depot", :media => "all" %>
<a href="http://www ">Home</a><br />
<a href="http://www /faq">Questions</a><br />
<a href="http://www /news">News</a><br />
<a href="http://www /contact">Contact</a><br />
<div class="olname"><%= h(order_line.name) %></div>
<div class="oladdress"><%= h(order_line.address) %></div>
</td>
<td class="olitembox">
<% order_line.line_items.each do |li| %>
<div class="olitem">
<span class="olitemqty"><%= li.quantity %></span>
<span class="olitemtitle"><%= li.product.title %></span>
Trang 12File 94 <h1>Product Listing</h1>
<table cellpadding="5" cellspacing="0">
<%= link_to 'Show', :action => 'show', :id => product %><br/>
<%= link_to 'Edit', :action => 'edit', :id => product %><br/>
<%= link_to 'Destroy', { :action => 'destroy', :id => product },
:confirm => "Are you sure?" %>
<table cellpadding="5" cellspacing="0">
<%= render(:partial => "order_line", :collection => @pending_orders) %>
Trang 13File 99 <% @page_title = "Administer your Store" -%>
<h1>Depot Store Status</h1>
Trang 14File 102 <% @page_title = "Checkout" -%>
<%= error_messages_for("order") %>
<%= render_component(:action => "display_cart",
:params => { :context => :checkout }) %>
<h3>Please enter your details below</h3>
<li><%= link_to 'Continue shopping', :action => "index" %></li>
<% unless params[:context] == :checkout -%>
<li><%= link_to 'Empty cart', :action => "empty_cart" %></li>
<li><%= link_to 'Checkout', :action => "checkout" %></li>
Trang 15<span class="catalogprice"><%= fmt_dollars(product.price) %></span>
<%= link_to 'Add to Cart',
{:action => 'add_to_cart', :id => product },:class => 'addtocart' %><br/>
File 122 ENV["RAILS_ENV"] = "test"
require File.dirname( FILE ) + "/ /config/environment"
Trang 16def login(name='fred', password='abracadabra')
post :login, :user => {:name => name, :password => password}
assert_redirected_to :action => "index"
description: How to use version control
description: How to automate your project
description: How to beat the clock
image_url: http:// /future.jpg
date_available: <%= 1.day.from_now.strftime("%Y-%m-%d") %>
Trang 17File 123 require File.dirname( FILE ) + '/ /test_helper'
class CartTest < Test::Unit::TestCase
@cart.total_priceassert_equal 2, @cart.items.size
end
def test_add_duplicate_product
@cart.add_product @version_control_book
@cart.add_product @version_control_bookassert_equal 2*@version_control_book.price, @cart.total_priceassert_equal 1, @cart.items.size
end
end
Trang 18depot_testing/test/unit/product_test.rb: require
→ page480
require File.dirname( FILE ) + '/ /test_helper'
class ProductTest < Test::Unit::TestCase
assert_equal "Pragmatic Version Control", @product.title
assert_equal "How to use version control", @product.description
assert_equal "http:// /sk_svn_small.jpg", @product.image_url
assert_equal vc_book["id"], @product.id
assert_equal vc_book["title"], @product.title
assert_equal vc_book["description"], @product.description
assert_equal vc_book["image_url"], @product.image_url
assert_equal vc_book["price"], @product.price
assert_equal vc_book["date_available"], @product.date_available_before_type_cast
end
def test_read_with_fixture_variable
assert_kind_of Product, @product
assert_equal @version_control_book.id, @product.id
assert_equal @version_control_book.title, @product.title
assert_equal @version_control_book.description, @product.description
assert_equal @version_control_book.image_url, @product.image_url
assert_equal @version_control_book.price, @product.price
assert_equal @version_control_book.date_available, @product.date_available
end
Trang 19def test_salable_items
items = Product.salable_itemsassert_equal 2, items.lengthassert items[0].date_available <= Time.nowassert items[1].date_available <= Time.nowassert !items.include?(@future_proof_book)
end
def test_salable_items_using_custom_assert
items = Product.salable_itemsassert_equal 2, items.lengthassert_salable items[0]
File 125 require File.dirname( FILE ) + '/ /test_helper'
class ProductTest < Test::Unit::TestCase
self.use_transactional_fixtures = true
# Re-raise errors caught by the controller.
class LoginController; def rescue_action(e) raise e end; end
class LoginControllerTest < Test::Unit::TestCase
def test_index
get :indexassert_response :success
end
def test_index_without_user
Trang 20get :index
assert_redirected_to :action => "login"
assert_equal "Please log in", flash[:notice]
post :login, :user => {:name => 'fred', :password => 'abracadabra'}
assert_redirected_to :action => "index"
assert_not_nil(session[:user_id])user = User.find(session[:user_id])assert_equal 'fred', user.name
assert_equal "Pragmatic Version Control", products[0].title assert_tag :tag => "div",
:attributes => { :class => "results" },
:children => { :count => 1,
:only => { :tag => "div",
:attributes => { :class => "catalogentry" }}}
end
end
Trang 21File 119 require File.dirname( FILE ) + '/ /test_helper'
require 'store_controller'
# Reraise errors caught by the controller.
class StoreController; def rescue_action(e) raise e end; end
class StoreControllerTest < Test::Unit::TestCase
fixtures :products, :orders
assert_template "store/display_cart"
end
def test_add_to_cart_invalid_product
get :add_to_cart, :id => '-1'
assert_redirected_to :action => 'index'
assert_equal "Invalid product", flash[:notice]
end
def test_checkout
test_add_to_cartget :checkoutassert_response :successassert_not_nil assigns(:order)
assert_template "store/checkout"
end
def test_save_invalid_order
test_add_to_cartpost :save_order, :order => {:name => 'fred', :email => nil}assert_response :success
post :save_order, :order => @valid_order_for_fred.attributesassert_redirected_to :action => 'index'
assert_equal "Thank you for your order.", flash[:notice]
Trang 22assert_template "store/index"
assert_equal 0, session[:cart].items.sizeassert_equal 2, Order.find_all.size
end
def test_assert_tags_many_options
test_add_to_cartget :save_order, :order => {:name => 'fred', :email => nil}
assert_tag :tag => "html"
assert_tag :content => "Pragprog Books Online Store"
assert_tag :tag => "head", :parent => { :tag => "html" } assert_tag :tag => "html", :child => { :tag => "head" } assert_tag :tag => "div", :ancestor => { :tag => "html" } assert_tag :tag => "html", :descendant => { :tag => "div" } assert_tag :tag => "ul", :children => {
order = Order.find(id)
Trang 23get :save_order, :order => order.attributesassert_redirected_to :action => 'index'
assert_equal("Thank you for your order.", flash[:notice])
end end
assert elapsedSeconds < 3.0, "Actually took #{elapsedSeconds} seconds"
Trang 25/**** styles for the catalog ***/
/* === Use the Holly Hack to fix layout bugs in IE on Windows === */
/* Hide from IE-mac \*/
Trang 26C.2 Sample System Notifier
The following is a modified version of the code used by the Basecamp
application to e-mail its maintainers when an exception occurs We show
how to hook this into the application on page 451
notifier/app/models/system_notifier.rb:
File 152 require 'pathname'
class SystemNotifier < ActionMailer::Base
SYSTEM_EMAIL_ADDRESS = %{"Error Notifier" <error.notifier@myapp.com>}
EXCEPTION_RECIPIENTS = %w{maintainer@myapp.com support@myapp.com}
def exception_notification(controller, request,
exception, sent_on=Time.now)
@subject = sprintf("[ERROR] %s\#%s (%s) %s",
controller.controller_name,controller.action_name,exception.class,
Trang 27C.3 Cross-Reference of Code Samples
The following list can be used to find the file containing source code in
num-ber, you can look that number up in the list that follows to determine
the file containing that code The files are available for download from