??xml version="1.0" encoding="utf-8" standalone="yes"?>色婷婷久久久久swag精品,国产一区福利,国产中文字幕在线播放http://www.aygfsteel.com/lzj520/category/16808.htmlzh-cnSat, 24 Nov 2007 18:48:59 GMTSat, 24 Nov 2007 18:48:59 GMT60071124 rails里用restful_authentication的login验证例子http://www.aygfsteel.com/lzj520/archive/2007/11/24/162864.htmllzj520lzj520Sat, 24 Nov 2007 08:20:00 GMThttp://www.aygfsteel.com/lzj520/archive/2007/11/24/162864.htmlhttp://www.aygfsteel.com/lzj520/comments/162864.htmlhttp://www.aygfsteel.com/lzj520/archive/2007/11/24/162864.html#Feedback0http://www.aygfsteel.com/lzj520/comments/commentRss/162864.htmlhttp://www.aygfsteel.com/lzj520/services/trackbacks/162864.htmlrails login

ruby script/plugin install http://svn.techno-weenie.net/projects/plugins/restful_authentication

ruby script/generate authenticated login_users login --include-activation
其中login_users是model
login是controller
–include-activation参数军_是否生成向新注册用户发送激zȝ的代码?/p>

在routes.rb加入
map.resources :login_users
map.resource :login
如果是rails 1.2.3Q需要加入的control是:
map.resource :login, controller=>'login'

在config/environment.rb加入
config.active_record.observers = :login_user_observer

你还可以在routes.rb中加入(Ҏ自己的需要)
map.signup '/signup', :controller => 'login_users', :action =>'new'
map.login '/login', :controller => 'login', :action =>'new'
map.logout '/logout', :controller => 'login', :action =>'destroy'

配置database.ymlQ徏立数据库login_development、login_test

rake db:migrate

rake

ruby script/server

讉KQhttp://127.0.0.1:3000/login_users/new/



