Railsでエラーページを表示する.

いつもpublic/404.htmlとか編集してる程度だったので、Railsの仕組みを使ったエラーページを作ろうかと色々試行錯誤してみた.


因にエラーページは通常はProductionモードでHTTPアクセスをローカルホスト( localhost / 127.0.0.1 )以外でアクセスしたときに確認できます.

エラーページの実装方法

方法はググってみると、ざっと3種類あった.

  • public/404.htmlを書き換える
  • ApplicationController内でrescue_fromを追記
  • ApplicationController内でrescue_action_in_publicメソッドをオーバライド


色々やるなら、最後のrescue_action_in_publicオーバライドする方法が一番よさそう. ただ欲を言うとRails自身もProductionモードのときちゃんと、404.htmlとか表示してるからその機能っぽくしたい.

Railsのコードを読んでみた.

ActionController::RescueにDEFAULT_RESCUE_RESPONSESなんてモノがあるじゃないか!コレを使ってRailsは例外からHTTPステータスコードに変換して404.htmlを表示してた.

    DEFAULT_RESCUE_RESPONSES = {
      'ActionController::RoutingError'             => :not_found,
      'ActionController::UnknownAction'            => :not_found,
      'ActiveRecord::RecordNotFound'               => :not_found,
      'ActiveRecord::StaleObjectError'             => :conflict,
      'ActiveRecord::RecordInvalid'                => :unprocessable_entity,
      'ActiveRecord::RecordNotSaved'               => :unprocessable_entity,
      'ActionController::MethodNotAllowed'         => :method_not_allowed,
      'ActionController::NotImplemented'           => :not_implemented,
      'ActionController::InvalidAuthenticityToken' => :unprocessable_entity
    }


ステータスコード自体はActionController::StatusCodes::STATUS_CODESで管理してた.

    STATUS_CODES = {
      :
      :
      200 => "OK",
      :
      404 => "Not Found",
      :
      500 => "Internal Server Error",
      :
      :
    }

例外毎にHTTPステータスを割り当てて表示する方法.

Railsのコードを読んで大体なにしてるか分かったので、404.htmlの用に例外からステータスコードを判定して表示するコードを書いてみた.


renderでレイアウトとか色々してるけどこの辺は普通のコントローラと書き方は同じ.

class ApplicationController < ActionController::Base

  # 例外とステータスの関連付け
  rescue_responses.update({
    'Foo::BarError' => :not_found
  })

  protected

    # 確認をhttp://localhost 等で確認するときはコメントアウト
    # が定石
    #def local_request?
    #  false
    #end
  
    def rescue_action_in_public(exception) #:doc:
      status_message_sym = response_code_for_rescue(exception) || :internal_server_error
      @http_status_code = SYMBOL_TO_STATUS_CODE[status_message_sym]
      
      respond_to do |format|
        format.html { render :template => "commons/error.html", :layout => 'application', :status => @http_status_code }
      end
    end


end

テンプレートをRAILS_ROOT/views/commons/error.html.erbに作成して、Productionモードで確認.

<%
title = "#{@http_status_code} #{ActionController::StatusCodes::STATUS_CODES[@http_status_code]}"
-%>
<h1><%= title %></h1>

<p>
<% if @http_status_code == 403 # Forbidden -%>
アクセス権限がありません.
<% elsif @http_status_code == 404 # Not Found -%>
お探しのページが見つかりませんでした.
<% elsif @http_status_code == 422 # Unprocessable Entity -%>
指定されたエンティティの処理ができませんでした.
<% elsif @http_status_code == 500 # Internal Server Error -%>
システムに障害が発生しました.
<% end -%>
</p>

[Java]JTableの選択状態をトグルにする

普通、テーブルの行選択にCtrlを押しながらクリックすると選択をトグル式に選択したり選択を外したりできる.
普段PCのUIに慣れている人は当たり前だけどCtrlを押しながらクリックは知らない人には使いにくい(っという人がいる)

