乐者为王

Do one thing, and do it well.

用Rails 2.3打造todolist应用

首先生成项目骨架:

1
2
3
4
rails todolist
cd todolist
script/generate scaffold todo title:string description:text done:boolean due_date:datetime
rake db:migrate

user-todo-association

安装认证和授权插件:

1
2
3
4
5
script/plugin install git://github.com/technoweenie/restful-authentication.git restful_authentication
script/generate authenticated user sessions

script/plugin install git://github.com/greenisus/forgot_password.git
script/generate forgot_password password user

然后将include AuthenticatedSystem移到ApplicationController中:

1
2
class ApplicationController < ActionController::Base
  include AuthenticatedSystem

添加Todo和User的关联:

1
2
3
4
5
class Todo < ActiveRecord::Base
  belongs_to :user

class User < ActiveRecord::Base
  has_many :todos

修改TodosController,将Todo和User绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class TodosController < ApplicationController
  before_filter :login_required

  def index
    @todos = current_user.todos

  def show
    @todo = current_user.todos.find(params[:id])

  def new
    @todo = Todo.new

  def edit
    @todo = current_user.todos.find(params[:id])

  def create
    @todo = Todo.new(params[:todo])
    @todo.user = current_user

  def update
    @todo = current_user.todos.find(params[:id])

  def destroy
    @todo = current_user.todos.find(params[:id])

创建一个应用的首页:

1
script/generate controller home index

为了可以访问到应用首页,需要删除public/index.html文件,并且在routes.rb中添加:

1
map.root :controller => 'home'

将app/views/layouts下的todos.html.erb改名为application.html.erb,然后添加下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<% if logged_in? -%>
  <div id="user-bar-greeting">
    Logged in as <%= link_to_current_user :content_method => :login %>
  </div>
  <div id="user-bar-action">
    (<%= link_to "Log out", logout_path, { :title => "Log out" } %>)
  </div>
<% else -%>
  <div id="user-bar-action">
    <%= link_to "Log in", login_path, { :title => "Log in" } %> /
     <%= link_to "Sign up", signup_path, { :title => "Create an account" } %>
  </div>
<% end -%>

Todo模型的description字段是text类型,在页面上用textarea表示简单了一点,将它改成使用TinyMCE编辑器。可以使用tinymce_hammer插件来集成。

1
script/plugin install git://github.com/trevorrowe/tinymce_hammer.git

执行以下命令后,将会安装TinyMCE到public/javascript/tiny_mce目录:

1
script/generate tinymce_installation

在layout下的模板中添加下面这条语句:

1
<%= init_tinymce_hammer_if_required %>

将app/views/todos下new.html.erb和edit.html.erb中的

1
<%= f.text_area :description %>

修改为

1
<%= f.tinymce :description, :rows => 10, :cols => 40 %>

去除app/views/todos/index.html.erb中description字段的h方法:

1
<td><%= todo.description %></td><br />

还有就是删除某条Todo后记录就彻底地没了,不能恢复,需要给它加上一剂后悔药,使之可以重新被捞出,acts_as_paranoid插件可以做到这点。

1
2
3
script/plugin install git://github.com/technoweenie/acts_as_paranoid.git
script/generate migration add_deleted_at_to_todos deleted_at:datetime
rake db:migrate

添加acts_as_paranoid到模型中:

1
2
class Todo < ActiveRecord::Base
  acts_as_paranoid

现在,调用这个模型的destroy方法将不会真正地删除记录,只会将记录从视图上移除,在deleted_at里记录删除的时间。当然,你可以在find中使用with_deleted或only_deleted参数得到被隐藏的记录。在Rails 3中在find中使用参数会报ArgumentError,显示如下错误:

1
Unknown key: only_deleted

解决办法是使用以下格式的代码:

1
current_user.todos.only_deleted.find(:all)

至此,一个简单的todolist就算完成了。

代码下载:https://github.com/dohkoos/todolist

启用Windows Server 2008无线网络支持

Windows Server 2008系统安装后,大家会发现虽然无线网卡能够被识别并正确安装了驱动,但是却无法接入到无线网络中。Windows 2008本是为服务器设计的系统,自然不会主动把无线服务安装,所以需要通过服务器管理器来添加无线局域网服务。

运行servermanager.msc进入服务器管理器,选中视图左窗中的“Features”,之后单击视图右窗中的“Add Features”添加功能。进入功能选择界面后,勾选“Wireless LAN Service”,根据向导提示进行安装。如下图:

wireless-lan-service

Plugin not found

使用Ruby 1.8.7和Rails 2.3.5,每次执行script/plugin install命令都出现:

1
Plugin not found: [...]

无论使用何种协议,或是在末尾添加斜杠都不起作用。

出现这个问题的原因是因为Ruby 1.8.7是用mingw32编译的,可以通过ruby -v查看:

1
ruby 1.8.7 (2010-08-16 patchlevel 302) [i386-mingw32]

RUBY_PLATFORM的值是i386-mingw32-{version},而许多库在判断当前操作系统时是这么做的:

1
file.open(RUBY_PLATFORM.match(/mswin/) ? 'NUL', '/dev/null')

这些库只判断了RUBY_PLATFORM里是否含有mswin,如果没有就认为是*nix平台,从而使用/dev/null,结果可想而知,文件必然打开失败,于是就会出现上述错误。

解决方式(推荐使用第4种方式):

方法1:打开script/plugin文件加入一行RUBY_PLATFORM = 'mswin',运行时会打印一条警告说常量重新赋值,不过不影响使用。

方法2:卸载掉1.8.7,重新安装Ruby 1.8.6-p26,这个版本是使用VC6编译的,RUBY_PLATFORM的值是mswin,不会出现平台判断错误。

方法3:使用VC自己编译Ruby 1.8.7,确保RUBY_PLATFORM的值是mswin即可。

方法4:打开RUBY_GEMS/activesupport-2.3.5/lib/active_support/core_ext/kernel/reporting.rb,找到

1
stream.reopen(RUBY_PLATFORM =~ /mswin/ ? 'NUL:' : '/dev/null')

将之修改以下代码即可

1
stream.reopen(RUBY_PLATFORM =~ /mswin|mingw/ ? 'NUL:' : '/dev/null')

使用will_paginate插件给Rails应用添加分页功能

安装will_paginate插件:

1
script/plugin install git://github.com/mislav/will_paginate.git

安裝好插件后,在action中將本來的find方法:

1
@contacts = Contact.all

改为

1
@contacts = Contact.paginate(:page => params[:page])

然后在action对应的view最后加入以下代码:

1
<%= will_paginate @contacts %>

现在便可以使用分页功能了,该语句会产生如下的HTML代码:

1
2
3
4
5
6
<div class="pagination">
  <span class="disabled prev_page">« Previous</span>
  <span class="current">1</span>
  <a href="/contacts?page=2&s=" rel="next">2</a>
  <a href="/contacts?page=2&s=" class="next_page" rel="next">Next »</a>
</div>

下面加入搜索功能,在view的适当位置加入:

1
2
3
4
5
6
<% form_tag contacts_path, :method => 'get' do %>
<p>
  <%= text_field_tag :s, params[:s] %>
  <%= submit_tag "Search", :name => nil %>
</p>
<% end %>

并且将action中的代码修改为:

1
2
3
@contacts = Contact.paginate(
  :page => params[:page],
  :conditions => ["name like ?", "%#{params[:s]}%"])

试着进行搜索,可以看到搜索结果也很好的进行了分页。

此外will_paginate还提供一些分页统计信息:

1
2
3
Total entries: <%= @contacts.total_entries %>
Total pages: <%= @contacts.total_pages %>
Current page: <%= @contacts.current_page %>

最后加上will_paginate推荐的CSS代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
.pagination {
  padding: 3px;
  margin: 3px;
}

.pagination a {
  padding: 2px 5px 2px 5px;
  margin: 2px;
  border: 1px solid #aaaadd;
  text-decoration: none;
  color: #000099;
}

.pagination a:hover, .pagination a:active {
  border: 1px solid #000099;
  color: #000;
}

.pagination span.current {
  padding: 2px 5px 2px 5px;
  margin: 2px;
  border: 1px solid #000099;
  font-weight: bold;
  background-color: #000099;
  color: #fff;
}

.pagination span.disabled {
  padding: 2px 5px 2px 5px;
  margin: 2px;
  border: 1px solid #eee;
  color: #ddd;
}

在写代码的时候碰到一个问题,网上的文章都使用page_count来表示总的分页数,但我在使用时出现了undefined method 'page_count'错误,把page_count换成total_pages后就好了。

2011/1/4更新

这里使用的是2.3.12版本的will_paginate,暂时还不支持i18n,不过可以通过在app/helps/application_helper.rb中添加以下代码来实现:

1
2
3
4
5
6
7
8
9
include WillPaginate::ViewHelpers