lzj520 2007-11-24 16:20 发表评论
]]>
071719 rspec在netbeans IED 6 beta上运行时的一些问?/title><link>http://www.aygfsteel.com/lzj520/archive/2007/11/19/161635.html</link><dc:creator>lzj520</dc:creator><author>lzj520</author><pubDate>Mon, 19 Nov 2007 07:45:00 GMT</pubDate><guid>http://www.aygfsteel.com/lzj520/archive/2007/11/19/161635.html</guid><wfw:comment>http://www.aygfsteel.com/lzj520/comments/161635.html</wfw:comment><comments>http://www.aygfsteel.com/lzj520/archive/2007/11/19/161635.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.aygfsteel.com/lzj520/comments/commentRss/161635.html</wfw:commentRss><trackback:ping>http://www.aygfsteel.com/lzj520/services/trackbacks/161635.html</trackback:ping><description><![CDATA[不知是我弄错了什么地方,q是本来是q样的,在netbeans IED 6 betaq行rspecӞ假如先是先运行scaffold_resourceQ然后按照rspec官方的文档的Ҏ来安装rspec、rspec_on_railsQƈq行rspec、rspec_modelQ然而此Ӟ试默认生成的modelQ就会得到类DL错误提示Q?br /> 1.0.1/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:34:in `require': no such file to load -- E:/NetBeansProjects/RailsApplication2/spec/models/../../vendor/generators/rspec/lib/rspec_on_rails (MissingSourceFile)<br /> 此时打开spec_helper.rbQ发现rspec_on_rails的这一路径默认是不对的Q需要手工把它设|ؓ正确的\径。或者我试过把这句\径的引用直接删除掉,好像也可以?br /> <br /> 处理完这一步之后,会遇到的跟着的这样一个错误提C:<br /> spec/models/../spec_helper.rb:9: undefined local variable or method `use_transactional_fixtures=' for Spec::Runner::Context:Class (NameError)<br /> q需要你去test_helper.rb里加入一句require File.expand_path(File.dirname(__FILE__) + "/test2spec_help")Q把use_transactional_fixtures加入q来。然后在spec_helper.rb里加入require File.expand_path(File.dirname(__FILE__) + "/../test/test_helper")<br /> <br /> 然后才能正式看到1 example, 1 failureq样比较正常的提C。不知是否我操作错误Q还是什么问题,M是到了这样一q串的问? 而这个解军_法也不知是否正确 <img src ="http://www.aygfsteel.com/lzj520/aggbug/161635.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.aygfsteel.com/lzj520/" target="_blank">lzj520</a> 2007-11-19 15:45 <a href="http://www.aygfsteel.com/lzj520/archive/2007/11/19/161635.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>071014 windows下JRuby 1.0.2 与rails1.2.5出错的处?/title><link>http://www.aygfsteel.com/lzj520/archive/2007/11/14/160598.html</link><dc:creator>lzj520</dc:creator><author>lzj520</author><pubDate>Wed, 14 Nov 2007 09:19:00 GMT</pubDate><guid>http://www.aygfsteel.com/lzj520/archive/2007/11/14/160598.html</guid><wfw:comment>http://www.aygfsteel.com/lzj520/comments/160598.html</wfw:comment><comments>http://www.aygfsteel.com/lzj520/archive/2007/11/14/160598.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.aygfsteel.com/lzj520/comments/commentRss/160598.html</wfw:commentRss><trackback:ping>http://www.aygfsteel.com/lzj520/services/trackbacks/160598.html</trackback:ping><description><![CDATA[在netbeans 6 ruby ide beta里面Q如果用jruby 1.0.2 q且rails升?.2.5Q就会出错如Q?br /> C:/jruby/jruby-1.0.1/lib/ruby/1.8/pathname.rb:420:in `realpath_rec': No such file or directory - C:/railstest/C: (Errno::ENOENT)<br /> ....<br /> <br /> 避免的办法可以重新装netbeans 6 ruby ideq用rails1.2.3<br /> <br /> 或者是在此处下载一个patchQ?br /> http://jira.codehaus.org/browse/JRUBY-1401?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel<br /> <br /> patch的用方法就是按照patch里面的说明来对pathname.rb原文件修改:<br /> <p>Index: pathname.rb</p> <p>===================================================================</p> <p>--- pathname.rb (revision 4471)</p> <p>+++ pathname.rb (working copy)</p> <p>@@ -285,6 +285,8 @@</p> <p>   def prepend_prefix(prefix, relpath)<br />      if relpath.empty?<br />        File.dirname(prefix)<br /> +    elsif prefix =~ /^[a-zA-Z]:$/<br /> +      File.join(prefix,relpath)<br />      elsif /#{SEPARATOR_PAT}/ =~ prefix<br />        prefix = File.dirname(prefix)<br />        prefix = File.join(prefix, "") if File.basename(prefix + 'a') != 'a'<br /> @@ -408,6 +410,8 @@</p> <p>         next<br />        elsif n == '..'<br />          resolved.pop<br /> +      elsif n =~ /^[a-zA-Z]:$/<br /> +        resolved << n<br />        else<br />          path = prepend_prefix(prefix, File.join(*(resolved + [n])))<br />          if h.include? path<br /> @@ -446,7 +450,9 @@</p> <p>   def realpath<br />      path = @path<br />      prefix, names = split_names(path)<br /> -    if prefix == ''<br /> +    if prefix == '' && names[0] =~ /^[a-zA-Z]:$/<br /> +      prefix = names.shift<br /> +    elsif prefix == ''<br />        prefix, names2 = split_names(Dir.pwd)<br />        names = names2 + names<br />      end</p> 很直观,+pC加多的内容Q?是表示减少的内? <img src ="http://www.aygfsteel.com/lzj520/aggbug/160598.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.aygfsteel.com/lzj520/" target="_blank">lzj520</a> 2007-11-14 17:19 <a href="http://www.aygfsteel.com/lzj520/archive/2007/11/14/160598.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>使用 RSpec q行行ؓ驱动试Q{Q?/title><link>http://www.aygfsteel.com/lzj520/archive/2007/10/23/155374.html</link><dc:creator>lzj520</dc:creator><author>lzj520</author><pubDate>Tue, 23 Oct 2007 11:01:00 GMT</pubDate><guid>http://www.aygfsteel.com/lzj520/archive/2007/10/23/155374.html</guid><wfw:comment>http://www.aygfsteel.com/lzj520/comments/155374.html</wfw:comment><comments>http://www.aygfsteel.com/lzj520/archive/2007/10/23/155374.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.aygfsteel.com/lzj520/comments/commentRss/155374.html</wfw:commentRss><trackback:ping>http://www.aygfsteel.com/lzj520/services/trackbacks/155374.html</trackback:ping><description><![CDATA[<blockquote>试热潮现在传播C Ruby ~程C֌Qƈ且愈演愈热。在q去一q里Q测试领域中最为瞩目的创新应属 RSpec 的引入和快速发展,q是一U行为驱动测试工兗通过本文了解 RSpec 如何改变Z思考测试的方式?</blockquote><!--start RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--end RESERVED FOR FUTURE USE INCLUDE FILES--> <p>在过dq中QY件开发h员对试的热情日渐低q同一时期出现的动态语aq没有提供编译程序来捕捉最基本的错误,q得测试变得更加重要。随着试C֌的成长,开发h员开始注意到Q除了捕?bug {最基本的优点外Q测试还h以下优势Q?/p> <ul> <li>试能够改进您的设计。进行测试的每个目标对象必须具备臛_两个客户机:生代码和测试用例。这些客h强制您对代码q行解耦。测试还鼓励开发h员用更、更单的Ҏ? <li>试减少了不必要的代码。在~写试用例Ӟ您养成了很好的测试习惯,卛_~写q行试用例所需的最代码。您抵制住了对功能进行编码的诱惑Q因为您目前q不需要它? <li>推动了测试优先开发。您~写的每个测试用例会定一个小问题。用代码解册个问题非常有用ƈ且可以推动开发。当我进行测试驱动开发时Q时间过得飞快? <li>试提供了更多的自主权。在使用试用例捕获可能的错误时Q您会发现自己非常愿意对代码q行改进?</li> </ul> <p><a name="N10062"><span id="wmqeeuq" class="atitle">试驱动的开发和 RSpec</span></a></p> <p>有关试的优Ҏ需赘述Q我向您介l一个简单的使用 RSpec 的测试驱动开发示例。RSpec 工具是一?Ruby 软g包,可以用它构徏有关您的软g的规范。该规范实际上是一个描q系l行为的试。?RSpec 的开发流E如下:</p> <ul> <li>~写一个测试。该试描述pȝ中某个较元素的行ؓ? <li>q行试。由于尚没有为系l中的相应部分构Z码,试p|。这一重要步骤测试您的测试用例,验测试用例是否在应当p|的时候失败? <li>~写_的代码,使测试通过? <li>q行试Q检验测试是否成功?</li> </ul> <p>实质上,RSpec 开发h员所做的工作是失败的试用例调试为成功的试用例。这是一个主动的q程。本文中Q我介l?RSpec 的基本用法?</p> <p>首先Q假设您已安装了 Ruby ?gems。您q需要安?RSpec。输入下面的内容Q?</p> <p><code>gem install rspec</code> </p> <br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td><img height="1" alt="" src="http://www.ibm.com/i/v14/rules/blue_rule.gif" width="100%" /><br /> <img height="6" alt="" src="http://www.ibm.com/i/c.gif" width="8" border="0" /></td> </tr> </tbody> </table> <br /> <br /> <p><a name="N10086"><span id="wmqeeuq" class="atitle">使用CZ</span></a></p> <p>接下来,我将逐步构徏一个状态机。我遵?TDD 规则。首先编写自q试用例Qƈ且直到测试用例需要时才编写代码。Rake 的创?Jim Weirich 认ؓq有助于角色扮演。在~写实际的生产代码时Q您希望充当一?jerk 开发h员的角色Q只完成最量的工作来使测试通过。在~写试Ӟ您则扮演试人员的角Ԍ试图为开发h员提供一些有益的帮助?/p> <p>以下的示例展CZ如何构徏一个状态机。如果您以前从未接触q状态机Q请查阅 <a >参考资?/a>。状态机h多种状态。每U状态支持可以{换状态机状态的事g。测试驱动开发入门的关键是从零入手Q尽量少C用假设条件。针Ҏ试进行程序设计?/p> <p>使用清单 1 的内容创建名?machine_spec.rb 的文件。该文g是您的规范。您q不了解 machine.rb 文g的作用,目前先创Z个空文g?/p> <br /> <a name="listing1"><strong>清单 1. 最初的 machine_spec.rb 文g </strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode"> require 'machine' </pre> </td> </tr> </tbody> </table> <br /> <p>接下来,需要运行测试。始l通过输入 <code>spec machine_spec.rb</code> q行试。清?2 展示了预料之中的试p|Q?/p> <br /> <a name="listing2"><strong>清单 2. q行I的规范</strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">~/rspec batate$ spec machine_spec.rb /opt/local/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require': no such file to load -- machine (LoadError) from /opt/local/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require' from ./state_machine_spec.rb:1 from ... </pre> </td> </tr> </tbody> </table> <br /> <p>在测试驱动开发中Q您需要进行增量开发,因此在进行下一ơ开发前Q需要先解决此次试出现的问题。现在,我将扮演 jerk 开发h员的角色Q即只完成满_用程序运行所需的最工作量。我创Z个名?machine.rb 的空文gQɋ试通过。我现在可以以逸待劻I试通过而我几乎没做M事情?/p> <p>l箋角色扮演。我现在扮演一个烦w的试人员Q促?jerk 开发h员做些实际的工作。我编码以下规范,需要?<code>Machine</code> c,如清?3 所C:</p> <br /> <a name="listing3"><strong>清单 3. 初始规范</strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">require 'machine' describe Machine do before :each do @machine = Machine end end </pre> </td> </tr> </tbody> </table> <br /> <p>该规范描qC目前不存在?<code>Machine</code> cR?code>describe</code> Ҏ提供?RSpec 描述Q您传入测试类的名U和包含实际规范的代码块。通常Q测试用例需要执行一定数量的讄工作。在 RSpec 中,由 <code>before</code> Ҏ完成q些讄工作。您?<code>before</code> Ҏ传递一个可选的标志和一个代码块。代码块中包含设|工作。标志确?RSpec 执行代码块的频率。默认的标志?<code>:each</code>Q表C?RSpec 在每次试之前调用 set up 代码块。您也可以指?<code>:all</code>Q表C?RSpec 在执行所有测试之前只调用一?<code>before</code> 代码块。您应该始终使用 <code>:each</code>Q各个试彼此独立?</p> <p>输入 <code>spec</code> q行试Q如清单 4 所C: </p> <br /> <a name="listing4"><strong>清单 4. 存在性测试失?/strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">~/rspec batate$ spec machine_spec.rb ./machine_spec.rb:3: uninitialized constant Machine (NameError) </pre> </td> </tr> </tbody> </table> <br /> <p>现在Q烦w的试人员要促?jerk 开发h员做点什么了 ?jerk 开发h员现在需要创建某个类。对我来_是修复试出现的错误。在 <code>machine.rb</code> 中,我输入最量的代码,如清?5 所C:</p> <br /> <a name="listing5"><strong>清单 5. 创徏初始 Machine c?/strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">class Machine end</pre> </td> </tr> </tbody> </table> <br /> <p>保存文gQ然后运行测试。毫无疑问,清单 6 昄的测试报告没有出现错误:</p> <br /> <a name="listing6"><strong>清单 6. 试 Machine 是否存在</strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">~/rspec batate$ spec machine_spec.rb Finished in 5.0e-06 seconds 0 examples, 0 failures </pre> </td> </tr> </tbody> </table> <br /> <br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td><img height="1" alt="" src="http://www.ibm.com/i/v14/rules/blue_rule.gif" width="100%" /><br /> <img height="6" alt="" src="http://www.ibm.com/i/c.gif" width="8" border="0" /></td> </tr> </tbody> </table> <br /> <br /> <p><a name="N10116"><span id="wmqeeuq" class="atitle">~写行ؓ</span></a></p> <p>现在Q我可以开始实现更多的行ؓ。我知道Q所有状态机必须在某些初始状态下启动。目前我q不是很清楚如何设计q个行ؓQ因此我先编写一个非常简单的试Q首先假?<code>state</code> Ҏ会返?<code>:initial</code> 标志。我?<code>machine_spec.rb</code> q行修改q运行测试,如清?7 所C:</p> <br /> <a name="listing7"><strong>清单 7. 实现初始状态ƈq行试</strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">require 'machine' describe Machine do before :each do @machine = Machine.new end it "should initially have a state of :initial" do @machine.state.should == :initial end end ~/rspec batate$ spec machine_spec.rb F 1) NoMethodError in 'Machine should initially have a state of :initial' undefined method `state' for #<Machine:0x10c7f8c> ./machine_spec.rb:9: Finished in 0.005577 seconds 1 example, 1 failure </pre> </td> </tr> </tbody> </table> <br /> <p>注意q条规则Q?code> it "should initially have a state of :initial" do @machine.state.should == :initial end</code>。首先注意到q条规则读v来像是一个英文句子。删除标点,得?<code>it should initially have a state of initial</code>。然后会注意到这条规则ƈ不像是典型的面向对象代码。它实不是。您现在有一个方法,UCؓ <code>it</code>。该Ҏh一个用引hh的字W串参数和一个代码块。字W串应该描述试需求。最后,<code>do</code> ?<code>end</code> 之间的代码块包含试用例的代码?</p> <p>可以看到Q测试进度划分得很细。这些微的步骤产生的收益却很大。它们我能够改q测试密度,提供旉供我思考期望的行ؓ以及实现行ؓ所需?API。这些步骤还能我在开发期间跟t代码覆盖情况,从而构建更加丰富的规范?/p> <p>q种风格的测试具有双重作用:试实现q在试的同时构建需求设计文档。稍后,我将通过试用例构徏一个需求列表?/p> <p>我用最单的方式修复了测试,q回 <code>:initial</code>Q如清单 8 所C:</p> <br /> <a name="listing8"><strong>清单 8. 指定初始状?/strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">class Machine def state :initial end end </pre> </td> </tr> </tbody> </table> <br /> <p>当查看实现时Q您可能会放声大W或感觉受到了愚弄。对于测试驱动开发,您必ȝ微改变一下思考方式。您的目标ƈ不是~写最l的生代码Q至现在不是。您的目标是使测试通过。当掌握以这U方式工作时Q您可能会发现新的实玎ͼq且~写的代码要q远于采用 TDD 时编写的代码?/p> <p>下一步是q行代码Q查看它是否通过试Q?/p> <br /> <a name="listing9"><strong>清单 9. q行初始状态测?/strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">~/rspec batate$ spec machine_spec.rb . Finished in 0.005364 seconds 1 example, 0 failures</pre> </td> </tr> </tbody> </table> <br /> <p>׃旉思考一下这个通过试的P代。如果查看代码的话,您可能会觉得气馁。因为ƈ没有取得什么进展。如果查看整个P代,看到更多内容:您捕获了一个重要需求ƈ~写试用例实现需求。作Z名程序员Q我的第一个行为测试帮助我明确了开发过E。因为实现细节随着试的进行越来越清晰?/p> <p>现在Q我可以实现一个更健壮的状态实现。具体来Ԍ我需要处理状态机的多个状态。我需要创Z个新的规则获取有效状态列表。像以前一P我将q行试q查看是否通过?/p> <br /> <a name="listing10"><strong>清单 10. 实现有效状态规?/strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode"> it "should remember a list of valid states" do @machine.states = [:shopping, :checking_out] @machine.states.should = [:shopping, :checking_out] end run test(note: failing first verifies test) ~/rspec batate$ spec machine_spec.rb .F 1) NoMethodError in 'Machine should remember a list of valid states' undefined method `states=' for #<Machine:0x10c7154> ./machine_spec.rb:13: Finished in 0.005923 seconds 2 examples, 1 failure</pre> </td> </tr> </tbody> </table> <br /> <p>在清?10 中,出现了一?RSpec 形式的断a。该断言?<code>should</code> Ҏ开始,然后d了一些比较关pR?code>should</code> Ҏ对应用程序进行某U观察。工作中的应用程序应该以某种方式q行?code>should</code> Ҏ很好地捕获了q种需求。在本例中,我的状态机应该记忆两种不同的状态?/p> <p>现在Q应该添加一个实例变量来实际记忆状态。像以往一P我在修改代码后运行测试用例,q观察测试是否成功?/p> <br /> <a name="listing11"><strong>清单 11. 创徏一个属性以记忆状?/strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">class Machine attr_accessor :states def state :initial end end ~/rspec batate$ spec machine_spec.rb .. Finished in 0.00606 seconds 2 examples, 0 failures</pre> </td> </tr> </tbody> </table> <br /> <br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td><img height="1" alt="" src="http://www.ibm.com/i/v14/rules/blue_rule.gif" width="100%" /><br /> <img height="6" alt="" src="http://www.ibm.com/i/c.gif" width="8" border="0" /></td> </tr> </tbody> </table> <br /> <br /> <p><a name="N10199"><span id="wmqeeuq" class="atitle">驱动重构</span></a></p> <p>此时Q我q不惛_定将 <code>:initial</code> 状态称为状态机的第一个状态。相反,我更希望W一个状态是状态数l中的第一个元素。我对状态机的理解在不断演变。这U现象ƈ不少见。测试驱动开发经常迫使我重新考虑之前的假设。由于我已经通过试用例捕获了早期需求,我可以轻村֜对代码进行重构。在本例中,重构是对代码进行调_使其更好地工作?/p> <p>修改W一个测试,使其如清?12 所C,q运行测试:</p> <br /> <a name="listing12"><strong>清单 12. 初始状态应该ؓ指定的第一个状?/strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">it "should initially have a state of the first state" do @machine.states = [:shopping, :checking_out] @machine.state.should == :shopping end ~/rspec batate$ spec machine_spec.rb F. 1) 'Machine should initially have a state of the first state' FAILED expected :shopping, got :initial (using ==) ./machine_spec.rb:10: Finished in 0.005846 seconds 2 examples, 1 failure</pre> </td> </tr> </tbody> </table> <br /> <p>可以q样_试用例起到作用了,因ؓ它运行失败,因此我现在需要修改代码以使其工作。显而易见,我的d是使测试通过。我喜欢q种试目的Q因为我的测试用例正在驱动我q行设计。我把初始状态传递给 <code>new</code> Ҏ。我对实现E作修改Q以W合修改后的规范Q如清单 13 所C?/p> <br /> <a name="listing13"><strong>清单 13. 指定初始状?/strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">start to fix it class Machine attr_accessor :states attr_reader :state def initialize(states) @states = states @state = @states[0] end end ~/rspec batate$ spec machine_spec.rb 1) ArgumentError in 'Machine should initially have a state of the first state' wrong number of arguments (0 for 1) ./machine_spec.rb:5:in `initialize' ./machine_spec.rb:5:in `new' ./machine_spec.rb:5: 2) ArgumentError in 'Machine should remember a list of valid states' wrong number of arguments (0 for 1) ./machine_spec.rb:5:in `initialize' ./machine_spec.rb:5:in `new' ./machine_spec.rb:5: Finished in 0.006391 seconds 2 examples, 2 failures</pre> </td> </tr> </tbody> </table> <br /> <p>现在Q测试出C一些错误。我扑ֈ了实C的一?bug。测试用例不再用正的接口Q因为我没有把初始状态传递给状态机。可以看刎ͼ试用例已经起到了保护作用。我q行了较大的更改Q测试就发现?bug。我们需要对试q行重构以匹配新的接口,初始状态列表传递给 <code>new</code> Ҏ。在q里我ƈ没有重复初始化代码,而是其攄?<code>before</code> Ҏ中,如清?14 所C:</p> <br /> <a name="listing14"><strong>清单 14. ?“before” 中初始化状态机 </strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">require 'machine' describe Machine do before :each do @machine = Machine.new([:shopping, :checking_out]) end it "should initially have a state of the first state" do @machine.state.should == :shopping end it "should remember a list of valid states" do @machine.states.should == [:shopping, :checking_out] end end ~/rspec batate$ spec machine_spec.rb .. Finished in 0.005542 seconds 2 examples, 0 failures</pre> </td> </tr> </tbody> </table> <br /> <p>状态机开始逐渐成型。代码仍然有一些问题,但是正在向良好的方向演化。我开始对状态机q行一些{换。这些{换将促代码实际记忆当前状态?/p> <p>试用例促我全面地思?API 的设计。我需要知道如何表CZ件和转换。首先,我将使用一个散列表表示转换Q而没有用成熟的面向对象实现。随后,试需求可能会要求我修改假设条Ӟ但是目前Q我仍然保持q种单性。清?15 昄了修改后的代码:</p> <br /> <a name="listing15"><strong>清单 15. d事g和{?/strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">remember events... change before conditions require 'machine' describe Machine do before :each do @machine = Machine.new([:shopping, :checking_out]) @machine.events = {:checkout => {:from => :shopping, :to => :checking_out}} end it "should initially have a state of the first state" do @machine.state.should == :shopping end it "should remember a list of valid states" do @machine.states.should == [:shopping, :checking_out] end it "should remember a list of events with transitions" do @machine.events.should == {:checkout => {:from => :shopping, :to => :checking_out}} end end ~/rspec batate$ spec machine_spec.rb FFF 1) NoMethodError in 'Machine should initially have a state of the first state' undefined method `events=' for #<Machine:0x10c6f38> ./machine_spec.rb:6: 2) NoMethodError in 'Machine should remember a list of valid states' undefined method `events=' for #z7lt;Machine:0x10c5afc> ./machine_spec.rb:6: 3) NoMethodError in 'Machine should remember a list of events with transitions' undefined method `events=' for #<Machine:0x10c4a58> ./machine_spec.rb:6: Finished in 0.006597 seconds 3 examples, 3 failures</pre> </td> </tr> </tbody> </table> <br /> <p>׃新的试代码位于 <code>before</code> 中,我的三个测试分解开来。尽如此,清单 16 中展C的试非常Ҏ修复。我添加另一个访问程序:</p> <br /> <a name="listing16"><strong>清单 16. 记忆事g</strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">class Machine attr_accessor :states, :events attr_reader :state def initialize(states) @states = states @state = @states[0] end end ~/rspec batate$ spec machine_spec.rb ... Finished in 0.00652 seconds 3 examples, 0 failures test</pre> </td> </tr> </tbody> </table> <br /> <p>试全部通过。我得到了一个能正常q行的状态机。接下来的几个测试将使它更加完善?/p> <br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td><img height="1" alt="" src="http://www.ibm.com/i/v14/rules/blue_rule.gif" width="100%" /><br /> <img height="6" alt="" src="http://www.ibm.com/i/c.gif" width="8" border="0" /></td> </tr> </tbody> </table> <br /> <br /> <p><a name="N101FD"><span id="wmqeeuq" class="atitle">接近真实的应用程?/span></a></p> <p>目前为止Q我所做的不过是触发了一ơ状态{换,但是我已l做好了所有基工作。我得到了一l需求。我q构Z一l测试。我的代码可以ؓ状态机提供使用的数据。此Ӟ理单个状态机转换仅表CZơ简单的转换Q因此我添加如清单 17 所C的试Q?/p> <br /> <a name="listing17"><strong>清单 17. 构徏状态机的状态{?/strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">it "should transition to :checking_out upon #trigger(:checkout) event " do @machine.trigger(:checkout) @machine.state.should == :checking_out end ~/rspec batate$ spec machine_spec.rb ...F 1) NoMethodError in 'Machine should transition to :checking_out upon #trigger(:checkout) event ' undefined method `trigger' for #<Machine:0x10c4d00> ./machine_spec.rb:24: Finished in 0.006153 seconds 4 examples, 1 failure</pre> </td> </tr> </tbody> </table> <br /> <p>我需要抵制快速构建大量功能的诱惑。我应该只编写少量代码,只要使测试通过卛_。清?18 展示的P代将表示 API 和需求。这p够了Q?/p> <br /> <a name="listing18"><strong>清单 18. 定义 trigger Ҏ</strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">def trigger(event) @state = :checking_out end ~/rspec batate$ spec machine_spec.rb .... Finished in 0.005959 seconds 4 examples, 0 failures</pre> </td> </tr> </tbody> </table> <br /> <p>q里出现了一个有的Ҏ。在~写代码Ӟ我两ơ都弄错了这个简单的Ҏ。第一ơ我q回?<code>:checkout</code>Q第二次我将状态设|ؓ <code>:checkout</code> 而不?<code>:checking_out</code>。在试中用较的步骤可以为我节省大量旉Q因为测试用例ؓ我捕Lq些错误在将来的开发中很难捕获到。本文的最后一个步骤是实际执行一ơ状态机转换。在W一个示例中Q我q不兛_实际的机器状态是什么样子的。我仅仅是根据事件进行盲目{换,而不考虑状态?/p> <p>两节点的状态机无法执行q个操作Q我需要在W三个节点中构徏。我没有使用已有?<code>before</code> ҎQ只是在新状态中d另外的状态。我在试用例中进行两ơ{换,以确保状态机能够正确地执行{换,如清?19 所C:</p> <br /> <a name="listing19"><strong>清单 19. 实现W一ơ{?/strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">it "should transition to :success upon #trigger(:accept_card)" do @machine.events = { :checkout => {:from => :shopping, :to => :checking_out}, :accept_card => {:from => :checking_out, :to => :success} } @machine.trigger(:checkout) @machine.state.should == :checking_out @machine.trigger(:accept_card) @machine.state.should == :success end ~/rspec batate$ spec machine_spec.rb ....F 1) 'Machine should transition to :success upon #trigger(:accept_card)' FAILED expected :success, got :checking_out (using ==) ./machine_spec.rb:37: Finished in 0.007564 seconds 5 examples, 1 failure</pre> </td> </tr> </tbody> </table> <br /> <p>q个试?<code>:checkout</code> ?<code>:accept_card</code> 事g建立新的状态机。在处理{ևӞ我选择使用两个事g而不是一个,q样可以防止发生双命令。签Z码可以确保状态机在签Z前处?<code>shopping</code> 状态。第一ơ签出首先将状态机?<code>shopping</code> 转换?<code>checking_out</code>。测试用例通过触发 <code>checkout</code> ?<code>accept_card</code> 事g实现两个转换Qƈ在调用事件之后检验事件状态是否正。与预期一P试用例p| ?我ƈ没有~写处理多个转换的触发器Ҏ。代码修正包含一行非帔R要的代码。清?20 展示了状态机的核心: </p> <br /> <a name="listing20"><strong>清单 20. 状态机的核?/strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">def trigger(event) @state = events[event][:to] end ~/rspec batate$ spec machine_spec.rb ..... Finished in 0.006511 seconds 5 examples, 0 failures</pre> </td> </tr> </tbody> </table> <br /> <p>试可以q行。这些粗p的代码W一ơ演变ؓ真正可以UC为状态机的东ѝ但是这q远q不够。目前,状态机~Z严密性。不状态如何,状态机都会触发事g。例如,当处?<code>shopping</code> 状态时Q触?<code>:accept_card</code> q不会{换ؓ <code>:success</code> 状态。您只能够从 <code>:checking_out</code> 状态触?<code>:accept_card</code>。在~程术语中,<code>trigger</code> Ҏ的范围应针对事g。我编写一个测试来解决问题Q然后修?bug。我编写一个负试Qnegative testQ,xa一个不应该出现的行为,如清?21 所C:</p> <br /> <a name="listing21"><strong>清单 21: 负测?/strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">it "should not transition from :shopping to :success upon :accept_card" do @machine.events = { :checkout => {:from => :shopping, :to => :checking_out}, :accept_card => {:from => :checking_out, :to => :success} } @machine.trigger(:accept_card) @machine.state.should_not == :success end rspec batate$ spec machine_spec.rb .....F 1) 'Machine should not transition from :shopping to :success upon :accept_card' FAILED expected not == :success, got :success ./machine_spec.rb:47: Finished in 0.006582 seconds 6 examples, 1 failure</pre> </td> </tr> </tbody> </table> <br /> <p>现在可以再次q行试Q其中一个测试如预期一栯行失败。修复代码同样只有一行,如清?22 所C:</p> <br /> <a name="listing22"><strong>清单 22. 修复 trigger 中的范围问题 </strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">def trigger(event) @state = events[event][:to] if state == events[event][:from] end rspec batate$ spec machine_spec.rb ...... Finished in 0.006873 seconds 6 examples, 0 failures</pre> </td> </tr> </tbody> </table> <br /> <br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td><img height="1" alt="" src="http://www.ibm.com/i/v14/rules/blue_rule.gif" width="100%" /><br /> <img height="6" alt="" src="http://www.ibm.com/i/c.gif" width="8" border="0" /></td> </tr> </tbody> </table> <br /> <br /> <p><a name="N1029C"><span id="wmqeeuq" class="atitle">l合代码</span></a></p> <p>现在Q我h一个可单运行的状态机。无Z哪方面来_它都不是一个完的E序。它q具有下面这些问题:</p> <ul> <li>状态散列实际上不具备Q何功能。我应该Ҏ状态对事g及其转换q行验证Q或者将所有状态集中v来。后l需求很可能会要求这样做? <li>某个既定事g只能存在于一个状态中。这U限制ƈ不合理。例如,<code>submit</code> ?<code>cancel</code> 事g可能需要处于多个状态? <li>代码q不具备明显的面向对象特征。ؓ佉K|保持简单,我将大量数据|入散列中。后l的q代会进一步驱动设计,使其朝面向对象设计方向发展?</li> </ul> <p>但是Q您q可以看刎ͼq个状态机已经能够满一些需求了。我q具备一个描q系l行为的文档Q这是进行一pd试的好L。每个测试用例都支持pȝ的一个基本需求。事实上Q通过q行 <code>spec machine_spec.rb --format specdoc</code>Q您可以查看ql规范组成的基本报告Q如清单 23 所C:</p> <br /> <a name="listing23"><strong>清单 23. 查看规范</strong></a><br /> <table cellspacing="0" cellpadding="0" width="100%" border="0"> <tbody> <tr> <td class="code-outline"> <pre class="displaycode">spec machine_spec.rb --format specdoc Machine - should initially have a state of the first state - should remember a list of valid states - should remember a list of events with transitions - should transition to :checking_out upon #trigger(:checkout) event - should transition to :success upon #trigger(:accept_card) - should not transition from :shopping to :success upon :accept_card Finished in 0.006868 seconds</pre> </td> </tr> </tbody> </table> <br /> <p>试驱动Ҏq不适合所有hQ但是越来越多的人开始用这U技术,使用它构建具有灵zL和适应性的高质量代码,q且Ҏ试从头构徏代码。当Ӟ您也可以通过其他框架Q如 test_unitQ获得相同的优点。RSpec q提供了优秀的实现方法。这U新试框架的一大亮点就是代码的表示。新手尤其可以从q种行ؓ驱动的测试方法中受益。请试使用该框架ƈ告诉我您的感受?/p> <img src ="http://www.aygfsteel.com/lzj520/aggbug/155374.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.aygfsteel.com/lzj520/" target="_blank">lzj520</a> 2007-10-23 19:01 <a href="http://www.aygfsteel.com/lzj520/archive/2007/10/23/155374.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Q{QRolling with Ruby on Rails, Part 2 (onlamp.com)http://www.aygfsteel.com/lzj520/archive/2006/11/04/79074.htmllzj520lzj520Sat, 04 Nov 2006 08:36:00 GMThttp://www.aygfsteel.com/lzj520/archive/2006/11/04/79074.htmlhttp://www.aygfsteel.com/lzj520/comments/79074.htmlhttp://www.aygfsteel.com/lzj520/archive/2006/11/04/79074.html#Feedback0http://www.aygfsteel.com/lzj520/comments/commentRss/79074.htmlhttp://www.aygfsteel.com/lzj520/services/trackbacks/79074.htmlAre you ready? Let's begin!