個人的にはそれくらい覚えといて良いだろうと思うけど...

そこでJTableでそのような挙動をさせるためにはchangeSelectionしてトグルを常にtrueにすればできたー.

new JTable(){
  @Override
  public void changeSelection(int rowIndex, int columnIndex, boolean toggle, boolean extend) {
    super.changeSelection(rowIndex, columnIndex, true, extend);
  }
}


これは以下のクラスのメソッドからコールされていました.

javax.swing.plaf.basic.BasicTableUI$Handler.mousePressed

[Git][Mac] MacでGitのサブコマンドのタブ補完をする方法

MacPortsでgitとbash-completionをインストール

$ sudo port install git-core
$ sudo port install bash-completion

.bashrcを修正

if [ -f /opt/local/etc/bash_completion ]; then
. /opt/local/etc/bash_completion
fi

if [ -f /opt/local/share/doc/git-core/contrib/completion/git-completion.bash ]; then
. /opt/local/share/doc/git-core/contrib/completion/git-completion.bash
fi

これでサブコマンドが補完されると思います.

ARで論理削除する為のモジュールを作ってみた.

Railsで論理削除が必要になったのでとりあえず調べてみた.希望としては

  • プラグインに頼らず手軽に使いたい
  • ARの機能そのままで使いたい

調べてみると以下の二つがでてきた.

acts_as_paranoidがググってみると結構多いが別に高機能を求めてる訳じゃないのでこれは使いたくない、っでRails作るときに参考にしているskipのソースを読むとモジュールにしているのがあった.

こっちは手軽で便利だけど論理削除を実装で意識する必要が強いのでこれを参考にオレオレモジュールを作ってみた.

論理削除オレオレモジュール

module Models
  #
  # 論理削除するようにします.
  #
  # 論理削除にはdeleted_atカラムを使用します.マイグレーションにdatetime型のカラムを追加してください.
  # 削除の判定にはdeleted_atがnullでない場合は削除済みとして扱われます.
  #
  # # find等のメソッドから削除されたレコードを取得しないようにする例
  # class Member < ActiveRecord::Base
  #   include Models::LogicalDestroyable
  #   default_scope :conditions => {:deleted_at => nil}
  # end
  #
  # # default_scopeを指定する場合
  # class Member < ActiveRecord::Base
  #   include Models::LogicalDestroyable
  #   hide_destroyed_with_default_scope
  # end
  #
  module LogicalDestroyable
    def self.included(base)
      base.alias_method_chain :destroy, :logical_destroyable
      base.named_scope(:active, {:conditions => {:deleted_at => nil}})
      base.attr_protected :deleted_at
    end
    
    #
    # default_scopeを使って削除されたレコードを検索結果から隠します
    #
    # findメソッドなどで削除項目も対象にしたい場合はwith_exclusive_scopeを使ってください.
    # 
    def self.hide_destroyed_with_default_scope
      # create時にも初期値として使われるけど大丈夫かな?
      self.default_scope :conditions => {:deleted_at => nil}
      #default_scopeは最後のdefault_scopingの値しか見ないので使えないっぽいからdefault_scopeを使うときは都度指定する必要がある.
      #options = {:conditions => {:deleted_at => nil}}
      #base.default_scoping.insert 0, { :find => options, :create => options[:conditions] }
    end
    
    def destroy_with_logical_destroyable
      before_destroy_with_logical_destroyable
      current_time = current_time_from_proper_timezone
      ret=update_attribute(:deleted_at, current_time)
      after_destroy_with_logical_destroyable
      ret
    end

    def before_destroy_with_logical_destroyable;end
    def after_destroy_with_logical_destroyable;end
    
    def destroyed?
      !self.deleted_at.nil?
    end
    alias_method :deleted?, :destroyed?

    def recover
      update_attribute(:deleted_at, nil)
    end
  end
end

あとがき

