jenni's blog

鸡毛蒜皮

  DonewsBlog  |  Donews首页  |  Donews社区  |  Donews邮箱  |  我的首页  |  联系作者  |  聚合   |  登录
  210篇文章 :: 0篇收藏:: 110篇评论:: 2个Trackbacks

文章

收藏

相册

亲朋好友

最爱blogger

最爱论坛

存档


正在读取评论……


翻译:Jennifer Weng

Task F: Administrivia
我们的客户现在很幸福--在非常短的时间里我们就一起建立了一个基本的购物车系统,而她可以开始展示给她的客户们了。她只希望我们再做一个改变。现在任何人都能够进入管理功能。她希望我们增加一个基本的用户管理系统,强制用户必须登陆以后才可以进入站点的管理部分。
我们很高兴做这个部分,因为这给了我们一个新的机会来尝试callback hooks(这个该翻译成什么呢?回叫钩?)和过滤器。它也让我们可以将应用代码做一点整理。
和我们的客户聊了聊,看起来我们无须为我们的应用做一个特别复杂的安全系统。我们仅需要基于用户名和密码来确认一些人。一旦获得确认,这些人可以使用全部管理功能。

11.1 Iteration F1: Adding Users
让我们开始于为我们的管理者创建一个简单的数据库表来保存用户名和经过哈希处理的密码
create table users (
id int not null auto_increment,
name varchar(100) not null,
hashed_password char(40) null,
primary key (id)
);
我们也将建立Rails model。
depot> ruby script/generate model User
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/user.rb
create test/unit/user_test.rb
create test/fixtures/users.yml
现在我们需要用某种方法来创建表中的用户。实际上,很可能是我们将增加一系列用户相关的功能:登陆,列表显示,删除,增加等等。让我们激昂它们放到自己的controller中以保持代码整洁。在此刻,我们可以调用Rails的scaffolding生成器,该生成器我们在产品维护时使用过。但是这一次让我们手动的来完成。这样我们尝试一些新的技术。因此,我们将生成我们的controller(login),以及我们所需要的每个行为方法。
depot> ruby script/generate controller Login add_user login logout \
delete_user list_users
exists app/controllers/
exists app/helpers/
create app/views/login
exists test/functional/
create app/controllers/login_controller.rb
create test/functional/login_controller_test.rb
create app/helpers/login_helper.rb
create app/views/login/login.rhtml
create app/views/login/add_user.rhtml
create app/views/login/delete_user.rhtml
create app/views/login/list_users.rhtml
我们知道如何在数据库表中创建新行;我们创建一个行为,将表单放入view中,调用model来保存数据。但是为了让本章变得更有趣一些,让我们使用一点不一样的方式来在controller中创建用户。
在我们用于维护products表的自动生成的scaffold代码中,edit行为设置了一个表单以编辑产品数据。当表单被用户填好后,它回到controller中一个单独的行为save。两个分开的方法一起实现了这部分工作。
相反,我们的用户创建代码将使用一个行为,add_user().在这个方法中我们将检测是否我们被调用来显示最初的(空的)表单还是被调用来保存已填好的表单。我们通过查看进来请求的http方法来实现这一点。如果它没有带来数据,它将进来一个GET请求。相反,如果它带有表单数据,我们将看到一个POST。在Rails controller中,请求信息可以从属性request中获得。我们可以通过方法get?()和post?()来检查请求类型。这里是文件login_controller.rb中的add_user()行为代码(记住我们增加了admin布局给了这个新的controller--让我们使得屏幕布局在所有的管理功能中保持一致)。
class LoginController < ApplicationController
layout "admin"
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
# . . .
如果进来的请求是GET,add_user()方法知道没有任何表单数据,所以它创建了一个新的User对象,以便view使用。如果请求不是GET,方法假定有POST数据。它将用来自表单的数据来填充一个User对象,并尝试保存它。如果保存成功,它将重定向到index页;否则再次显示自己的view,允许用户纠正错误。
为了使该行为能够正确运行,我们需要为它创建一个视图。这是app/views/login中的add_user.rhtml。注意form_tag无须任何参数,因为它默认提交表单回到产生模板的controller和action。
<% @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 %>
其他不那么直接的是我们的User model。在数据库中,用户的密码被保存为一个40个字符的经过哈希处理的字符串,但是在表单中用户输入的是纯文本。User model需要有分开的处理,当处理表单数据时维护纯文本密码,当写到数据库时要处理经过哈希处理的密码。
由于User类是一个Active Record model,它知道users表中的列--它将自动有一个hashed_password属性。但是在数据库中没有纯文本的密码,因此我们使用ruby的attr_accessor来创建一个可读写的model属性。
class User < ActiveRecord::Base
attr_accessor :password
我们需要确保在model数据被写到数据库前
经过哈希处理的密码能够从纯文本属性中获得。我们可以使用内嵌在Active Record中的hook功能来实现它。
Active Record定义了大量的可在model对象的生命周期中的不同点调用的callback hooks。例如,在一个model被有效性确认前,在一行被保存前,在一个新的行被创建后等等都可以运行callback。在我们的例子中,我们可以使用创建前和创建后callbacks来处理密码。
在user的记录被保存前,我们使用before_create() hook来获得纯文本密码,并对之进行SHAI哈希功能,并将结果保存到hashed_password属性。这样在model被写入以前数据库中的hashed_password列将被设置为纯文本密码的哈希后的值。
当记录被保存后,我们使用after_create() hook来清除纯文本密码字段。因为user对象将最终被保存在session数据中,我们不希望这些密码保存在磁盘上让人们看见。
有很多方法定义hook方法。这里我们简单的用callbacks(before_create()和after_create())同样的名字来定义方法。以后p126,我们将看到如何用声明的方法来实现它。
这里是密码处理的代码。
require "digest/sha1"
class User < ActiveRecord::Base
attr_accessor :password
attr_accessible :name, :password
def before_create
self.hashed_password = User.hash_password(self.password)
end
def after_create
@password = nil
end
private
def self.hash_password(password)
Digest::SHA1.hexdigest(password)
end
end
增加一些有效性检查,user model的工作就完成了(目前)。
class User < ActiveRecord::Base
attr_accessor :password
attr_accessible :name, :password
validates_uniqueness_of :name
validates_presence_of :name, :password
在login controller中的Add_user()方法调用了redirect_to_index( )方法。我们以前在store controller中定义了它,因此在login controller中它不再可用。为了使得重定向方法可以在不同的controller中被使用,我们需要将它从store controller中移到app/controllers目录中的application.rb。该文件定义了ApplicationController类,那是我们应用所有controller类的父类。在这里定义的方法将可以被所有controller使用。
class ApplicationController < ActionController::Base
model :cart
model :line_item
private
def redirect_to_index(msg = nil)
flash[:notice] = msg if msg
redirect_to(:action => 'index')
end
end
是的,现在我们可以增加用户到我们的数据库了,让我们试试。浏览
http://localhost:3000/login/add_user,你将看到这出色的足以让人晕倒的页面设计。