Updating Ruby on Rails

When I wrote Part 1, the current version of Rails was 0.9.3. At the time of this writing, Rails is up to version 0.10.0 and has some useful new features. I will use Rails 0.10.0 for this article. If you installed Rails after February 24, 2005, you already have 0.10.0 installed.

Figure 1 shows how to see what RubyGems you have installed (and their version numbers). As with Part 1, I am working on a Windows system, so you will need to translate if you use a different platform.

listing installed RubyGems
Figure 1. Listing installed RubyGems

Open a command window and run the command:

				
gem list --local

Tip: the command gem list --remote will show all the available RubyGems on the remote gem server on rubyforge.org.

If you don't have Rails 0.10.0 (or later) installed, then you will need to rerun the command:

				
gem install rails

MySQL security update

In Part 1, I recommended that you leave the MySQL root password blank because (at the time of writing) Rails did not support MySQL's new password protocol. Many of you were not happy with this state of affairs, and to make matters worse, there is now a virus that exploits password vulnerabilities in MySQL on Windows.

Happily, starting with version 0.9.4, Rails now supports the new password protocol.

New scaffold feature

Rails has a new scaffold feature, which I won't explore here, but it's cool enough that I want to make sure you know about it. This is best illustrated by an example.

Part 1 showed how to create a recipe model and controller with the commands:

				
ruby script\generate model Recipe
ruby script\generate controller Recipe