とりあえずモジュール作ってみたけどdeleteとdestroyって似てるけど内部の処理が違うんだよなー.

  • delete
    • DELETE文(SQL)で消す
  • destroy
    • ARオブジェクトを一旦作ってから削除
    • before_destroyコールバックで関連付けされてるやつも消したりするように成ってる.

関連づけ先が勝手に消されないようにbefore_destroyコールバックとは別のコールバックするようにしたけど.
deleteをオーバライドして関連まで影響ない仕様をそのまま論理削除に置き換えて作れば良かったかも.
物理削除の場合はdestroyするようにすればARの仕様を大きく変えないしなー.

Rails2.3でerror_message_onのi18n対応

なぜかRails2.3のerror_message_onはi18n対応されてないのでしてみました.

対応前 before

error_message_on(:user, :name)
=> "を入力してください。"

対応後 after

error_message_on(:user, :name)
=> "名前を入力してください。"

コード

プラグインなり、初期化時なり適当な(rails起動時に一度だけ実行する)ところで読み込ませてください.

module ActionView
  module Helpers
    module ActiveRecordHelper
      
      # エラーメッセージの翻訳
      alias_method :error_message_on_without_translate, :error_message_on
      def error_message_on(object, method, *args )
        obj = instance_variable_get("@#{object}")
        options = args.extract_options!
        options.reverse_merge!(:prepend_text => obj.class.human_attribute_name(method.to_s))
        
        if args.empty?
          args = options
        else  
          args.push options
        end
        error_message_on_without_translate(object, method, args)
      end
    end
  end
end

jpmobileでsession_keyが空のときがある気がする.

自分の環境だけかもしれないけど、request.session_options[:key]がnilの時があったのでとりあえず、対応してみた.

元の実装よりも判定を増やした程度なのでこのままでもいい気がするけど様子見.

Index: trans_sid.rb
===================================================================
--- trans_sid.rb        (リビジョン old)
+++ trans_sid.rb        (リビジョン new)
@@ -87,7 +87,9 @@
   private
   # session_keyを返す。
   def session_key
-    (request.session_options || ActionController::Base.session_options)[:key]
+    opts1 = request.session_options
+    opts2 = ActionController::Base.session_options
+    (opts1 && opts1[:key]) || (opts2 && opts2[:key])
   end
   # session_idを返す
   def jpmobile_session_id

RailsでプロジェクトのSVNバージョンを表示する

今作ってるシステムでSVNのバージョンを表示させようと思いって書いてみました.

config/environment.rbを編集

config/environment.rbに以下を追加

APP_CONFIG = {}
APP_CONFIG[:admin_email] = "MyApp <admin@example.com>"
APP_CONFIG[:site_name]   = "MyApp"
APP_CONFIG[:major_version] = "0"
APP_CONFIG[:minor_version] = "1"
APP_CONFIG[:revision]      = nil

# バージョンを拾ってこれれば拾ってくる.
begin
  require "rexml/document"
  src = `svn info --xml #{RAILS_ROOT}`
  unless src.nil? || src.empty?
    doc = REXML::Document.new src
    APP_CONFIG[:revision] = doc.elements['/info/entry'].attributes['revision']
  end
rescue
end
APP_CONFIG[:version] = "#{APP_CONFIG[:major_version]}.#{APP_CONFIG[:minor_version]}"
APP_CONFIG[:version] += ".#{APP_CONFIG[:revision]}" unless APP_CONFIG[:revision].nil?
APP_CONFIG[:full_version] = "#{APP_CONFIG[:site_name]} ver.#{APP_CONFIG[:version]}"

viewを編集

viewのバージョンを表示したい場所に以下を追加

<%= APP_CONFIG[:full_version] -%>

Railsを起動して確認

っで、以下の様に表示されます

MyApp ver.0.1.281

処理内容

  1. Railsを起動
    1. svnコマンドを叩いてローカルリポジトリの情報を取得
    2. リポジトリ情報をパースしてrevisionを取得

リポジトリのrevesionが取得できない場合はrevesion情報は表示しません.
例外処理もしているのでこれが原因でアプリのエラーならないようにしてあります.