当我们点击add user按钮,应用崩溃了,因为我们还没有定义一个index行为。但是我们可以查看数据库来检查是否用户数据被创建了。
depot> mysql depot_development
mysql> select * from users;
+----+------+------------------------------------------+
| id | name | hashed_password |
+----+------+------------------------------------------+
| 1 | dave | e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4 |
+----+------+------------------------------------------+
1 row in set (0.00 sec)

11.2 Iteration F2: Logging In
为我们store的管理者增加login支持意味着什么?
 ○ 我们需要提供一个表单以允许他们输入自己的姓名和密码。
 ○ 一旦他们登陆,我们需要为了他们会话的其他部分记录这个事实(直到他们log out)
 ○ 我们需要限制对应用管理部分的访问,仅允许登陆进来的人管理store.
在login controller中我们需要一个login()行为,它将在会话里记录一些东西以表明一个管理者登陆进来了。让我们让它保存user对象的id,其key为:user_id。login代码看起来如下所示:
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 combination"
end
end
end
这使用了我们在add_user()方法中使用的同样把戏,在同一方法中处理最初的请求以及响应。在最初的GET中,我们分配了一个新的user对象以便form有默认的数据使用。我们也清空了用户部分的会话数据;当你到达login行为时,直到你下次成功的登陆以前你都是log out状态。
如果login行为接受到POST数据,它将数据抽取到user对象中。调用对象的try_to_login()方法。当name和hashed password匹配时将根据数据库中的用户记录返回一个新的user对象。其在user.rb model文件中的实现,是很直观的。
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)
end
我们也需要一个login的view,login.rhtml。这和add_user view很类似,因此就不在这里显示了(记住Depot应用的完整列表从p486开始)。
最后是时间来增加index页了,这是管理者们登陆以后看到的第一个页面。让我们使它有点用--我们让它显示我们商店中的订单总数,以及等待发货的数目。view在目录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>
Index()行为完成了统计。
def index
@total_orders = Order.count
@pending_orders = Order.count_pending
end
我们需要在order model中增加一个类方法以返回待发货的订单数
def self.count_pending
count("shipped_at is null")
end
现在我们可以体验一下作为管理者登陆的喜悦了。