def will_paginate_with_i18n(collection, options = {})
  will_paginate_without_i18n(collection, options.merge(
                        :previous_label => I18n.t(:previous, :default => 'Previous'),
                        :next_label => I18n.t(:next), :default => 'Next'))
  end

alias_method_chain :will_paginate, :i18n

然后在config/locales/zh.yml中添加:

1
2
previous: "« 前一页"
next: "后一页 »"

findViewById()返回null

通过findViewById()获取一个Button时一直返回null值,百思不得其解。最后发现是因为在layout文件中使用了id的旧风格,如下所示:

1
<Button id="@+id/btn_okay" />

改成android:id就成功了。

1
<Button android:id="@+id/btn_okay" />

构建一个更友好的404页面

用户访问网站时偶尔会遇到404错误。404是一个HTTP状态码,告诉用户请求的网页不存在或链接错误。大多数网站托管和Web服务器会提供自己的默认错误页面,但如果开发者有提供定制的404页面,就可以增强用户的体验,对搜索引擎也更友好。定制的404页面非常非常有用,它不仅提供有关错误信息给用户,也会提供即时反馈给开发者,以便问题能被尽快修复;还可以告知搜索引擎链接已经失效,不要再索引。

这里是我对定制404页面的一些思考。

问题的分析

为了提供有用且特定的信息给用户,需要界定404错误可能的原因。按照页面访问的来源,错误大致可以分为以下4种:

  1. 用户拼错URL地址或者使用过期的书签。
  2. 站内的失效链接。
  3. 其它网站返回的失效链接。
  4. 搜索引擎返回的失效链接。

其中后面3种都是由于链接被修改、页面被删除或被移到其它地方所致。

问题的解决

为了确定404错误的来源,我们的代码需要访问HTTP Referer,它会告诉我们用户是从哪个页面链接过来的。以下是大概的代码执行步骤:

  1. 检测HTTP Referer以确定404错误的来源;
  2. 显示适当的消息给用户;
  3. 如果是特定的错误,发送邮件给开发者。

问题1:用户拼错URL地址或者使用过期的书签

在这种情况下,HTTP Referer通常为空,所以我们需要使用下面的代码做检查:

1
if request.referer.blank?

然后是定制404页面上的显示消息,告诉用户问题是什么:

1
2
3
4
5
很抱歉,你试图访问的页面 http://example.com/no-such-page.html 不存在。

看起来你似乎拼错了URL地址或者使用了过期的书签。

你或许应该试试搜索这个网站或者使用我们的站点地图去找到你想要的东西。

因为这是用户导致的错误,且没有问题要被修复,所以这里就不需要发送邮件给开发者。

问题2:站内的失效链接

当HTTP Referer不为空时,要检查它是指向本站、其它网站还是搜索引擎。如果包含本站域名,那么就可以知道用户是从站内其它页面过来的。这里使用以下的代码检测:

1
unless request.referer.index(request.host).nil?

然后,我们就可以给用户显示消息,告诉他该页面的链接失效了。

1
2
3
很抱歉,你试图访问的页面 http://example.com/no-such-page.html 不存在。

很明显,我们的页面上出现了失效链接。已经给开发者发送了邮件,问题将会很快被修复。你无需做任何事情。

并且,在显示消息给用户的同时发送包含所有必要信息的邮件给开发者。邮件的标题要清晰地指出哪个域名站点上有失效链接,邮件的内容要告诉开发者用户所在页面以及请求页面的URL地址。

1
2
3
4
5
6
7
8
9
From: example.com 404 error

Subject: 在站点 example.com 上有失效链接

Message: 站内的失效链接

在 http://example.com/badlink.html 页面上似乎存在失效链接。
有人正试图通过这个页面去访问 http://example.com/no-such-page.html 页面。
你应该去检查下该页面是否有什么问题。

问题3:其它网站返回的失效链接

如果404错误是由其它网站上的失效链接造成,那么可以显示以下的信息给用户:

1
2
3
4
5
很抱歉,你试图访问的页面 http://example.com/no-such-page.html 不存在。

很明显,在你过来的页面上存在失效链接。我们已经注意到了这个情况,并将试图去联系该页面的所有者修复它。

你或许应该试试搜索这个网站或者使用我们的站点地图去找到你想要的东西。

同时发送邮件给开发者,让开发者访问有失效链接的页面,如果页面上有所有者的联系信息,那么就可以通知他们修复这个问题。

1
2
3
4
5
6
7
8
9
From: example.com 404 error