I then instantiated the scaffolding by inserting scaffold :recipe into the RecipeController class. The resulting CRUD controllers and view templates were created on the fly and are not visible for inspection.

The technique described above still works, but you now have another option. Run the command:

				
ruby script\generate scaffold Recipe

This generates both the model and the controller, plus it creates scaffold code and view templates for all CRUD operations. This allows you to see the scaffold code and modify it to meet your needs. Be careful using this if you've already created models, controllers, or view templates, as it will overwrite any existing files as it creates the scaffold code.

Completing the Recipe Application

It's time to round out the recipe application a bit. After that I'll present some other features of Rails that I'm sure you'll want to know about.

Remember that I created my cookbook application in the directory c:\rails\cookbook; all paths used in this article assume this base directory. If you chose a different location, please be sure to make the proper adjustments to the application paths you see in this article.

You can also download my cookbook source code for this tutorial in one single zip file. This works with Rails 0.13 and later, so if you're still using an older version, I suggest that you follow the upgrade instructions.

For those of you who are cheating (you know who you are) and plan to just download my source code without going through Part 1, you will also need to create a database named cookbook in MySQL and populate it using cookbook.sql.

Creating a new recipe with a category

Because the code still relies on the scaffolding to create new recipes, there is no way to assign a category to a recipe. This wouldn't be so bad--except that the page created to list all recipes assumes that every recipe will have a category, and it generates an error if this is not true. That means that in the way I left things in Part 1, if you add a new recipe, you'll receive errors while trying to list them.