我们向客户展示了成果,但是她指出我们还没有控制对管理页面的访问呢(那可是这个练习的重点阿)。

11.3 Iteration F3: Limiting Access
我们要阻止人们不经过管理页的登陆就访问admin页面。而使用Rails 过滤(filter)功能能够很容易的实现它。
Rails filter允许你拦截对行为方法的调用,在它们被调用前或者它们返回后增加你自己的处理过程,或者两者都处理。在我们的例子中,我们使用before filter来拦截所有对admin controller中行为的调用。拦截程序检查session[:user_id],如果被设置了,应用知道一个管理者已登陆,调用将继续。如果没有设置,拦截程序将进行重定向,在本例中转向我们的登陆页面。
我们应该将该方法放到哪里呢?它可以直接被放到admin controller中,但是为了马上就会很明显的原因,让我们将它放到AppllicationController中,我们所有controller类的父类。App/controllers目录中的 application.rb文件。
def authorize
unless session[:user_id]
flash[:notice] = "Please log in"
redirect_to(:controller => "login", :action => "login")
end
end
仅需增加一行代码,授权方法就能在管理controller中的任何行为前被调用。
class AdminController < ApplicationController
before_filter :authorize
# ...
我们需要对Login controller做类似的改变。但是这里,我们要允许login行为可被调用,即使用户在未登陆前。因此我们将它免于检查。
class LoginController < ApplicationController
before_filter :authorize, :except => :login
# . .
如果你一直跟着我们,删除你的session文件(因为在其中我们已经登陆了)。浏览
http://localhost:3000/admin/ship. filter方法在我们通往发货页面的路上拦截了我们,并向我们展示了登陆屏幕。

我们向客户展示了它,获得了一个大大的微笑和一个请求。我们能否将用户管理的一切加到侧边的菜单,以增加显示和删除管理用户的空间?那当然!
将用户列表增加到login controller很容易;实际上它如此容易我们都不想在这里展示它。看看p490的controller源代码以及p498的view。注意我们如何将删除功能的链接放到每个用户的列表中。我们没有一个要求用户名并删除用户的删除页面,我们只是在用户列表的每个名字旁增加了一个删除链接。

Would the Last Admin to Leave…
然而删除功能确实带来一个有趣的问题。我们不想从系统中删除所有的管理用户(因为如果这样我们除了黑掉数据库外再没有别的方法能回来了)。为了防止这一点,我们在User model中使用了一个hook方法,用于在一个用户被删除前调用方法don't_destroy_dave()。该方法在试图删除一个名叫dave的用户时抛出一个异常(Dave看起来是一个全能用户的好名字,是吧?)。而我们有了一个新的机会来展示定义callback的第二种方法,使用类层次的声明(before_destroy),它引用具体完成工作的实例方法。
before_destroy :dont_destroy_dave
def dont_destroy_dave
raise "Can't destroy dave" if self.name == 'dave'
end
该异常被login controller中的delete()行为所捕获,它会报告一个错误给用户。
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

