Archive

Posts Tagged ‘ruby’

把AJAX操作 从controller中移到helper

September 3rd, 2010 yakjuly 2 comments

rails中的ajax操作结合prototype,非常方便。方法也挺简单。

我平常操作ajax的代码喜欢在controller中使用 render :update {|page| … }

但是这样的代码在controller写多了 会显得非常肮脏 难读。并且,我要对页面进行某个操作,需要在controller中写页面中的dom元素的id等。

例如:

render :update do |page|
  page.replace_html("xxxx", :partial => "xxx")
end

render :update do |page|
   #use alert
   page.alert(@object.errors.full_messages.join(","))
   #or replace notice area
   page.replace_html("error_div", @object.errors.full_messages.join(","))
end

当一次ajax访问需要 对页面调用多个动作时,代码就显得乱了。利用rails的helper,我们可以将render:update的block中的内容放到helper中,这为我们复用和重构提供了一个有效的工具。 例如:

module ApplicationHelper

  def update_time
    page.alert 'hello'
  end

  def spike
    render(:update){|page| page.alert_hello}
  end
end

另外,把代码移到helper中也可以让这些ajax操作在controller中 显得更有意义。

controller:

class User::CommentsController < User::BaseController
  before_filter :prepare_comment

  def index

  end

  def new

    respond_to do |type|
      type.js {
        render :update do |page|
          page.create_comment_form
        end
      }
    end
  end

  def create
    success = @comment.save

    respond_to do |type|
      type.js {
        render :update do |page|
          if success
            page.clear_comment_form
            refresh_comment_list(page)
          else
            page.show_error_messages(@comment)
          end
        end
      }
    end
  end

  def edit
    respond_to do |type|
      type.js {
        render :update do |page|
          page.create_comment_form
        end
      }
    end
  end

  def update
    success = @comment.save
    respond_to do |type|
      type.js {
        render :update do |page|
          if success
            page.clear_comment_form
            refresh_comment_list(page)
          else
            page.show_error_messages(@comment)
          end
        end
      }
    end
  end

  def destroy
    @comment.destroy
    render :nothing => true
  end

  private

  def prepare_comment
    case action_name
    when "new", "create"
      @comment = @user.comments.build(params[:comment])
    when "edit", "update", "destroy"
      @comment = @user.comments.find(params[:id])
      @comment.attributes=(params[:comment])
    when "index"
      @comments = @user.comments
    end
  end

end

接下来 helper

module User::CommentHelper
  def refresh_comment_list(page)
    @comments = @user.comments
    page.replace_html("comment_list", :partial => "list")
  end

  def clear_comment_form
    page.replace_html("edit_comment", "")
  end

  def create_comment_form
    page.replace_html("edit_comment", :partial => "form")
  end

  def comment_form_head
    case params[:action]
    when "new", "create"
      [@comment, {:url => user_comments_path}]
    when "edit", "update"
      [@comment, {:url => user_comment_path(@user, @comment), :html => {:method => :put}}]
    end
  end
end

这里有个缺憾 就是 refresh_comment_list方法 不能直接写成page.refresh_comment_list

因为在javascript generator中不能读到 @user 这个实例变量。因此只有写成refresh_comment_list(page). 并且helper方法中包含了数据库查询语句。

虽然美中不足,但代码看起来还是舒服多了。

view中还可以重用helper中的部分方法

<% remote_form_for *comment_form_head do |f| %>
<div class="order" style="clear:left">
  <table>
    ........
    <tr><th></th><td><%= submit_tag(t(:submit)) %> <%= link_to_function(t(:cancel), update_page{|page| page.clear_comment_form}) %></td></tr>
  </table>
</div>
<% end %>

经过这样改写后,controller中不用关注 dom对象如何变化,helper与页面中的dom对象关系更加紧密。

Categories: rails, ruby Tags: ,

rails中的select和include查询

August 2nd, 2010 yakjuly No comments

Rails中ActiveRecord finders不允许我们在include预加载关联对象时使用select。

例如这样的情况:

class Student < ActiveRecord::Base
   has_many :calls
   has_one :call
end

class Call < ActiveRecord::Base
   belongs_to :student
end

希望在显示student时,显示它的最近更新的call的日期。

改善前:

@students = Student.all(:include => [:call])

改善后:

#include eql left outer join
@students = Student.all(:select => "students.id, students.login, students.called_times,
calls.updated_at as last_called_at", :joins => "left outer join calls on students.id = calls.student_id")
Categories: rails, ruby Tags: ,

rails3 !!! Missing the mysql gem. Add it to your Gemfile: gem ‘mysql’, ’2.8.1′

July 16th, 2010 yakjuly No comments

Rails3中 rake db:create 或 rake db:migrate 时出错

rake aborted!
!!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql', '2.8.1'
/usr/lib/ruby/gems/1.8/gems/activerecord-3.0.0.beta4/lib/active_record/connection_adapters/mysql_adapter.rb:22:in `mysql_connection'

原因:rails3中使用bundler 管理gem,尽管system中包含gem mysql 仍然报错。

解决:找到rails_app_path下 的 GemFile。添加 gem ‘mysql’

更多:Rails3:使用bundler管理gems

Categories: rails, ruby Tags: ,

named_scope的扩展命名用法

June 6th, 2010 yakjuly No comments

Nick Kallen颇受欢迎的has_finder插件以named_scope的方式集成到了Rails 2.3版本。

平常我们在使用named_scope时,对于一些sql语句无法表达的集合筛选。总是需要进行select处理。代码读起来就不那么优雅。

named_scope + block 则解决了这个问题。

 class User < ActiveRecord::Base
   has_many :stories

   named_scope :inactive, :conditions => {:active => false} do
      def latest(number = 1, role = nil)
        collection = role.blank? ? self : self.find_all_by_role(role)
        collection[0, number]
      end
   end  

 end  

 # Re-activate all inactive users
 User.inactive.latest(5,'admin')

rails中不仅named_scope可以这样写,has_many等关联方法 也可以 带block.例如:

 class User < ActiveRecord::Base
   has_many :stories do
     def latest_public
       self.select{|story| story.public? }.first
     end
   end
 end

这两个例子中block内的self 都是代表当前集合,所以self其实是对集合做操作。

Categories: rails, ruby Tags: ,

自定义generators

May 27th, 2010 yakjuly No comments

相信每个人都用过 ruby script/generate model xxx.创建自己的generator 可以将一些简单的可重用的代码自动添加到项目代码中。

先看一下原理

建议先下载 restful_authentication 参考

Rails在以下地方查找用户自定义的Generators:
RAILS_ROOT/lib/generators
RAILS_ROOT/vendor/generators
RAILS_ROOT/vendor/plugins/any_subdirectory/generators
~/.rails/generators
以及以_generator为后缀的Gems
我们看看一个例子代码:

class TumblepostGenerator < Rails::Generator::NamedBase
  def manifest
    record do |m|
      m.class_collisions class_name
      m.template  "app/controllers/controller_template.rb",
                  "app/controllers/#{file_name}_controller.rb"
      m.template  "app/models/model_template.rb",
                  "app/models/#{file_name}.rb"
      m.directory File.join('app/views', file_name)
      m.template  "app/views/form_template.rhtml",
                 "app/views/#{file_name}/_form.rhtml"
      m.template  "app/views/view_template.rhtml",
                 "app/views/#{file_name}/_view.rhtml"  

      m.readme "POST_GENERATION_REMINDER"
    end
  end
end

其中Rails::Generator::NamedBase是ruby script/generator后面带参数的,而Rails::Generator::Base是不带参数的
生成的文件会先用ERB解析,这样我们可以自己构建生成文件的模板,如:

class <%= class_name %>Controller < TumblepostController
   def new
     @thing = <%= class_name %>.new
   end
end

如果我们要生成数据库Migration,我们可以这样写:

m.migration_template "db/migrations/migration_template.rb", "db/migrate"

今天我要做的是想把文件上传这一类型做成一个plugin,只要每个项目 generate一下 就能轻松使用。

文件结构如图:

restful_attached_file_struct
代码如下:

rails_commands.rb(直接抄restful_authentication)

Rails::Generator::Commands::Create.class_eval do
  def route_resource(*resources)
    resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
    sentinel = 'ActionController::Routing::Routes.draw do |map|'

    logger.route "map.resource #{resource_list}"
    unless options[:pretend]
      gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
        "#{match}\n  map.resource #{resource_list}\n"
      end
    end
  end
end

Rails::Generator::Commands::Destroy.class_eval do
  def route_resource(*resources)
    resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
    look_for = "\n  map.resource #{resource_list}\n"
    logger.route "map.resource #{resource_list}"
    gsub_file 'config/routes.rb', /(#{look_for})/mi, ''
  end
end

Rails::Generator::Commands::List.class_eval do
  def route_resource(*resources)
    resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
    logger.route "map.resource #{resource_list}"
  end
end

attached_generator.rb

require 'rails_commands'
#generate attached attachment
class AttachedGenerator < Rails::Generator::NamedBase
  default_options :skip_migration => false

  attr_reader   :model_controller_name,
                :model_controller_class_path,
                :model_controller_file_path,
                :model_controller_class_nesting,
                :model_controller_class_nesting_depth,
                :model_controller_class_name,
                :model_controller_singular_name,
                :model_controller_plural_name
  alias_method  :model_controller_file_name,  :model_controller_singular_name
  alias_method  :model_controller_table_name, :model_controller_plural_name

  def initialize(runtime_args, runtime_options = {})
    super
    @model_controller_name = @name.pluralize
    #@class_name = @name.camelize
    # model controller
    base_name, @model_controller_class_path, @model_controller_file_path, @model_controller_class_nesting, @model_controller_class_nesting_depth = extract_modules(@model_controller_name)
    @model_controller_class_name_without_nesting, @model_controller_singular_name, @model_controller_plural_name = inflect_names(base_name)

    if @model_controller_class_nesting.empty?
      @model_controller_class_name = @model_controller_class_name_without_nesting
    else
      @model_controller_class_name = "#{@model_controller_class_nesting}::#{@model_controller_class_name_without_nesting}"
    end

    p base_name
    self.instance_variables.sort.each do |var|
      method = var.gsub(/@/,"")
      p var => self.send(method) if self.respond_to?( method )
    end
    p "*" * 80
  end

  def manifest
    recorded_session = record do |m|
      # Check for class naming collisions.
      m.class_collisions model_controller_class_path, "#{model_controller_class_name}Controller", # Model Controller
                                                      "#{model_controller_class_name}Helper"

      m.class_collisions [], 'AttachedSystem'

      # Controller, helper, views, and test directories.
      m.directory File.join('app/models', class_path)
      m.directory File.join('app/controllers', model_controller_class_path)
      m.directory File.join('app/helpers', model_controller_class_path)
      m.directory File.join('app/views', model_controller_class_path, model_controller_file_name)

      p '1' * 70
      m.template 'model.rb',
                  File.join('app/models',
                            class_path,
                            "#{file_name}.rb")
      m.template 'main_image.rb',
                  File.join('app/models',
                            class_path,
                            "main_image.rb")
      m.template 'sub_image.rb',
                  File.join('app/models',
                            class_path,
                            "sub_image.rb")
      p '2' * 70

      m.template 'model_controller.rb',
                  File.join('app/controllers',
                            model_controller_class_path,
                            "#{model_controller_file_name}_controller.rb")
      p '3' * 70
      m.template 'attached_system.rb',
                  File.join('lib', 'attached_system.rb')

      p '4' * 70

      m.template 'model_helper.rb',
                  File.join('app/helpers',
                            model_controller_class_path,
                            "#{model_controller_file_name}_helper.rb")

      p '5' * 70

      # Controller templates
      #m.template 'show.html.erb',  File.join('app/views', controller_class_path, controller_file_name, "new.html.erb")
      #m.template 'signup.html.erb', File.join('app/views', model_controller_class_path, model_controller_file_name, "new.html.erb")

      p '6' * 70

      unless options[:skip_migration]
        m.migration_template 'migration.rb', 'db/migrate', :assigns => {
          :migration_name => "Create#{class_name.pluralize.gsub(/::/, '')}"
        }, :migration_file_name => "create_#{file_path.gsub(/\//, '_').pluralize}"
      end

      p '7' * 70
      m.route_resources model_controller_plural_name

      p '8' * 70
    end

    action = nil
    action = $0.split("/")[1]
    case action
      when "generate"
        puts
        puts ("-" * 70)
        puts "Don't forget to:"
      when "destroy"
        puts
        puts ("-" * 70)
        puts
        puts "Thanks for using restful_authentication"
        puts
        puts "Don't forget to comment out the observer line in environment.rb"
        puts "  (This was optional so it may not even be there)"
        puts "  # config.active_record.observers = :#{file_name}_observer"
        puts
        puts ("-" * 70)
        puts
      else
        puts
    end

    recorded_session
  end

  protected
    # Override with your own usage banner.
    def banner
      "Usage: #{$0} attached  [ModelName]"
    end

    def add_options!(opt)
      opt.separator ''
      opt.separator 'Options:'
      opt.on("--skip-migration",
             "Don't generate a migration file for this model") { |v| options[:skip_migration] = v }
      opt.on("--rspec",
             "Force rspec mode (checks for RAILS_ROOT/spec by default)") { |v| options[:rspec] = true }
    end
end

完成之后就可以使用命令ruby script/generate attached attachment –backtrace 来调用了。

运行结果如下:

D:\app\suitablehouse>ruby script/generate attached attachment --backtrace --skip
-migration
"attachments"
{"@args"=>[]}
{"@class_name"=>"Attachment"}
{"@class_nesting"=>""}
{"@class_nesting_depth"=>0}
{"@class_path"=>[]}
{"@destination_root"=>"D:/app/suitablehouse"}
{"@file_path"=>"attachment"}
{"@model_controller_class_name"=>"Attachments"}
{"@model_controller_class_nesting"=>""}
{"@model_controller_class_nesting_depth"=>0}
{"@model_controller_class_path"=>[]}
{"@model_controller_file_path"=>"attachments"}
{"@model_controller_name"=>"attachments"}
{"@model_controller_plural_name"=>"attachments"}
{"@model_controller_singular_name"=>"attachments"}
{"@name"=>"attachment"}
{"@options"=>{:collision=>:ask, :skip_migration=>true, :quiet=>false, :generator
=>"attached", :command=>:create, :backtrace=>true}}
{"@plural_name"=>"attachments"}
{"@singular_name"=>"attachment"}
{"@source_root"=>"D:/app/suitablehouse/vendor/plugins/restful_attachment/generat
ors/attached/templates"}
{"@table_name"=>"attachments"}
"*******************************************************************************
*"
"1111111111111111111111111111111111111111111111111111111111111111111111"
"2222222222222222222222222222222222222222222222222222222222222222222222"
"3333333333333333333333333333333333333333333333333333333333333333333333"
"4444444444444444444444444444444444444444444444444444444444444444444444"
"5555555555555555555555555555555555555555555555555555555555555555555555"
"6666666666666666666666666666666666666666666666666666666666666666666666"
"7777777777777777777777777777777777777777777777777777777777777777777777"
"8888888888888888888888888888888888888888888888888888888888888888888888"

----------------------------------------------------------------------
Don't forget to:
      exists  app/models/
      exists  app/controllers/
      exists  app/helpers/
      exists  app/views/attachments
   identical  app/models/attachment.rb
   identical  app/models/main_image.rb
   identical  app/models/sub_image.rb
   identical  app/controllers/attachments_controller.rb
   identical  lib/attached_system.rb
   identical  app/helpers/attachments_helper.rb
       route  map.resources :attachments

可以看得出来,里面有很多debug的代码,也有一些没有用的代码没有去掉。不过这都是因为我完全没写过generator的缘故。

Rails在以下地方查找用户自定义的Generators:
RAILS_ROOT/lib/generators
RAILS_ROOT/vendor/generators
RAILS_ROOT/vendor/plugins/any_subdirectory/generators
~/.rails/generators
以及以_generator为后缀的Gems
我们看看一个例子代码:

Java代码
  1. class TumblepostGenerator < Rails::Generator::NamedBase
  2. def manifest
  3. record do |m|
  4. m.class_collisions class_name
  5. m.template  “app/controllers/controller_template.rb”,
  6. “app/controllers/#{file_name}_controller.rb”
  7. m.template  “app/models/model_template.rb”,
  8. “app/models/#{file_name}.rb”
  9. m.directory File.join(‘app/views’, file_name)
  10. m.template  “app/views/form_template.rhtml”,
  11. “app/views/#{file_name}/_form.rhtml”
  12. m.template  “app/views/view_template.rhtml”,
  13. “app/views/#{file_name}/_view.rhtml”
  14. m.readme “POST_GENERATION_REMINDER”
  15. end
  16. end
  17. end

其中Rails::Generator::NamedBase是ruby script/generator后面带参数的,而Rails::Generator::Base是不带参数的
生成的文件会先用ERB解析,这样我们可以自己构建生成文件的模板,如:

Java代码
  1. class <%= class_name %>Controller < TumblepostController
  2. def new
  3. @thing = <%= class_name %>.new
  4. end

如果我们要生成数据库Migration,我们可以这样写:

Java代码
  1. m.migration_template “db/migrations/migration_template.rb”, “db/migrate”

事实上已经有很多Generators创建好并以gems部署了,让我搜索一下:

Java代码
  1. gem search -r generator
Categories: rails, ruby Tags: ,