完整的Depot应用源代码 Source Code
参考 《应用 Rails 进行敏捷 Web 开发》第一版
林芷薰 翻译
透明 审校
Database Files
数据库文件
D:\work\depot\config\database.yml
development:
adapter: mysql
database: depot_development
username: root
password:
host: localhost
# Warning: The database defined as 'test' will be erased and
# re-generated from your development database when you run 'rake'.
# Do not set this db to the same as development or production.
test:
adapter: mysql
database: depot_test
username: root
password:
host: localhost
production:
adapter: mysql
database: depot_production
username: root
password:
host: localhost
D:\work\depot\db\create.sql
drop table if exists users;
drop table if exists line_items;
drop table if exists orders;
drop table if exists products;
create table products (
id int not null auto_increment,
title varchar(100) not null,
description text not null,
image_url varchar(200) not null,
price decimal(10,2) not null,
date_available datetime not null,
primary key(id)
);
create table orders (
id int not null auto_increment,
name varchar(100) not null,
email varchar(255) not null,
address text not null,
pay_type char(10) not null,
shipped_at datetime null,
primary key (id)
);
create table line_items (
id int not null auto_increment,
product_id int not null,
order_id int not null,
quantity int not null default 0,
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 (
id int not null auto_increment,
name varchar(100) not null,
hashed_password char(40) null,
primary key (id)
);
/* password = 'admin' */
insert into users values(null, 'admin','d033e22ae348aeb5660fc2140aec35850c4da997');
D:\work\depot\db\product_data.sql
lock tables products write;
insert into products( title, description, image_url, price, date_available ) values(
'Pragmatic Project Automation',
'A really great read!',
'http://localhost:3000/images/svn.JPG',
'29.95',
'2007-12-25 05:00:00' );
insert into products( title, description, image_url, price, date_available ) values(
'Pragmatic Version Control',
'A really contrlooed read!',
'http://localhost:3000/images/utc.jpg',
'29.95',
'2007-12-01 05:00:00');
insert into products( title, description, image_url, price, date_available ) values(
'Pragmatic Version Control2',
'A really contrlooed read!',
'http://localhost:3000/images/auto.jpg',
'29.95',
'2007-12-01 05:00:00');
unlock tables;
D:\work\depot\db\product_data2.sql
lock tables products write;
insert into products( title, description, image_url, price, date_available ) values(
'Pragmatic Project Automation',
'A really great read!',
'/images/svn.JPG',
'29.95',
'2007-12-25 05:00:00' );
insert into products( title, description, image_url, price, date_available ) values(
'Pragmatic Version Control',
'A really contrlooed read!',
'/images/utc.jpg',
'29.95',
'2007-12-01 05:00:00');
insert into products( title, description, image_url, price, date_available ) values(
'Pragmatic Version Control2',
'A really contrlooed read!',
'/images/auto.jpg',
'29.95',
'2007-12-01 05:00:00');
unlock tables;
控制器
D:\work\depot\app\controllers\application.rb
class ApplicationController < ActionController::Base
model :cart
model :line_item
# Pick a unique cookie name to distinguish our session data from others'
session :session_key => '_depot_session_id'
private
def redirect_to_index(msg = nil)
flash[:notice] = msg if msg
redirect_to(:action => 'index')
end
def authorize
unless session[:user_id]
flash[:notice] = "Please log in"
redirect_to(:controller => "login", :action => "login")
end
end
end
D:\work\depot\app\controllers\admin_controller.rb
class AdminController < ApplicationController
before_filter :authorize
# GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
verify :method => :post, :only => [ :destroy, :create, :update ],
:redirect_to => { :action => :list }
def index
list
render :action => 'list'
end
def list
@product_pages, @products = paginate :products, :per_page => 10
end
def show
@product = Product.find(params[:id])
end
def new
@product = Product.new
end
def create
@product = Product.new(params[:product])
if @product.save
flash[:notice] = 'Product was successfully created.'
redirect_to :action => 'list'
else
render :action => 'new'
end
end
def edit
@product = Product.find(params[:id])
end
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
else
render :action => 'edit'
end
end
def destroy
Product.find(params[:id]).destroy
redirect_to :action => 'list'
end
def ship
count = 0
if things_to_ship = params[:to_be_shipped]
count = do_shipping(things_to_ship)
if count > 0
count_text = pluralize(count, "order")
flash.now[:notice] = "#{count_text} marked as shipped"
end
end
@penging_orders = Order.pending_shipping
end
private
def do_shipping(things_to_ship)
count = 0
things_to_ship.each do |order_id, do_it|
if do_it == "yes"
order = Order.find(order_id)
order.mark_as_shipped
order.save
count += 1
end
end
count
end
def pluralize(count, noun)
case count
when 0: "No #{noun.pluralize}"
when 1: "One #{noun}"
else "#{count} #{noun.pluralize}"
end
end
end
D:\work\depot\app\controllers\login_controller.rb
class LoginController < ApplicationController
layout "admin"
before_filter :authorize, :except => :login
def index
@total_orders = Order.count
@pending_orders = Order.count_pending
end
def login
if request.get?
session[:user_id] = nil
@user = User.new
else
@user = User.new(params[:user])
logged_in_user = @user.try_to_login
if logged_in_user
session[:user_id] = logged_in_user.id
redirect_to(:action => "index")
else
flash[:notice] = "Invalid user/password conbination"
end
end
end
def add_user
if request.get?
@user = User.new
else
@user = User.new(params[:user])
if @user.save
redirect_to_index("User #{@user.name} created")
end
end
end
def delete_user
id = params[:id]
if id && user = User.find(id)
begin
user.destroy
flash[:notice] = "User #{user.name} deleted"
rescue
flash[:notice] = "Can't delete that user"
end
end
redirect_to(:action => :list_users)
end
def list_users
@all_users = User.find(:all)
end
def logout
session[:user_id] = nil
flash[:notice] = "Logged out"
redirect_to(:action => "login")
end
end
D:\work\depot\app\controllers\store_controller.rb
class StoreController < ApplicationController
before_filter :find_cart, :except => :index
def index
@products = Product.salable_items
end
def add_to_cart
product = Product.find(params[:id])
@cart.add_product(product)
redirect_to(:action => 'display_cart')
rescue
logger.error("Attempt to access invalid product #{params[:id]}")
redirect_to_index('Invalid product')
end
def display_cart
@items = @cart.items
if @items.empty?
redirect_to_index('Your cart is currently empty')
end
if params[:context] == :checkout
render(:layout => false)
end
end
def empty_cart
@cart.empty!
redirect_to_index('Your cart is now empty')
end
def checkout
@items = @cart.items
if @items.empty?
redirect_to_index("There's nothing in your cart!")
else
@order = Order.new
end
end
def save_order
@order = Order.new(params[:order])
@order.line_items << @cart.items
if @order.save
@cart.empty!
redirect_to_index('Thank you for your order.')
else
render(:action => 'checkout')
end
end
private
def find_cart
@cart = (session[:cart] ||= Cart.new)
end
end
模型
D:\work\depot\app\models\cart.rb
class Cart
attr_reader :items
attr_reader :total_price
def initialize
empty!
end
def empty!
@items = []
@total_price = 0.0
end
def add_product(product)
item = @items.find {|i| i.product_id == product.id}
if item
item.quantity += 1
else
item = LineItem.for_product(product)
@items << item
end
@total_price += product.price
end
end
D:\work\depot\app\models\line_item.rb
class LineItem < ActiveRecord::Base
belongs_to :product
belongs_to :order
def self.for_product(product)
item = self.new
item.quantity = 1
item.product = product
item.unit_price = product.price
item
end
end
D:\work\depot\app\models\order.rb
class Order < ActiveRecord::Base
has_many :line_items
PAYMENT_TYPES = [
[ "Check", "check"],
[ "Credit Card", "cc"],
[ "Purchas Order", "po"]
].freeze # freeze to make this array constant
validates_presence_of :name, :email, :address, :pay_type
def self.pending_shipping
find(:all, :conditions => "shipped_at is null")
end
def self.count_pending
count("shipped_at is null")
end
def mark_as_shipped
self.shipped_at = Time.now
end
end
D:\work\depot\app\models\product.rb
class Product < ActiveRecord::Base
validates_presence_of :title, :description, :image_url
validates_numericality_of :price
validates_uniqueness_of :title
validates_format_of :image_url,
:with => %r{^http:.+\.(gif|jpg|png)$}i,
:message => "must be a URL for a GIF, JPG, or PNG image"
def self.salable_items
find(:all,
:conditions => "date_available <= now()",
:order => "date_available desc")
end
protected
def validate
errors.add(:price, "should be positive") unless price.nil? || price >= 0.01
end
end
D:\work\depot\app\models\user.rb
require "digest/sha1"
class User < ActiveRecord::Base
attr_accessor :password
attr_accessible :name, :password
validates_uniqueness_of :name
validates_presence_of :name, :password
def self.login(name, password)
hashed_password = hash_password(password || "")
find(:first,
:conditions => ["name = ? and hashed_password = ?",
name, hashed_password])
end
def try_to_login
User.login(self.name, self.password)
User.find_by_name_and_hashed_password(name, "")
end
def before_create
self.hashed_password = User.hash_password(self.password)
end
before_destroy :dont_destroy_admin
def dont_destroy_admin
raise "Can't destroy admin" if self.name == 'admin'
end
def after_create
@password = nil
end
private
def self.hash_password(password)
Digest::SHA1.hexdigest(password)
end
end
视图
D:\work\depot\app\views\layouts\admin.rhtml
<html>
<head>
<title>ADMINISTER Pragprog Books Online Store</title>
<%= stylesheet_link_tag 'scaffold', 'depot', 'admin', :media => "all" %>
</head>
<body>
<div id="banner">
<%= @page_title || "Adminster Bookshelf" %>
</div>
<div id="columns">
<div id="side">
<% if session[:user_id] -%>
<%= link_to("Products", :controller => "admin", :action => "list") %><br />
<%= link_to("Shipping", :controller => "admin", :action => "ship") %><br />
<hr />
<%= link_to("Add user", :controller => "login", :action => "add_user") %><br />
<%= link_to("List users", :controller => "login", :action => "list_users") %><br />
<hr />
<%= link_to("Log out", :controller => "login", :action => "logout") %>
<% end -%>
</div>
<div id="main">
<% if flash[:notice] -%>
<div id="notice"><%= flash[:notice] %></div>
<% end -%>
<%= @content_for_layout %>
</div>
</div>
</body>
</html>
D:\work\depot\app\views\layouts\store.rhtml
<html>
<head>
<title>Pragprog Books Online Store</title>
<%= stylesheet_link_tag "scaffold", "depot", :media => "all" %>
</head>
<body>
<div id="banner">
<img src="/images/logo.png" />
<%= @page_title || "Pragmatic Bookshelf" %>
</div>
<div id="columns">
<div id="side">
<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>
<div id="main">
<% if @flash[:notice] -%>
<div id="notice"><%= @flash[:notice] %></div>
<% end -%>
<%= @content_for_layout %>
</div>
</div>
</body>
</html>
D:\work\depot\app\views\admin\_order_line.rhtml
<tr valign="top">
<td class="olnamebox">
<div class="olnamebox"><%= 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>
</div>
<% end %>
</td>
<td>
<%= check_box("to_be_shipped", order_line.id, {}, "yes", "no") %>
</td>
</tr>
D:\work\depot\app\views\admin\list.rhtml
<h1>Products Listing </h1>
<table cellpadding="5" cellspacing="0">
<%
odd_or_even = 0
for product in @products
odd_or_even = 1 - odd_or_even
%>
<tr valign="top" class="ListLine<%= odd_or_even %>">
<td>
<img width="60" height="70" src="<%= product.image_url %>"/>
</td>
<td width="60%">
<span class="ListTitle"><%= h(product.title) %></span><br />
<%= h(truncate(product.description, 80)) %>
</td>
<td allign="right">
<%= product.date_available.strftime("%y-%m-%d") %><br />
<strong>$<%= sprintf("%0.2f", product.price) %></strong>
</td>
<td class="ListActions">
<%= 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?', :method => :post %>
</td>
<tr>
<% end %>
</table>
<%= if @product_pages.current.previous
link_to ('Previous page', { :page => @product_pages.current.previous })
end
%>
<%= if @product_pages.current.next
link_to ('Next page', { :page => @product_pages.current.next })
end
%>
<br />
<%= link_to 'New product', :action => 'new' %>
D:\work\depot\app\views\admin\ship.rhtml
<div class="olheader">Orders To Be Shipped</div>
<%= form_tag(:action => "ship") %>
<table cellpadding="5" cellspacing="0">
<%= render(:partial => "order_line", :collection => @pending_orders) %>
</table>
<br />
<input type="submit" value=" SHIP CHECKED ITEMS " />
<%= end_form_tag %>
<br />
D:\work\depot\app\views\login\add_user.rhtml
<% @page_title = "Add a User" -%>
<%= error_messages_for 'user' %>
<%= form_tag %>
<table>
<tr>
<td>User name:</td>
<td><%= text_field("user", "name") %></td>
</tr>
<tr>
<td>Password:</td>
<td><%= password_field("user", "password") %></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value=" ADD USER " /></td>
</tr>
</table>
<%= end_form_tag %>
D:\work\depot\app\views\login\index.rhtml
<% @page_title = "Administer your Store" -%>
<h1>Depot Store Status</h1>
<p>
Total orders in system: <%= @total_orders %>
</p>
<p>
Orders pending shipping: <%= @pending_orders %>
</p>
D:\work\depot\app\views\login\list_users.rhtml
<% @page_title = "User List" -%>
<table>
<% for user in @all_users -%>
<tr>
<td><%= user.name %></td>
<td><%= link_to("(delete)", :action => :delete_user, :id => user.id) %></td>
</tr>
<% end %>
</table>
D:\work\depot\app\views\login\login.rhtml
<%= form_tag %>
<table>
<tr>
<td>User name:</td>
<td><%= text_field("user", "name") %></td>
</tr>
<tr>
<td>Password</td>
<td><%= password_field("user", "password") %></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value=" LOGIN " /></td>
</tr>
</table>
<%= end_form_tag %>
D:\work\depot\app\views\store\checkout.rhtml
<% @page_title = "Checkout" -%>
<%= error_messages_for("order") %>
<%= render_component(:acton => "display_cart",
:params => { :context => :checkout }) %>
<h3>Please enter your details below</h3>
<%= start_form_tag(:action => "save_order") %>
<table>
<tr>
<td>Name:</td>
<td><%= text_field("order", "name", "size" => 40) %></td>
</tr>
<tr>
<td>EMail:</td>
<td><%= text_field("order", "email", "size" => 40) %></td>
</tr>
<tr valign="top">
<td>Address:</td>
<td><%= text_area("order", "address", "cols" => 40, "rows" => 5) %></td>
</tr>
<tr>
<td>Pay using:</td>
<td><%=
options = [["Select a payment option", ""]] + Order::PAYMENT_TYPES
select("order", "pay_type", options) %></td>
</tr>
<tr>
<td></td>
<td><%= submit_tag(" CHECKOUT ") %></td>
</tr>
</table>
<%= end_form_tag %>
D:\work\depot\app\views\store\display_cart.rhtml
<% @page_title = "Your Pragmatic Cart" -%>
<div id="cartmenu">
<u1>
<li><%= link_to 'Continue shopping', :action => "index" %></li>
<% unless params[:context] == :checkout -%>
<li><%= link_to 'Empty car', :action => "empty_cart" %></li>
<li><%= link_to 'Checkout', :action => "checkout" %></li>
<% end -%>
</u1>
</div>
<table cellpadding="10" cellspacing="0">
<tr class="carttitle">
<td rowspan="2">Qty</td>
<td rowspan="2">Description</td>
<td colspan="2">Price</td>
</tr>
<tr class="carttitle">
<td>Each</td>
<td>Total</td>
</tr>
<%
for item in @items
product = item.product
-%>
<tr>
<td><%= item.quantity %></td>
<td><%= h(product.title) %></td>
<td align="right"><%= fmt_dollars(item.unit_price) %></td>
<td align="right"><%= fmt_dollars(item.unit_price * item.quantity) %></td>
</tr>
<% end %>
<tr>
<td colspan="3" align="right"><strong>Total:</strong></td>
<td id="totalcell"><%= fmt_dollars(@cart.total_price) %></td>
</tr>
</table>
D:\work\depot\app\views\store\index.rhtml
<% for product in @products %>
<div class="catalogentry">
<img src="<%= product.image_url %>"/>
<h3><%= h(product.title) %></h3>
<%= product.description %>
<span class="catalogprice"><%= fmt_dollars(product.price) %></span>
<%= link_to 'Add to Cart',
{:action => 'add_to_cart', :id => product },
:class => 'addtocart' %><br />
<div class="separator"> </div>
<% end %>
<%= link_to "Show my cart", :action => "display_cart" %>
辅助模块
D:\work\depot\app\helpers\application_helper.rb
# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
def fmt_dollars(amt)
sprintf("$%0.2f", amt)
end
end
CSS 文件
D:\work\depot\public\stylesheets\depot.css
/* Global styles */
/* START:notice */
#notice {
border: 2px solid red;
padding: 1em;
margin-bottom: 2em;
background-color: #f0f0f0;
font: bold smaller sans-serif;
}
/* END:notice */
/* Styles for admin/list */
#product-list .list-title {
color: #244;
font-weight: bold;
font-size: larger;
}
#product-list .list-image {
width: 60px;
height: 70px;
}
#product-list .list-actions {
font-size: x-small;
text-align: right;
padding-left: 1em;
}
#product-list .list-line-even {
background: #e0f8f8;
}
#product-list .list-line-odd {
background: #f8b0f8;
}
/* Styles for main page */
#banner {
background: #9c9;
padding-top: 10px;
padding-bottom: 10px;
border-bottom: 2px solid;
font: small-caps 40px/40px "Times New Roman", serif;
color: #282;
text-align: center;
}
#banner img {
float: left;
}
#columns {
background: #141;
}
#main {
margin-left: 15em;
padding-top: 4ex;
padding-left: 2em;
background: white;
}
#side {
float: left;
padding-top: 1em;
padding-left: 1em;
padding-bottom: 1em;
width: 14em;
background: #141;
}
#side a {
color: #bfb;
font-size: small;
}
h1 {
font: 150% sans-serif;
color: #226;
border-bottom: 3px dotted #77d;
}
/* And entry in the store catalog */
#store .entry {
border-bottom: 1px dotted #77d;
}
#store .title {
font-size: 120%;
font-family: sans-serif;
}
#store .entry img {
width: 75px;
float: left;
}
#store .entry h3 {
margin-bottom: 2px;
color: #227;
}
#store .entry p {
margin-top: 0px;
margin-bottom: 0.8em;
}
#store .entry .price-line {
}
#store .entry .add-to-cart {
position: relative;
}
#store .entry .price {
color: #44a;
font-weight: bold;
margin-right: 2em;
}
/* START:inline */
#store .entry form, #store .entry form div {
display: inline;
}
/* END:inline */
/* START:cart */
/* Styles for the cart in the main page and the sidebar */
.cart-title {
font: 120% bold;
}
.item-price, .total-line {
text-align: right;
}
.total-line .total-cell {
font-weight: bold;
border-top: 1px solid #595;
}
/* Styles for the cart in the sidebar */
#cart, #cart table {
font-size: smaller;
color: white;
}
#cart table {
border-top: 1px dotted #595;
border-bottom: 1px dotted #595;
margin-bottom: 10px;
}
/* END:cart */
/* Styles for order form */
.depot-form fieldset {
background: #efe;
}
.depot-form legend {
color: #dfd;
background: #141;
font-family: sans-serif;
padding: 0.2em 1em;
}
.depot-form label {
width: 5em;
float: left;
text-align: right;
margin-right: 0.5em;
display: block;
}
.depot-form .submit {
margin-left: 5.5em;
}
/* The error box */
.fieldWithErrors {
padding: 2px;
background-color: red;
display: table;
}
#errorExplanation {
width: 400px;
border: 2px solid red;
padding: 7px;
padding-bottom: 12px;
margin-bottom: 20px;
background-color: #f0f0f0;
}
#errorExplanation h2 {
text-align: left;
font-weight: bold;
padding: 5px 5px 5px 15px;
font-size: 12px;
margin: -7px;
background-color: #c00;
color: #fff;
}
#errorExplanation p {
color: #333;
margin-bottom: 0;
padding: 5px;
}
#errorExplanation ul li {
font-size: 12px;
list-style: square;
}
D:\work\depot\public\stylesheets\admin.css
#banner {
background: #ecc;
color: #822;
}
#columns {
background: #411;
}
#side {
background: #411;
}
#side a {
color: #fdd;
}
@side a:hover {
background: #411;
}
/* order shipping screen */
.olheader {
font: bold large sans-serif;
color: #411;
margin-bottom: 2ex;
}
.olnamebox, .olitembox {
padding-bottom: 3ex;
padding-right: 3em;
border-top: 1px dotted #411;
}
.olname {
font-weight: bold;
}
.oladdress {
font-size: smaller;
white-space: pre;
}
.olitemqty {
font-size: smaller;
font-weight: bold;
}
.olitemqty:after {
content: " x ";
}
.olitemtitle {
font-weight: bold;
}
.ListTitle {
color: #244;
font-weight: bold;
font-size: larger;
}
.ListActions {
font-size: x-small;
text-align: right;
padding-left: lem;
}
.ListLine0 {
background: #e0f8f8;
}
.ListLine1 {
background: #f8e0f8;
}
D:\work\depot\public\stylesheets\scaffold.css
body { background-color: #fff; color: #333; }
body, p, ol, ul, td {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 13px;
line-height: 18px;
}
pre {
background-color: #eee;
padding: 10px;
font-size: 11px;
}
a { color: #000; }
a:visited { color: #666; }
a:hover { color: #fff; background-color:#000; }
.fieldWithErrors {
padding: 2px;
background-color: red;
display: table;
}
#errorExplanation {
width: 400px;
border: 2px solid red;
padding: 7px;
padding-bottom: 12px;
margin-bottom: 20px;
background-color: #f0f0f0;
}
#errorExplanation h2 {
text-align: left;
font-weight: bold;
padding: 5px 5px 5px 15px;
font-size: 12px;
margin: -7px;
background-color: #c00;
color: #fff;
}
#errorExplanation p {
color: #333;
margin-bottom: 0;
padding: 5px;
}
#errorExplanation ul li {
font-size: 12px;
list-style: square;
}
div.uploadStatus {
margin: 5px;
}
div.progressBar {
margin: 5px;
}
div.progressBar div.border {
background-color: #fff;
border: 1px solid grey;
width: 100%;
}
div.progressBar div.background {
background-color: #333;
height: 18px;
width: 0%;
}
.ListTitle {
color: #244;
font-weight: bold;
font-size: larger;
}
.ListActions {
font-size: x-small;
text-align: right;
padding-left: lem;
}
.ListLine0 {
background: #e0f8f8;
}
.ListLine1 {
background: #f8b0f8;
}
以上代码 完全参考书本 考虑到 第一版的 代码下载官方网站已经没有提供了,所以特意贴出来,为今后无论是自己还是别人如果看的书是第一版的,免得输入累。