Updating the Sidebar
给侧边拦增加额外的管理功能是非常直观的。我们编辑admin.rhtml布局,并跟随我们在admin controller中增加功能的模式就可以了。可是,这里有个小问题。我们可以使用view可用的session信息来判定当前的session是否有一个登陆进来的用户。如果不是,我们压缩侧边拦的显示。
<html>
<head>
<title>ADMINISTER Pragprog Books Online Store</title>
<%= stylesheet_link_tag "scaffold", "depot", "admin", :media => "all" %>
</head>
<body>
<div id="banner">
<%= @page_title || "Administer Bookshelf" %>
</div>
<div id="columns">
<div id="side">
<% if session[:user_id] -%>
<%= link_to("Products", :controller => "admin",
<%= 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>

Logging Out
我们的管理布局在侧边拦有一个logou选项。这在login controller中的实现是很简单的。
def logout
session[:user_id] = nil
flash[:notice] = "Logged out"
redirect_to(:action => "login")
end
我们最后一次叫来我们的客户,她玩了一会儿store应用。她尝试了我们新的管理功能,并检查了买者的体验。她尝试输入坏数据,应用处理的非常漂亮。她微笑了,那么我们基本上完成了。
我们已经完成了增加功能。但是在我们离开前我们再看一看代码。我们注意到store controller中一个稍有点丑的重复代码。每个除了index的行为都需要在session数据中寻找用户的购物车。这行代码
@cart = find_cart
在controller中出现了5次。现在我们知道filter可以修补它。我们改变了find_cart()方法来将其结果直接保存到@cart实例变量中。
def find_cart
@cart = (session[:cart] ||= Cart.new)
end
我们将使用一个before filter来在除了index的所有行为中调用这个方法。
before_filter :find_cart, :except => :index
这使我们移去行为方法中5个对@cart的赋值。最终列表显示在P491开始的页中。

11.4 Finishing Up
编码结束了,但是在我们将应用部署到产品前我们仍然要做一些整理工作。
我们也许需要检查我们应用的文档。当我们编码时,我们已经为我们所有的类和方法写了一些简单雅致的注释(将代码抽取到本书时由于想节约空间我们没有显示它们)。Rails使得对应用的所有源文件运行Ruby的RDoc工具来创建好看的程序员文档十分容易。但是,在我们生成这样的文档前,我们可以创建一个很好的介绍页以便以后的开发者理解我们应用是干什么的。为了实现这一点,编辑文件doc/README_FOR_APP,并键入任何你觉得有用的东西。该文件将被RDoc处理,因此你有很大的格式灵活性。
你可以用rake命令来生成HTML格式的文档。
depot> rake appdoc
它生成的文档被置于目录doc/app。图11.1显示了生成的初始页面


11.5 More Icing on the Cake
尽管写我们自己的login代码很有趣,但我们在此过程中学习了许多Rails知识。在实际项目中我们也许会采用不同的方法。
Rails生成器可被扩展--人们可以为其它用途创建新的生成器。如果你看附录,你会看到至少2个现成的login controllers,都比我们刚刚写的那个有更多功能。试验它们要比创建你自己的用户管理系统要更谨慎一些。
如果你决定创建你自己的login controller,你也许会对一个由Erik Hatcher所建议的简单技巧感兴趣。我们所写的Authorize()方法在任何进来的请求之前被调用。它是否应该决定如果用户不登陆,它就直接定向到login行为?
Erik建议扩展它,让它在重定向到登陆页面前在session中保存request参数。
def authorize
unless session[:user_id]
flash[:notice] = "Please log in"
# save the URL the user requested so we can hop back to it
# after login
session[:jumpto] = request.parameters
redirect_to(:controller => "login", :action => "login")
end
end
这样,一旦登陆成功了,在重定向中使用保存的参数来让浏览器回到用户最初想访问的页面。
def login
if request.get?
session[:user_id] = nil
@user = User.new
else
@user = User.new(params[:user_id])
logged_in_user = @user.try_to_login
if logged_in_user
session[:user_id] = logged_in_user
jumpto = session[:jumpto] || { :action => "index" }
session[:jumpto] = nil
redirect_to(jumpto)
else
flash[:notice] = "Invalid user/password combination"
end
end
end

What We Just Did
在本次会话的最后时刻,我们完成了下列工作:

  • 我们在user model中使用了hook方法来将password从应用侧的纯文本映射到数据库的哈希处理后形式。我们也使用了hook在哈希处理后的密码被保存后移除用户对象的纯文本密码。
  • 我们将一些应用范围的controller helper方法移到app/controllers目录里application.rb文件中的ApplicationController类。
  • 我们使用一种新的行为方法和view的交互形式,在该形式中单一的行为利用request类型来决定它是否应该显示一个新的view还是处理存在的数据。
  • 我们利用before filters调用authorize()方法来控制对管理功能的访问。
  • 我们使得侧边拦的菜单动态化,仅当管理者登陆以后才显示。
  • 我们看到了如何生成应用的文档。


Trackback: http://tb.donews.net/TrackBack.aspx?PostId=767736


[点击此处收藏本文]  发表于2006年03月14日 4:01 PM




正在读取评论……

发表评论

大名:
网址:
验证码
评论