Subject: 在站点 domain.com 上有失效链接

Message: 其它网站返回的失效链接

在 http://domain.com/badlink.html 页面上似乎存在失效链接。
有人正试图通过这个页面去访问 http://example.com/no-such-page.html 页面。
你应该联系页面的所有者去检查下该页面有什么问题。

问题4:搜索引擎返回的失效链接

为了确定用户是来自搜索引擎的结果页面,需要用一个搜索引擎URL列表来检测HTTP Referer,这个列表是一个排序的文本文件,这样我们可以在任何时候更新列表而不需要修改代码。这里是做检查的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
from_searchengine = false

IO.readlines('searchengines.txt') do |searchengine|
  unless request.referer.index(searchengine).nil?
    from_searchengine = true
    break
  end
end

if from_searchengine
  # do something
end

在这种情况下,我们要让用户知道搜索引擎返回的是旧链接:

1
2
3
4
5
很抱歉,你试图访问的页面 http://example.com/no-such-page.html 不存在。

看起来似乎是搜索引擎返回了旧页面的链接,等到搜索引擎更新索引后旧链接就会被移除。

你或许应该试试搜索这个网站或者使用我们的站点地图去找到你想要的东西。

因为没有什么我们可以做的,所以不需要发送邮件给开发者。

创建视图时的with check option选项

通过有with check option选项的视图操作基表,有以下结论:

  1. 首先视图只操作它可以查询出来的数据,对于它查询不出的数据,即使基表有,也不可以通过视图来操作;
  2. 对于update,有with check option,要保证update后,数据能被视图查询出来;
  3. 对于delete,有无with check option都一样;
  4. 对于insert,有with check option,要保证insert后,数据要被视图查询出来。

对于没有where子句的视图,使用with check option是多余的。

下面用一个例子来说明第4条:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE students (
    id int NOT NULL AUTO_INCREMENT,
    name varchar(50) NOT NULL,
    age int,
    sex char(1),
    PRIMARY KEY (id)
);

CREATE VIEW male_students_view AS
    SELECT name, age FROM students
    WHERE sex = 'M'
    WITH CHECK OPTION;
1
2
-- 报CHECK OPTION failed错误,原因如4,插入数据后要还能被视图查询出来。
INSERT INTO male_students_view VALUES('WU', 18);
1
2
-- 报Column count doesn't match value count错误,因为视图中根本没有sex列。
INSERT INTO male_students_view VALUES('WU', 18, 'M');

将students.sex列改成默认值为M:

1
ALTER TABLE students MODIFY sex char(1) NOT NULL DEFAULT 'M'
1
2
-- 执行成功,1 row affected。
INSERT INTO male_students_view VALUES('WU', 18);
1
2
-- 还是同样的问题,Column count doesn't match value count。
INSERT INTO male_students_view VALUES('WU', 18, 'M');

或者不修改students.sex的属性,将视图改成:

1
2
3
4
CREATE VIEW male_students_view AS
    SELECT name, age, sex FROM students
    WHERE sex = 'M'
    WITH CHECK OPTION;
1
2
-- 会报CHECK OPTION failed错误。
INSERT INTO male_students_view VALUES('WU', 18);
1
2
-- 执行成功,1 row affected。
INSERT INTO male_students_view VALUES('WU', 18, 'M');

以上例子都在MySQL上实际执行过。

使用SQL Server 2000个人版出现的问题

下午在机房值班,14:00的时候有人报告说交易客户端不能登录了,确定问题后两同事急忙重启AR和AS以及交易网关,可是还是不能登录。紧急重启数据库后问题才解决。

出了问题不可怕,关键是找到问题根源所在,解决问题,使同样的错误不再重复地发生。

打电话联系恒生的工程师,向他们提供系统的日志文件,希望他们那边能提供帮助,找到原因。我也在这边分析系统的事件和日志文件,想找出到底是在哪里出的问题。分析着从SQL Server的LOG目录下找到的几个ERRORLOG文件,发现从13:57开始有连续多条如下的警告语句出现:

1
SQL Server已为8个并发查询而优化。36个查询超过了此限制,因而性能可能会受到不良影响。

很快,恒生工程师也打电话来说可能是查询数据库过多导致的问题。这个我们一开始就想过,但关键是那时候只连接进来了400个左右的客户,怎么可能查询过多呢?而且平时也没出现过这种情况。