The fix is to take over the new action from the scaffolding just as I showed already with the edit action. Edit c:\rails\cookbook\app\controllers\recipe_controller.rb and add a new method like in Figure 2.

the Recipe controller's new method
Figure 2. The Recipe controller's new method

The code @recipe = Recipe.new creates a new, empty recipe object and assigns it to the instance variable @recipe. Remember, an instance of the Recipe class represents a row in the recipes database table. When creating a new recipe object, the Recipe class can assign default values for each field that the view template can use.

The Recipe model class doesn't currently set any such default values, but the view template I'll show off momentarily will use whatever is in the @recipe object to initialize the display form. Later, you could add default values in the Recipe class that will show up when you create a new recipe.

As with the edit action, this also retrieves a collection of all categories so that it can display a drop-down list of categories from which the user can choose. The @categories instance variable holds this list of categories.

In the directory c:\rails\cookbook\app\views\recipe, create a file named new.rhtml that contains the HTML template shown below. It's mostly standard HTML, with some extra code to create the <select> and <option> tags for the drop-down list of categories:

				
<html>
<head>
<title>New Recipe</title>
</head>
<body>
<h1>New Recipe</h1>
<form action="/recipe/create" method="post">
<p>
<b>Title</b><br/>
<input id="recipe_title" name="recipe[title]" size="30" type="text" value=""/>
</p>
<p>
<b>Description</b><br/>
<input id="recipe_description" name="recipe[description]"
size="30" type="text" value=""/>
</p>
<p>
<b>Category:</b><br/>
<select name="recipe[category_id]">
<% @categories.each do |category| %>
<option value="<%= category.id %>">
<%= category.name %>
</option>
<% end %>
</select>
</p>
<p>
<b>Instructions</b><br/>
<textarea cols="40" id="recipe_instructions" name="recipe[instructions]"
rows="20" wrap="virtual">
</textarea>
</p>
<input type="submit" value="Create"/>
</form>
<a href="/recipe/list">Back</a>
</body>
</html>