在网上搜索资料后知道出现这个问题通常是数据库版本的原因,检查我们的SQL Server数据库,发现竟然是2000个人版。后来又从交易结算部们了解到他们因为要做报表执行了查询所有客户资料的操作,并且我们的备份数据库在几个星期前曾因损坏做过恢复,恢复的同事忘记修改备份数据库的时间,导致本应该执行在备份数据库上的查询执行在了主数据库上,引起了数据库崩溃。

再次联系恒生的工程师,准备这个周末切换数据库到企业版。唉,又要加班了!

为什么使用Paperclip而不是Fleximage来实现图片上传

Rails有很多处理上传的插件:

FileColumn是最早的这类插件,比较容易使用。UploadColumn与之类似。ActiveUpload则需要SWFUpload配合使用。attachment_fu作为acts_as_attachment的进化版本非常强大,不过使用起来比较复杂。Fleximage是现在上传和处理图片的首选,据说连Paperclip也比不上它,不过使用下来的情况是使用简单,功能太弱。比如要把上传的图片放到以用户名为目录的文件夹中就实现不了,而且上传的图片名字也不能设定。Paperclip作为一个处理附件上传的插件,既有attachment_fu的功能强大,又有Fleximage的使用简单,相对于FileColumn在灵活性和效率上更胜一筹,而且代码也更优雅。

将用户名作为上传图片的目录,先要在config/initializers中创建一个paperclip.rb文件,定义一个username变量:

1
2
3
Paperclip.interpolates :username do |attachment, style|
  attachment.instance.user.login
end

上面的代码中attachment.instance就是指该附件实例,因为在avatar.rb中有如下语句:

1
belongs_to :user

所以该实例有一个User对象,User对象有个login字段,该字段就是我们需要的用户名。

如何为绩效考核做准备

绩效考核是有压力的,但它也是重新审视你当前的位置、你与你雇主的关系、以及你的职业目标的机会。通过问自己一些关键的问题,会让你的绩效考核更富有成效。你可以在考核之前思考以下的这几个方面:

1. 今年我做了什么?

首先回顾全年(或者自上次考核以后的任何时间段)的工作。如果记不太清楚的话可以检查旧的电子邮件和文件。逐月查看你的职责和成就,包括预期的和意外的,特别要留意那些超出你职责要求以外的任何事情。例如,当雇员人数减少时,你是否承担了更多的职责?你是否找到方法去减少与特定项目或流程相关的成本?还要注意任何没有达到预期的项目,以及任何你经历过的挑战。像发生了什么,以及在最终结果中你的角色是什么?做好这样的准备可以确保你在考核中被提问时不会措手不及,也将有助于奠定你与你主管进行实质性讨论的基础。

2. 我的职业目标和重点是什么?

花些时间去查找和反思上次考核的评价,并提出一些新的目标或需要改进的地方。如果有目标半途而废了,考虑它们是否仍然重要,或者是否新的目标现在更合适。在你的考核期间,要毫不犹豫地去询问你的雇主是否有能力帮助你实现这些目标,因为在组织致力于活下去的时候,许多有价值的职业发展意图会被搁置。同时也要询问是否有你想继续的技术或业务培训?大多数雇主都有兴趣帮助雇员保持他们的职业向前迈进。

3. 我应该要求加薪吗?

即使你认为加薪是当之无愧的,在谈论之前也要考虑到你雇主的财务状况,以及你雇主表达感谢你贡献的不同方式,如灵活调度、在家工作的选项或额外的好处。如果你确实要求加薪,请准备好你为公司节省了时间和金钱方面的具体证据。并对公司的状况,过去的加薪,以及你所在地区其它相同职位的薪酬水平和通货膨胀作个调查,随时准备引用这些数据。

4. 如果得到负面考核怎么办?

首先,切勿对批评小题大作。如果你习惯了赞美,那么一些改进的建议也会让你觉得像是严厉的批判。请记住,即使是最优秀的雇员也有需要改进的地方。你应该做的是与你的主管一起制定计划去解决出现的问题。防卫性地反应或情绪化的批评可能比问题绩效本身更具破坏性。如果你发现自己正试图把过失推卸到同事(或更糟的是,你的老板)身上,最好管住你的舌头。你可以在更客观地审核它之后要求另外的会议来进一步讨论这个问题。如果讨论时批评转变的令人惊讶,这表明你和你主管沟通的不够。建议定期会晤,以便让彼此更好地了解。

绩效考核应该是一次对话,而不是一个审判。带着考核的教训进入来年将有助于你保持你的日常重点和长期目标一致。