This is not much different from the edit template from Part 1. I left out the recipe's date because I'll set it to the current date when a user posts the form back to the web app. This ensures that the recipe's date will always be its creation date.

If you look at the form tag, you will see that this form will post to a create action in the recipe controller. Edit c:\rails\cookbook\app\controllers\recipe_controller.rb and add this create method:

				
def create
@recipe = Recipe.new(@params['recipe'])
@recipe.date = Date.today
if @recipe.save
redirect_to :action => 'list'
else
render_action 'new'
end
end

This method first creates a new recipe object and initializes it from the parameters posted by the form in new.rhtml. Then it sets the recipe's date to today's date, and tells the recipe object to save itself to the database. If the save is successful, it redirects to the list action that displays all recipes. If the save fails, it redirects back to the new action so the user can try again.

Give it a try. Start the web server by opening a command window, navigating to c:\rails\cookbook, and running the command ruby script\server. Then browse to http://127.0.0.1:3000/recipe/new and add a new recipe like the one shown in Figure 3.

adding a new recipe with a category
Figure 3. Adding a new recipe with a category

After you create the new recipe, you should see something like Figure 4.

list of all recipes
Figure 4. List of all recipes

Deleting a recipe

If you remember from Part 1, once I took over the list action from the scaffolding I no longer had a way to delete a recipe. The list action must implement this. I'm going to add a small delete link after the name of each recipe on the main list page that will delete its associated recipe when clicked. This is easy.

First, edit c:\rails\cookbook\app\views\recipe\list.rhtml and add the delete link by making it look like this:

						
<html>
<head>
<title>All Recipes</title>
</head>
<body>
<h1>Online Cookbook - All Recipes</h1>
<table border="1">
<tr>
<td width="40%"><p align="center"><i><b>Recipe</b></i></td>
<td width="20%"><p align="center"><i><b>Category</b></i></td>
<td width="20%"><p align="center"><i><b>Date</b></i></td>
</tr>
<% @recipes.each do |recipe| %>
<tr>
<td>
<%= link_to recipe.title,
:action => "show",
:id => recipe.id %>
<font size=-1>
   
<%= link_to "(delete)",
{:action => "delete", :id => recipe.id},
:confirm => "Really delete #{recipe.title}?" %>
</font>
</td>
<td><%= recipe.category.name %></td>
<td><%= recipe.date %></td>
</tr>
<% end %>
</table>
<p><%= link_to "Create new recipe", :action => "new" %></p>
</body>
</html>

The main change here is the addition of this link:

						
<%= link_to "(delete)", {:action => "delete", :id
=> recipe.id},
:confirm => "Really delete #{recipe.title}?" %>

This is different from the previous ones. It uses an option that generates a JavaScript confirmation dialog. If the user clicks on OK in this dialog, it follows the link. It takes no action if the user clicks on Cancel.

Try it out by browsing to http://127.0.0.1:3000/recipe/list. Try to delete the Ice Water recipe, but click on Cancel when the dialog pops up. You should see something like Figure 5.

confirm deleting the ice water recipe
Figure 5. Confirm deleting the Ice Water recipe

Now try it again, but this time click on OK. Did you see the results shown in Figure 6?

error deleting the ice water recipe
Figure 6. Error deleting the Ice Water recipe

Alright, I admit it; I did this on purpose to remind you that it's OK to make mistakes. I added a link to a delete action in the view template, but never created a delete action in the recipe controller.

Edit c:\rails\cookbook\app\controllers\recipe_controller.rb and add this delete method:

						
def delete
Recipe.find(@params['id']).destroy
redirect_to :action => 'list'
end

The first line of this method finds the recipe with the ID from the link, then calls the destroy method on that recipe. The second line merely redirects back to the list action.

Try it again. Browse to http://127.0.0.1:3000/recipe/list and try to delete the Ice Water recipe. Now it should look like Figure 7, and the Ice Water recipe should be gone.

ice water recipe is gone
Figure 7. Ice Water recipe is gone

Using layouts

Part 1 used Rails' scaffolding to provide the full range of CRUD operations for categories, but I didn't have to create any links from our main recipe list page. Instead of just throwing in a link on the recipe list page, I want to do something more generally useful: create a set of useful links that will appear at the bottom of every page. Rails has a feature called layouts, which is designed just for things like this.

Most web sites that have common headers and footers across all of the pages do so by having each page "include" special header and footer text. Rails layouts reverse this pattern by having the layout file "include" the page content. This is easier to see than to describe.

Edit c:\rails\cookbook\app\controllers\recipe_controller.rb and add the layout line immediately after the class definition, as shown in Figure 8.

adding a layout to the recipe controller
Figure 8. Adding a layout to the recipe controller

This tells the recipe controller to use the file standard-layout.rhtml as the layout for all pages rendered by the recipe controller. Rails will look for this file using the path c:\rails\cookbook\app\views\layouts\standard-layout.rhtml, but you will have to create the layouts directory because it doesn't yet exist. Create this layout file with the following contents:

						
<html>
<head>
<title>Online Cookbook</title>
</head>
<body>
<h1>Online Cookbook</h1>
<%= @content_for_layout %>
<p>
<%= link_to "Create new recipe",
:controller => "recipe",
:action => "new" %>
  
<%= link_to "Show all recipes",
:controller => "recipe",
:action => "list" %>
  
<%= link_to "Show all categories",
:controller => "category",
:action => "list" %>
</p>
</body>
</html>

Only one thing makes this different from any of the other view templates created so far--the line:

						
<%= @content_for_layout %>

This is the location at which to insert the content rendered by each recipe action into the layout template. Also, notice that I have used links that specify both the controller and the action. (Before, the controller defaulted to the currently executing controller.) This was necessary for the link to the category list page, but I could have used the short form on the other two links.

Before you try this out, you must perform one more step. The previous recipe view templates contain some HTML tags that are now in the layout, so edit c:\rails\cookbook\app\views\recipe\list.rhtml and delete the extraneous lines at the beginning and end to make it look like this:

						
<table border="1">
<tr>
<td width="40%"><p align="center"><i><b>Recipe</b></i></td>
<td width="20%"><p align="center"><i><b>Category</b></i></td>
<td width="20%"><p align="center"><i><b>Date</b></i></td>
</tr>
<% @recipes.each do |recipe| %>
<tr>
<td>
<%= link_to recipe.title,
:action => "show",
:id => recipe.id %>
<font size=-1>
   
<%= link_to "(delete)",
{:action => "delete", :id => recipe.id},
:confirm => "Really delete #{recipe.title}?" %>
</font>
</td>
<td><%= recipe.category.name %></td>
<td><%= recipe.date %></td>
</tr>
<% end %>
</table>

Similarly, edit both c:\rails\cookbook\app\views\recipe\edit.rhtml and c:\rails\cookbook\app\views\recipe\new.rhtml to delete the same extraneous lines. Only the form tags and everything in between should remain.

Browse to http://127.0.0.1:3000/recipe/list, and it should look like Figure 9.

using a layout with common links
Figure 9. Using a layout with common links

The three links at the bottom of the page should now appear on every page displayed by the recipe controller. Go ahead and try it out!

If you clicked on the "Show all categories" link, you probably noticed that these nice new links did not appear. That is because the category pages display through the category controller, and only the recipe controller knows to use the new layout.

To fix that, edit c:\rails\cookbook\app\controllers\category_controller.rb and add the layout line as shown in Figure 10.

adding a layout to the recipe controller
Figure 10. Adding a layout to the category controller

Now you should see the common links at the bottom of all pages of the recipe web application.

Showing recipes in a category

The final task is to add the ability to display only those recipes in a particular category. I'll take the category displayed with each recipe on the main page and turn it into a link that will display only the recipes in that category.

To do this, I'll change the recipe list view template to accept a URL parameter that specifies what category to display, or all categories if the user has omitted the parameter. First, I need to change the list action method to retrieve this parameter for use by the view template.

Edit c:\rails\cookbook\app\controllers\recipe_controller.rb and modify the list method to look like this:

						
def list
@category = @params['category']
@recipes = Recipe.find_all
end

Then edit c:\rails\cookbook\app\views\recipe\list.rhtml to look like this:

						
<table border="1">
<tr>
<td width="40%"><p align="center"><i><b>Recipe</b></i></td>
<td width="20%"><p align="center"><i><b>Category</b></i></td>
<td width="20%"><p align="center"><i><b>Date</b></i></td>
</tr>
<% @recipes.each do |recipe| %>
<% if (@category == nil) || (@category == recipe.category.name)%>
<tr>
<td>
<%= link_to recipe.title,
:action => "show",
:id => recipe.id %>
<font size=-1>
   
<%= link_to "(delete)",
{:action => "delete", :id => recipe.id},
:confirm => "Really delete #{recipe.title}?" %>
</font>
</td>
<td>
<%= link_to recipe.category.name,
:action => "list",
:category => "#{recipe.category.name}" %>
</td>
<td><%= recipe.date %></td>
</tr>
<% end %>
<% end %>
</table>

There are two changes in here that do all the work. First, this line:

						
<% if (@category == nil) || (@category == recipe.category.name)%>

decides whether to display the current recipe in the loop. If the category is nil (there was no category parameter on the URL), or if the category from the URL parameter matches the current recipe's category, it displays that recipe.

Second, this line:

						
<%= link_to recipe.category.name,
:action => "list",
:category => "#{recipe.category.name}" %>

creates a link back to the list action that includes the proper category parameter.

Browse to http://127.0.0.1:3000/recipe/list and click on one of the Snacks links. It should look like Figure 11.

showing only snacks
Figure 11. Showing only snacks

What is it? How long did it take?

That's it! This is a reasonably functional online cookbook application developed in record time. It's a functional skeleton just begging for polish.

Wading through all of the words and screenshots in this article may have obscured (at least somewhat) exactly what this code can do and in what amount of developer time. Let me present some statistics to try to put it all into perspective.

Fortunately, Rails has some built-in facilities to help answer these questions. Open up a command window in the cookbook directory (c:\rails\cookbook) and run the command:

						
rake stats

Your results should be similar to Figure 12. Note that LOC means "lines of code."

viewing development statistics
Figure 12. Viewing development statistics

I won't give a detailed description of each number produced, but the last line has the main figure I want to point out:

						
Code LOC: 47

This says that the actual number of lines of code in this application (not counting comments or test code) is 47. It took me about 30 minutes to create this application! I could not have come even close to this level of productivity in any other web app development framework that I have used.

Maybe you're thinking that this is an isolated experience using an admittedly trivial example. Maybe you're thinking that this might be OK for small stuff, but it could never scale. If you harbor any such doubts, the next section should lay those to rest.

Ruby on Rails Success Stories

Rails is a relatively young framework. As of this writing, it's been barely six months since the first public release. Yet it debuted with such a stunning feature set and solid stability that a vibrant developer community quickly sprang up around it. Within this time frame, several production web applications have been deployed that were built with Ruby on Rails.



lzj520 2006-11-04 16:36 发表评论
]]>
վ֩ģ壺 Ϸ| ׼| | ޵| Դ| Ӣ| | | ֬| | ӱ| | | | | Į| ֹ| Զ| ͬ| ̨| | | ״| | ־| | Ͳ| | | | ־| Թ| | ԣ| ̨| | ˫| Ǭ| | | |