restful_authentication + ruby-gettext + form_forで権限管理付きログインページの雛形を作る(日本語版)

restful_authenticationに権限(role)管理を追加して、実用的に利用するカスタマイズ例として、Restful Authentication with all the bells and whistlesを参考にいろいろ考えてみた。コードを読みながら、次第に以下のような疑問や考えが浮かんできた。

  • メール送信処理のきっかけに、なぜobserverを利用するだろうか?
    • observerはコールバック(before_createとかafter_save等)を外部のオブザーバーオブジェクトで処理するようなもの。
      • モデル自身のコールバックとして処理しても良いはず。
    • restful_authenticationの実装を見ると、コールバックの中で条件によって処理を分岐するために、モデルの状態をインスタンス変数にわざわざ保存している。
      • 例:@activated = true、@forgotten_password = true、@reset_password= true
      • それぞれのメール送信のタイミングは、DB保存後の数ある条件のうち、たった一つのタイミングに限定されている。
      • それなら、インスタンス変数に状態を保存するタイミングで、メールを送信してしまえば、もっとシンプルになるのでは?
  • 結構コントローラーが一生懸命仕事をしている。もっとモデルに働いてもらうようにする。
    • Change passwordアクションとか、Reset passwordアクションの部分。
  • コントローラーの命名をより分かり易いダイレクトな名前にしてしまう。

イベント 以前のコントローラー名前 新しいコントローラー名前
アクティベーションを有効にする accountsコントローラー activationsコントローラー
パスワード忘れ? accountsコントローラー forgot_passwordsコントローラー
パスワード変更 passwordsコントローラー change_passwordsコントローラー

  • ruby-gettextによる日本語化をしておきたい。
    • メッセージを直接日本語に置き換えてしまっても良いが、gettextの _("message") メドッソで囲っておくことで、後でメッセージの修正や調整する時に幸せになる。
    • エラーメッセージもより柔軟に日本語化できる。%記法がとても便利。(http://www.yotabanana.com/hiki/ja/ruby-gettext-howto-ror.html
%{fn}、%{d}、%{val}、_("This is %{name}") % {:name=>@user.name}

以上のようなことを考えながら、自分なりに修正してみた。

form_for関連等のヘルパメソッドの拡張

  • 独自のform_builderを定義した。
# ---------- lib/base_form_builder.rb ----------
class BaseFormBuilder < ActionView::Helpers::FormBuilder
  class_inheritable_accessor :form_helpers
  self.form_helpers = ActionView::Helpers::FormHelper.instance_methods + 
                        ActionView::Helpers::FormOptionsHelper.instance_methods
  # 上記設定は以下と同等、参考ページ<http://rubyist.g.hatena.ne.jp/yamaz/20070107>
  #  def self.form_helpers
  #    @@form_helpers = ActionView::Helpers::FormHelper.instance_methods + 
  #                       ActionView::Helpers::FormOptionsHelper.instance_methods
  #  end

private

  # 以下のオプションをマージする
  #   args(f.text_field等のオプション)
  #   @options(form_for,fields_forのオプション)の中の独自オプション
  def merge_options_with(args)
    #args_hash = args.last.is_a?(Hash) ? args.pop : {}
    args_hash = args.extract_options!
    args << form_options.merge(args_hash)
  end

  # フォームに設定する独自オプションだけ取り出す
  def form_options
    _options = @options.dup
    [:url, :html, :builder].each {|key| _options.delete(key)}
    _options
  end

  # オブジェクト名からクラスを取得する
  def object_class
    Object.const_get(@object_name.to_s.classify)
  end
end
  • 再利用しそうなメソッドをbase_form_builder(上記)に定義して、実際のフォームの定義はbase_form_builderを継承したlabel_msg_form_builder(下記)で設定した。
# ---------- lib/label_msg_form_builder.rb ----------
class LabelMsgFormBuilder < BaseFormBuilder
  # <% form_for @slip do |f| %>
  #   <p>
  #     <%= f.label :number %>
  #     <%= f.text_field :number %>
  #     <%= f.error_messages_on :number %>
  #   </p>
  # <% end %>
  #
  # :builder=>LabelMsgFormBuilderオプションを利用すると以下のように書ける
  #
  # <% form_for @slip, :builder=>LabelMsgFormBuilder do |f| %>
  #   <%= f.text_field :number %>
  # <% end %>
  (form_helpers - %w(label form_for field_for hidden_field error_messages_on)).each do |selector|
    define_method(selector) do |field, *args|
      args_hash = args.extract_options!
      label = args_hash.delete(:label)
      args << args_hash
      @template.content_tag('p', 
        @template.label(@object_name, field, label, :style=>"display:table") + #'<br/>' +
        super(field, *merge_options_with(args)) + 
        @template.error_messages_on(@object_name, field))
    end
  end

  # form_forのoptionとは無関係
  def submit(value = "Save changes", options = {})
    @template.content_tag('p', super)
  end

  # form_forのoptionと共用する場合 
#  def submit(value = "Save changes", *args)
#    @template.content_tag('p', super(value, *merge_options_with(args)))
#  end



  # hidden_fieldだけ特例扱い、以下理由
  #   見えないフィールドにtdタグは設定したくない
  #   form_for等で設定した独自オプション(:index等)だけは有効にしたい
  def hidden_field(field, *args)
    super(field, *merge_options_with(args))
  end
end
  • 共通のヘルパーメソッドapplication_helperを拡張
# ---------- app/helpers/application_helper.rb ----------
# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
  # LabelMsgFormBuilderを利用するform_forヘルパー
  #
  #   form_for @todo, :builder=>LabelMsgFormBuilder do |f|
  #     f.text_field :body
  #   end
  #
  #   simple_form_forを利用すれば:builderオプションは省略できる
  #
  #   simple_form_for @todo do |f|
  #     f.text_field :body
  #   end
  def simple_form_for(record_or_name_or_array, *args, &block)
    #options = args.last.is_a?(Hash) ? args.pop : {}
    options = args.extract_options!
    args << options.merge(:builder=>LabelMsgFormBuilder)

    form_for(record_or_name_or_array, *args, &block)
  end

  # simple_form_forを<fieldset>と<legend>でラップする
  def label_msg_form_for(record_or_name_or_array, *args, &block)
    #options = args.last.is_a?(Hash) ? args.pop : {}
    options = args.extract_options!
    args << options.merge(:builder=>LabelMsgFormBuilder)

    name = case record_or_name_or_array
           when String, Symbol
             _(record_or_name_or_array)
           else
             _(ActionController::RecordIdentifier.singular_class_name(record_or_name_or_array.to_a.last))
           end

    concat('<fieldset>', block.binding)
    concat("<legend>#{name}</legend>", block.binding)
    form_for(record_or_name_or_array, *args, &block)
    concat('</fieldset>', block.binding)
  end
end

# FormHelperの拡張
module ActionView
  module Helpers
    module FormHelper
      # FormBuilderに合わせて、内容は同じだが念のためオーバーライド
      def label(object_name, method, text = nil, options = {})
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_label_tag(text, options)
      end

      # 円数値入力用のテキストフィールドを生成
      # 右寄せ、3桁区切り[   1,000]、オートコンプリートオフ
      def yen_field(object_name, method, options = {})
        # object_nameに基づくオブジェクト(モデルのインスタンス)から、methodが示すフィールドの値を取得している。
        # 例: yen_field 'slip', 'total_yen' --> @slip.total_yenがvalueに設定される。
        object = options[:object] || self.instance_variable_get("@#{object_name}")
        value = object.send(method)
        # デフォルトのオプション設定
        options.merge!(:value=>number_with_delimiter(value), 
                       :autocomplete=>'off', 
                       :style=>"text-align:right")
        InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("text", options)
      end

      # error_message_onの複数形版
      # 指定したフィールドのすべてのエラーを返す
      def error_messages_on(object, method, prepend_text = "", append_text = "", css_class = "formErrorMsg")
        if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) && (errors = obj.errors.on(method))
          errors_list = errors.map {|error| "<li>#{prepend_text}#{error}#{append_text}</li>"}.join
          content_tag('ul', errors_list, :class => css_class, :style=>"padding:0;margin-top:0;")
        else 
          ''
        end
      end
    end

    class InstanceTag #:nodoc:
      # ruby-gettextで翻訳するため修正
      def to_label_tag(text = nil, options = {})
        name_and_id = options.dup
        add_default_name_and_id(name_and_id)
        options["for"] = name_and_id["id"]
        #content = (text.blank? ? nil : text.to_s) || method_name.humanize
        content = (text.blank? ? nil : text.to_s) || object_class.human_attribute_name(method_name)
        content_tag("label", content, options)
      end

      # オブジェクト名からクラスを取得する
      def object_class
        Object.const_get(@object_name.to_s.classify)
      end
    end
    
    # form_for, fields_forブロック内のメソッド定義
    class FormBuilder
      # f.labl
      # 内容は同じだが、ここでオーバーライドしておかないと翻訳されなかった
      def label(method, text = nil, options = {})
        @template.label(@object_name, method, text, objectify_options(options))
      end

      # f.yen_field
      def yen_field(method, options = {})
        @template.yen_field(@object_name, method, options.merge(:object => @object))
      end

      # f.error_messages_on
      def error_messages_on(method, prepend_text = "", append_text = "", css_class = "formErrorMsg")
        @template.error_messages_on(@object, method, prepend_text, append_text, css_class)
      end
    end
  end
end

# visual_effectの:startcolorオプションでエラー対策
# http://d.hatena.ne.jp/zariganitosh/20080123/1201163615#visual_effect_startcolor
module ActionView
  module Helpers
    module ScriptaculousHelper
      def visual_effect(name, element_id = false, js_options = {})
        element = element_id ? element_id.to_json : "element"
        
        js_options[:queue] = 
          if js_options[:queue].is_a?(Hash)
            '{' + js_options[:queue].map {|k, v| k == :limit ? "#{k}:#{v}" : "#{k}:'#{v}'" }.join(',') + '}'
          elsif js_options[:queue]
            "'#{js_options[:queue]}'"
          end if js_options[:queue]
        
        [:endcolor, :direction, :startcolor, :scaleMode, :restorecolor].each do |option|
          js_options[option] = "'#{js_options[option]}'" if js_options[option] && !(/\A(['"]).+\1\z/ =~ js_options[option])
        end

        if TOGGLE_EFFECTS.include? name.to_sym
          "Effect.toggle(#{element},'#{name.to_s.gsub(/^toggle_/,'')}',#{options_for_javascript(js_options)});"
        else
          "new Effect.#{name.to_s.camelize}(#{element},#{options_for_javascript(js_options)});"
        end
      end
    end
  end
end

# nil.to_s(:simple)を利用可能にする
class NilClass
  def to_s(*args)
    ""
  end
end

restful_authenticationの修正

scaffoldやコントローラーの追加
  • 権限の種類をroleに保存して、userとroleはpermissionを経由して多:多の関係を持つ。
  • activations、forgot_passwords、change_passwordsは、RESTな規約を守るためにuserコントローラーから機能を分離した。対応するモデルはuserになる。
# ---------- ターミナルでの操作 ----------
$ script/generate scaffold role name:string
$ script/generate scaffold permission user_id:integer role_id:integer
$ script/generate controller activations
$ script/generate controller forgot_passwords
$ script/generate controller change_passwords
マイグレーションの設定
  • usersテーブルの設定
# ---------- 20080812202830_create_users.rb ----------
class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table "users", :force => true do |t|
      t.column :login,                     :string
      t.column :email,                     :string
      t.column :crypted_password,          :string, :limit => 40
      t.column :salt,                      :string, :limit => 40
      t.column :created_at,                :datetime
      t.column :updated_at,                :datetime
      t.column :remember_token,            :string
      t.column :remember_token_expires_at, :datetime
      t.column :activation_code, :string, :limit => 40
      t.column :activated_at, :datetime
      t.column :password_reset_code, :string, :limit => 40
      t.column :enabled, :boolean, :default => true      
    end
  end

  def self.down
    drop_table "users"
  end
end
  • rolesテーブルの設定
# ---------- 20080813091459_create_roles.rb ----------
class CreateRoles < ActiveRecord::Migration
  def self.up
    create_table :roles do |t|
      t.string :name

      t.timestamps
    end
  end

  def self.down
    drop_table :roles
  end
end
  • permissionsテーブルの設定とユーザー'admin'の登録
    • デフォルトで管理者権限'administrator'を持つ'admin'ユーザーを登録しておく。
# ---------- 20080813103634_create_permissions.rb ----------
class CreatePermissions < ActiveRecord::Migration
  def self.up
    create_table :permissions do |t|
      t.integer :role_id, :user_id, :null => false

      t.timestamps
    end
    #Make sure the role migration file was generated first    
    Role.create(:name => 'administrator')
    #Then, add default admin user
    #Be sure change the password later or in this migration file
    user = User.new
    user.login = "admin"
    user.email = "--ADMIN_NAME--@gmail.com"
    user.password = "admin"
    user.password_confirmation = "admin"
    user.save(false)
    user.send(:activate!)
    role = Role.find_by_name('administrator')
    user = User.find_by_login('admin')
    permission = Permission.new
    permission.role = role
    permission.user = user
    permission.save(false)
  end

  def self.down
    drop_table :permissions
    Role.find_by_rolename('administrator').destroy   
    User.find_by_login('admin').destroy   
  end
end
モデルの設定
  • userとroleは多:多関連。permissionを中間テーブルとして関係を持つ。
# ---------- app/models/role.rb ----------
class Role < ActiveRecord::Base
  has_many :permissions
  has_many :users, :through => :permissions

  validates_presence_of :name
end
# ---------- app/models/permission.rb ----------
class Permission < ActiveRecord::Base
  belongs_to :user
  belongs_to :role
end
  • observerを利用せずメール送信を行うため、DB保存の処理を6つのバッチ処理として区別したメソッドを設定した。
    • sign_up(ユーザー登録する時、アクティベーションのメールを送信する。)
    • activate!(アクティベーションを有効にして、その完了の通知メールを送信する。)
    • remember_me_until(time)(ログイン時、ログイン状態を維持するフラグを更新する。)
    • forget_me(ログアウトする時、ログイン状態の維持も無効にする。)
    • forgot_password(パスワードを忘れた場合、変更手続きメールを送信する。)
    • reset_password(パスワードの変更手続きと、その完了通知メールを送信する。)
  • 上記変更によって、オブザーバーは不要になり、その時の状態を判定するためのインスタンス変数やその判定メソッドも不要になった。
    • recently_activated?、pending?、recently_forgot_password?、recently_reset_password?
# ---------- app/models/user.rb ----------
require 'digest/sha1'
class User < ActiveRecord::Base
  N_("User|Password")
  N_("User|Password confirmation")
  N_("User|Old password")
  N_("%{fn} doesn't match confirmation")

  # Virtual attribute for the unencrypted password
  attr_accessor :password, :old_password

  #validates_presence_of     :login, :email
  #validates_presence_of     :password,                   :if => :password_required?
  validates_presence_of     :password_confirmation,      :if => :password_required?
  validates_length_of       :password, :within => 4..40, :if => :password_required?
  validates_confirmation_of :password,                   :if => :password_required?
  validates_length_of       :login,    :within => 3..40
  validates_length_of       :email,    :within => 6..100
  validates_uniqueness_of   :login, :email, :case_sensitive => false
  validates_format_of       :email, :with => /(^([^@\s]+)@((?:[-_a-z0-9]+\.)+[a-z]{2,})$)|(^$)/i

  def validate_on_update
    # コントローラーで params[:user][:old_password] || "" とすることで、必ず:old_passwordが検証される
    errors.add(:old_password, "古いパスワードが違っています。") if old_password && !authenticated?(old_password)
  end

  has_many :permissions
  has_many :roles, :through => :permissions

  before_save :encrypt_password

  # prevents a user from submitting a crafted form that bypasses activation
  # anything else you want your user to change should be added here.
  attr_accessible :login, :email, :password, :password_confirmation, :old_password

  # アクティベーションのエラー詳細を独自の例外に定義
  class ActivationCodeNotFound < StandardError; end
  class AlreadyActivated < StandardError
    attr_reader :user, :message;
    def initialize(user, message=nil)
      super()
      @user, @message = user, message
    end
  end

  # Finds the user with the corresponding activation code, activates their account and returns the user.
  #
  # Raises:
  #  +User::ActivationCodeNotFound+ if there is no user with the corresponding activation code
  #  +User::AlreadyActivated+ if the user with the corresponding activation code has already activated their account
  def self.find_and_activate!(activation_code)
    raise ArgumentError if activation_code.nil?
    user = find_by_activation_code(activation_code)
    raise ActivationCodeNotFound if !user
    raise AlreadyActivated.new(user) if user.active?
    user.send(:activate!)
    user
  end

  # DB操作付随処理 - ユーザー登録
  def sign_up
    self.activation_code = secret_code
    if save
      UserMailer.deliver_signup_notification(self)
      true
    end
  end

  def active?
    # the presence of an activation date means they have activated
    !activated_at.nil?
  end

  # Authenticates a user by their login name and unencrypted password.  Returns the user or nil.
  # Updated 2/20/08
  # ユーザー名とパスワードによってユーザー認証を行う。認証されたユーザーまたはnilが返る。
  def self.authenticate(login, password)    
    user = find_by_login(login) # need to get the salt
    user && user.authenticated?(password) ? user : nil  
  end

  # Encrypts some data with the salt.
  def self.encrypt(password, salt)
    Digest::SHA1.hexdigest("--#{salt}--#{password}--")
  end

  # Encrypts the password with the user salt
  def encrypt(password)
    self.class.encrypt(password, salt)
  end

  # パスワードが一致するかどうか?(ハッシュ値同士を比較)
  def authenticated?(password)
    crypted_password == encrypt(password)
  end

  # ログイン状態が記憶されているかどうか?
  def remember_token?
    remember_token_expires_at && Time.now.utc < remember_token_expires_at
  end

  # These create and unset the fields required for remembering users between browser closes
  # ログイン状態を記憶する期間
  def remember_me
    remember_me_for 2.weeks
  end

  # ログイン状態を記憶する期日に変換
  def remember_me_for(time)
    remember_me_until time.from_now.utc
  end

  # DB操作付随処理 - Remember meチェクあり - ログイン状態を記憶する期日を保存
  def remember_me_until(time)
    self.remember_token_expires_at = time
    self.remember_token            = encrypt("#{email}--#{remember_token_expires_at}")
    save(false)
  end

  # DB操作付随処理 - ログアウト
  def forget_me
    self.remember_token_expires_at = nil
    self.remember_token            = nil
    save(false)
  end
 
  # DB操作付随処理 - パスワード忘れ? - メールアドレス送信
  def forgot_password
    self.password_reset_code = secret_code
    save(false)
    UserMailer.deliver_forgot_password(self)
  end

  # DB操作付随処理 - パスワード忘れ? - パスワード再設定
  # validationの条件判定で利用
  def reset_password
    # First update the password_reset_code before setting the
    # reset_password flag to avoid duplicate email notifications.
    self.password_reset_code = nil
    if save
      UserMailer.deliver_reset_password(self)
      true
    end
  end

  def has_role?(rolename)
    self.roles.find_by_name(rolename) ? true : false
  end

  protected

    # before_save
    # パスワードをハッシュ値に変換する
    def encrypt_password
      return if password.blank?
      self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") if new_record?
      self.crypted_password = encrypt(password)
    end

    # パスワード入力が必要かどうかの判定
    def password_required?
      #crypted_password.blank? || !password.blank?
      crypted_password.blank? || !password.nil?
    end

  private

    # DB操作付随処理 - アクティベーション
    def activate!
      raise unless self.update_attribute(:activated_at, Time.now.utc)
      UserMailer.deliver_activation(self)
    end

    # 秘密のコード生成
    def secret_code
      Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
    end
end
コントローラーとビューの設定
users_controllerとそのビュー
# ---------- app/controllers/users_controller.rb ----------
class UsersController < ApplicationController
  before_filter :not_logged_in_required, :only => [:new, :create] 
  before_filter :login_required, :only => [:show, :edit, :update]
  before_filter :check_administrator_role, :only => [:index, :destroy, :enable]

  #Time.zone = 'UTC'
  #Time.zone = 'Tokyo'

  def index
    @users = User.find(:all)
  end

  #This show action only allows users to view their own profile
  def show
    @user = current_user
  end

  # render new.rhtml
  def new
    @user = User.new
  end

  def create
    cookies.delete :auth_token
    @user = User.new(params[:user])
    if @user.sign_up
      # Uncomment to have the user logged in after creating an account - Not Recommended
      # アカウント作成後、そのユーザーをログイン状態にするにはコメントマークを削除します - 推奨されません
      #self.current_user = @user
      flash[:notice] = _("Thanks for signing up! Please check your email to activate your account before logging in.")
      redirect_to login_path
    else
      flash.now[:error] = _("There was a problem creating your account.")
      render :action => 'new'
    end
  end

  def edit
    @user = current_user
  end

  def update
    @user = User.find(current_user)
    if @user.update_attributes(params[:user])
      flash[:notice] = _("User updated")
      redirect_to @user
    else
      flash.now[:error] = _("Your email was not changed.")
      render :action => 'edit'
    end
  end

  def destroy
    @user = User.find(params[:id])
    if @user.update_attribute(:enabled, false)
      flash[:notice] = _("%{user_login} disabled") % {:user_login=>@user.login}
    else
      flash[:error] = _("There was a problem disabling this user.")
    end
    redirect_to :action => 'index'
  end
  
  def enable
    @user = User.find(params[:id])
    if @user.update_attribute(:enabled, true)
      flash[:notice] = _("%{user_login} enabled") % {:user_login=>@user.login}
    else
      flash[:error] = _("There was a problem enabling this user.")
    end
      redirect_to :action => 'index'
  end
end
  • administrator権限で許可されるユーザーリスト
<%# ---------- app/views/users/index.html.erb ---------- %>
<div class="admin">
<h1><%= _("All Users") %></h1>
<table>
  <tr>
    <th>ID</th>
    <th>Username</th>
    <th>Email</th>
    <th>Enabled?</th>
    <th>Roles</th>
    <th>remember_at</th>
    <th>activated_at</th>
    <th>password_reset</th>
    <th>created_at</th>
    <th>updated_at</th>
  </tr>

<% @users.each do |user| %>
  <tr class="<%= user.enabled ? '' : 'disable' %> <%= cycle('odd', 'even') %>">
    <td><%=h user.id %></td>
    <td><%=h user.login %></td>
    <td><%=h user.email %></td>
    <td>
      <%= user.enabled ? 'yes' : 'no' %>
      <% if user.enabled %>
        <%= link_to_unless((user == current_user), _('disable'), user_path(user.id), :method => :delete) {} %>
      <% else %>
        <%= link_to_unless((user == current_user), _('enable'), enable_user_path(user.id), :method => :put) {} %>
      <% end %>
    </td>
    <td><%= link_to_unless((user == current_user), _('edit roles'), user_permissions_path(user)) {} %></td>
    <td><%= user.remember_token_expires_at.to_s(:simple) %></td>
    <td><%= user.activated_at.to_s(:simple) %></td>
    <td><%= user.password_reset_code ? 'xxxxxxxx...' : '' %></td>
    <td><%= user.created_at && user.created_at.to_s(:simple) %></td>
    <td><%= user.updated_at && user.updated_at.to_s(:simple) %></td>
  </tr>
<% end %>
</table>
</div>
  • はじめのユーザー登録
<%# ---------- app/views/users/new.html.erb ---------- %>
<div class="signup">
<h3><%= _("New Account") %></h3>
<% simple_form_for @user, :autocomplete=>'off' do |f| %>
  <%= f.text_field :login %>
  <%= f.text_field :email %> 
  <%= f.password_field :password %>
  <%= f.password_field :password_confirmation %>
  
  <p>
    <%= link_to _('Cancel'), login_path, :id=>'cancel_link' %>
    <%= submit_tag _('Sign up') %>
  </p>
<% end %>
</div>
  • ユーザー情報を編集するページ。(とりあえずemailを編集するフォームになっているが、セキュリティ的には好ましくない状態。)
<%# ---------- app/views/users/edit.html.erb ---------- %>
<div class="signup">
<h3><%= _("Edit Your Account") %></h3>

<% simple_form_for @user, :autocomplete=>'off' do |f| %>
  <%= f.text_field :email %>

  <p>
    <%= link_to _('Cancel'), root_path, :id=>'cancel_link' %>
    <%= f.submit _('Save') %>
  </p>
<% end %>
</div>
  • ユーザー情報を表示するページ。(とりあえずユーザー名と最終更新日時を表示する。)
<%# ---------- app/views/users/show.html.erb ---------- %>
<div class="signup">
<h3><%= _("User: ") + h(@user.login) %></h3>

<p><%= _("Updated on: ") + @user.updated_at.to_s(:ja) %></p>

<p><%= link_to _("Home"), root_path %></p>
</div>
activations_controller
class ActivationsController < ApplicationController
  before_filter :not_logged_in_required, :only => :show

  # Activate action
  def show
    # Uncomment and change paths to have user logged in after activation - not recommended
    # アクティベーション後ログイン状態にするには、コメントマークを削除して、リダイレクト先を変更する - 推奨されない
    #self.current_user = User.find_and_activate!(params[:id])
    #redirect_to root_path
    User.find_and_activate!(params[:id])
    flash[:notice] = _("Your account has been activated! You can now login.")
    redirect_to login_path
  rescue User::ArgumentError
    flash[:error] = _('Activation code not found. Please try creating a new account.')
    redirect_to new_user_path 
  rescue User::ActivationCodeNotFound
    flash[:error] = _('Activation code not found. Please try creating a new account.')
    redirect_to new_user_path
  rescue User::AlreadyActivated
    flash[:notice] = _('Your account has already been activated. You can log in below.')
    redirect_to login_path
  rescue
    flash[:notice] = _('System error occured.(Perhaps, update_attribute in activate!)')
    redirect_to login_path
  end
end
forgot_passwords_controllerとそのビュー
# ---------- app/controllers/forgot_passwords_controller.rb ----------
class ForgotPasswordsController < ApplicationController
  before_filter :not_logged_in_required

  verify :method=>:post, :only=>:create, :redirect_to=>{:action=>:new}
  verify :method=>:put, :only=>:update, :redirect_to=>{:action=>:edit}

  # Enter email address to recover password 
  def new
  end

  # Forgot password action
  def create    
    if @user = User.find_by_email(params[:email], :conditions=>'activated_at IS NOT NULL')
      @user.forgot_password
      flash[:notice] = _("A password reset link has been sent to your email address.")
      redirect_to login_path
    else
      flash.now[:error] = _("Could not find a user with that email address.")
      render :action => 'new'
    end  
  end
   
  # Action triggered by clicking on the /reset_password/:id link recieved via email
  # Makes sure the id code is included
  # Checks that the id code matches a user in the database
  # Then if everything checks out, shows the password reset fields
  def edit
    # if params[:id]重要!! これがないとDB先頭のユーザーが代入されてしまう
    @user = User.find_by_password_reset_code(params[:id]) if params[:id]
    raise if @user.nil?
  rescue
    logger.error "Invalid Reset Code entered."
    flash[:notice] = _("Sorry - That is an invalid password reset code. Please check your code and try again. (Perhaps your email client inserted a carriage return?)")
    redirect_to new_forgot_password_path
  end

  # Reset password action /reset_password/:id
  # Checks once again that an id is included and makes sure that the password field isn't blank
  def update
    # if params[:id]重要!! これがないとDB先頭のユーザーが代入されてしまう
    @user = User.find_by_password_reset_code(params[:id]) if params[:id]
    raise if @user.nil?
    @user.password              = params[:user][:password]
    @user.password_confirmation = params[:user][:password_confirmation]
    if @user.reset_password
      flash[:notice] = _("Password reset.")
      redirect_to login_path
    else
      flash.now[:error] = _("Password mismatch.")
      render :action=>'edit', :id=>params[:id]
    end
  rescue
    logger.error "Cracking?"
    flash[:error] = _("Sorry - Password not reset.")
    redirect_to login_path
  end     
end
  • 登録しているメールアドレスを入力するページ
<%# ---------- app/views/forgot_passwords/new.html.erb ---------- %>
<div class="password">
<h3><%= _("Forgot Password?") %></h3>

<% form_tag url_for(:action => 'create') do %>
  <p><%= label_tag :email, _("What is the email address used to create your account?") %><br />
  <%= text_field_tag :email, "", :size => 30, :autocomplete=>'off' %></p>

  <p>
    <%= link_to _('Cancel'), login_path, :id=>'cancel_link' %>
    <%= submit_tag _('Reset Password') %>
  </p>
<% end %>
</div>
  • パスワードを2回入力して再設定するページ
<%# ---------- app/views/forgot_passwords/edit.html.erb ---------- %>
<div class="password">
<h3><%= _("Reset Password") %></h3>

<% simple_form_for @user, :url=>{:action => "update", :id => params[:id]} do |f| %>
  <%= f.password_field :password %>
  <%= f.password_field :password_confirmation %>

  <p>
    <%#= link_to 'Cancel', login_path, :id=>'cancel_link' %>
    <%= submit_tag _('Reset Your Password') %>
  </p>
<% end %>
</div>
change_passwords_controllerとそのビュー
  • ログインしている状態でパスワードの変更処理を行う。
# ---------- app/controllers/change_passwords_controller.rb ----------
class ChangePasswordsController < ApplicationController
  before_filter :login_required

  verify :method=>:put, :only=>:update, :redirect_to=>{:action=>:edit}

  def edit
    @user = current_user
  end

  # Change password action
  def update
    @user = User.find(current_user)
    @user.old_password          = params[:user][:old_password] || ""
    @user.password              = params[:user][:password]
    @user.password_confirmation = params[:user][:password_confirmation]
    if @user.save
      flash[:notice] = _("Password successfully updated.")
      redirect_to @user
    else
      flash.now[:error] = _("An error occured, your password was not changed.")
      render :action => 'edit'
    end
  end
end
  • 現在のパスワードと、新しいパスワードを2回入力するページ
<%# ---------- app/views/change_passwords/edit.html.erb ---------- %>
<div class="signup">
<h3><%= _("Change Password") %></h3>

<% simple_form_for @user, :url=>url_for(:action => "update") do |f| %>
  <%= f.password_field :old_password %>
  <%= f.password_field :password, :label=>_("New password") %>
  <%= f.password_field :password_confirmation, :label=>_("New password confirmation") %>

  <p>
    <%= link_to _('Cancel'), root_path, :id=>'cancel_link' %>
    <%= submit_tag _('Change password') %>
  </p>
<% end %>
</div>
sessions_controller
  • ログイン、ログアウトの処理を行う。
# ---------- app/controllers/sessions_controller.rb ----------
# This controller handles the login/logout function of the site.  
class SessionsController < ApplicationController
  before_filter :login_required, :only => :destroy
  before_filter :not_logged_in_required, :only => [:new, :create]

  # render new.html.erb
  def new
  end

  def create
    user = User.authenticate(params[:login], params[:password])
    case
    when user == nil
      render_new_with_msg(_("Your username or password is incorrect."))
    when user.activated_at.blank?
      render_new_with_msg(_("Your account is not active, please check your email for the activation code."))
    when user.enabled == false
      render_new_with_msg(_("Your account has been disabled."))
    else
      self.current_user = user
      if params[:remember_me] == "1"
        self.current_user.remember_me
        cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires_at }
      end
      flash[:notice] = _("Logged in successfully")
      redirect_back_or_default('/')
    end
  end

  def destroy
    self.current_user.forget_me if logged_in?
    cookies.delete :auth_token
    reset_session
    flash[:notice] = _("You have been logged out.")
  redirect_to login_path
  end

private

  def render_new_with_msg(message)
    flash.now[:error] = message
    render :action => 'new'
  end
end
  • ログインのページ
<%# ---------- app/views/sessions/new.html.erb ---------- %>
<div class="login">

<h3><%= _("Log In") %></h3>

<div class="login_form">
  <% form_tag session_path do %>
    <p><%= label_tag 'login', _('User|Login') %><br />
    <%= text_field_tag 'login', nil, :autocomplete=>'off' %></p>

    <p><%= label_tag 'password', _('User|Password') %><br />
    <%= password_field_tag 'password' %></p>

    <p><%= check_box_tag 'remember_me' %>
    <%= label_tag 'remember_me', _('Remember me') %></p>

    <%= submit_tag _('Log in') %>
  <% end %>
</div>

<div class="login_link">
  <ul>
    <li><%= link_to _("Sign Up"), new_user_path %></li>
    <li><%= link_to _("Forgot Password?"), forgot_password_path %></li>
  </ul>
</div>

</div>
permissions_controllerとそのビュー
  • administrator権限でユーザーに権限を設定する。
# ---------- app/controllers/permissions_controller.rb ----------
class PermissionsController < ApplicationController
   before_filter :check_administrator_role
  
   def index
     @user = User.find(params[:user_id])
     @all_roles = Role.find(:all)
   end
  
   def update
     @user = User.find(params[:user_id])
     @role = Role.find(params[:id])
     if @user.has_role?(@role.name)
       @user.roles.delete(@role)
     else
       @user.roles << @role
     end
     redirect_to :action => 'index'
   end
end
  • 設定済の権限、未設定の権限を表示するページ
<%# ---------- app/views/permissions/index.html.erb ---------- %>
<div class="admin">

<h2><%= _("Roles for %{user_login}") % {:user_login => h(@user.login.capitalize)} %></h2>

<p>
  <% field_set_tag _("Roles assigned") do %>
  <ul><%= render :partial => @user.roles %></ul>
  <% end %>
</p>

<p>
  <% field_set_tag _("Roles available") do %>
  <ul><%= render :partial => (@all_roles - @user.roles) %></ul>
  <% end %>
</p>

<p><%= link_to _('Back'), users_path %></p>

</div>
roles_controllerとそのビュー
# ---------- app/controllers/roles_controller.rb ----------
class RolesController < ApplicationController
  before_filter :check_administrator_role
...(以下scaffoldからの変更なし)...
  • リスト表示と新規追加、編集のページ
<%# ---------- app/views/roles/index.html.erb ---------- %>
<div class="admin">
<h1><%= _("Listing roles") %></h1>

<table>
  <tr>
    <th style="width:2em">ID</th>
    <th>Name</th>
    <th style="width:4em"></th>
    <th style="width:4em"></th>
    <th style="width:4em"></th>
  </tr>

<% for role in @roles %>
  <tr>
    <td><%=h role.id %></td>
    <td><%=h role.name %></td>
    <td><%= link_to _('Show'), role %></td>
    <td><%= link_to _('Edit'), edit_role_path(role) %></td>
    <td><%= link_to _('Destroy'), role, :confirm => _('Are you sure?'), :method => :delete %></td>
  </tr>
<% end %>
</table>

<p><%= link_to _('New role'), new_role_path %></p>
</div>
<%# ---------- app/views/roles/_label_msg_form.html.erb ---------- %>
<%= label_msg_form.text_field :name %>
<%# ---------- app/views/roles/new.html.erb ---------- %>
<div class="admin">
<% label_msg_form_for(@role) do |f| %>
  <%= render :partial=>f %>

  <%= f.submit _("Create") %>
<% end %>

<p>
  <%= link_to _('Back'), roles_path %>
</p>
</div>
<%# ---------- app/views/roles/edit.html.erb ---------- %>
<div class="admin">
<% label_msg_form_for(@role) do |f| %>
  <%= render :partial=>f %>

  <%= f.submit _("Update") %>
<% end %>

<p>
  <%= link_to _('Show'), @role %> |
  <%= link_to _('Back'), roles_path %>
</p>
</div>
<%# ---------- app/views/roles/show.html.erb ---------- %>
<div class="admin">
<% label_msg_form_for(@role, :disabled=>true) do |f| %>
  <%= render :partial=>f %>
<% end %>

<p>
  <%= link_to _('Edit'), edit_role_path(@role) %> |
  <%= link_to _('Back'), roles_path %>
</p>
</div>
  • permission_controllerから権限を設定する時に呼び出される。
<%# ---------- app/views/roles/_role.html.erb ---------- %>
<li>
  <%= role.name %>
  <% if @user.has_role?(role.name) %>
    <%= link_to _('remove role'), user_permission_url(@user, role.id), :method => :put %>
  <% else %>
    <%= link_to _('assign role'), user_permission_url(@user, role.id), :method => :put %>
  <% end %>
</li>
共通のレイアウトファイルの設定
  • コントローラー名.html.erbのレイアウトファイルが存在しない時は、レイアウトファイルapplication.html.erbが利用される。
  • application.html.erb以外のレイアウトファイルはすべて削除した。
  • メニューバーとフッターバーは、それぞれ_menu.html.erb、_footer.html.erbをrender :partialで描画している。
    • メニューについては、1階層だけプルダウン可能にした。
    • フッターについては、ページ内容が少ない時でも、常にページの一番下に表示される。
<%# ---------- app/views/layouts/application.html.erb ---------- %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title><%= controller.controller_name %>: <%= controller.action_name %></title>
  <%= stylesheet_link_tag 'scaffold', 'menu', 'login', 'signup', 'password', 'admin', 'footer' %>
</head>
<body>
<div class="wrapper">

  <%= render :partial=>'layouts/menu' %>

  <div id="page">

    <p style="color:green; text-align:center"><%= flash[:notice] %></p>
    <p style="color:red; text-align:center"><%= flash[:error] %></p>

    <%= yield  %>

    <div class="push"></div>

  </div>
</div>

<div class="footer">
  <%= render :partial=>'layouts/footer' %>
</div>
</body>
</html>
<%# ---------- app/views/layouts/_menu.html.erb ---------- %>
<div class="menu">
<ul id="nav">
   <% if logged_in? %>
   <% if current_user.has_role?('administrator') %>
     <li><%= link_to_unless_current _('Roles'), roles_path %></li>
     <li><%= link_to_unless_current _('Users'), users_path %></li>
   <% end %>
     <li></li><%# for space %>
     <li><%= link_to h(current_user.login.capitalize), user_path(current_user) %>
       <ul>
         <li><%= link_to _('Edit Profile'), edit_user_path(current_user) %></li>
         <li><%= link_to _('Change Password'), edit_user_change_password_path(current_user) %></li>
         <li><%= link_to _('Log Out'), logout_url %></li>
       </ul>
     </li>
     <li style="text-align:right; margin-right:0.5em;"><%= _("Logged in as:") %></li>
     <li><%= link_to_unless_current _("Home"), root_path %></li>
   <% else %>
     <li><%= link_to_unless_current _('Forgot Password?'), new_forgot_password_path %></li>
     <li><%= link_to_unless_current _('Sign Up'), new_user_path %></li>
     <li><%= link_to_unless_current _('Log In'), login_path %></li>
   <% end %>
</ul>
<div class="float-clear"></div>
</div>
<%# ---------- app/views/layouts/_footer.html.erb ---------- %>
<div id="footer" align="right">
  Powered by <%= link_to 'Rails', "http://www.rubyonrails.org/" %> 2.1
</div>
user_mailerモデルとビューの設定(送信メールの内容を設定)
# ---------- app/models/user_mailer.rb ----------
class UserMailer < ActionMailer::Base
  YOURSITE = 'localhost:3000'
  ADMINEMAIL = 'ユーザー名@gmail.com'

  def signup_notification(user)
    setup_email(user)
    @subject    += _('Please activate your new account')
  
    @body[:url]  = "http://#{YOURSITE}/activate/#{user.activation_code}"
  
  end
  
  def activation(user)
    setup_email(user)
    @subject    += _('Your account has been activated!')
    @body[:url]  = "http://#{YOURSITE}/"
  end

  def forgot_password(user)
    setup_email(user)
    @subject    += _('You have requested to change your password')
    @body[:url]  = "http://#{YOURSITE}/reset_password/#{user.password_reset_code}"
  end
  
  def reset_password(user)
    setup_email(user)
    @subject    += _('Your password has been reset.')
  end  

  protected
    def setup_email(user)
      @recipients  = "#{user.email}"
      @from        = ADMINEMAIL
      @subject     = "[#{YOURSITE}] "
      @sent_on     = Time.now
      @body[:user] = user
    end
end
<%# ---------- app/views/user_mailer/signup_notification.html.erb ---------- %>
<%= _("Your account has been created.") %>

  <%= _("Username: %{user_login}") % {:user_login=>h(@user.login)} %>

<%= _("Visit this url to activate your account:") %>

  <%= @url %>
<%# ---------- app/views/user_mailer/activation.html.erb ---------- %>
<%= _("%{user_login}, your account has been activated.  To visit the site, follow the link below:") % {:user_login=>h(@user.login)} %>

  <%= @url %>
  • パスワードを再設定するURLを送信するメール
<%# ---------- app/views/user_mailer/forgot_password.html.erb ---------- %>
<%= _("%{user_login}, to reset your password, please visit") % {:user_login=>h(@user.login)} %>

  <%= @url %>
  • パスワードが再設定されたことを通知するメール
<%# ---------- app/views/user_mailer/reset_password.html.erb ---------- %>
<%=h _("%{user_login}, Your password has been reset") % {:user_login=>h(@user.login)} %>
authenticated_systemの設定
# ---------- lib/authenticated_system.rb ----------
module AuthenticatedSystem
  protected
    # Returns true or false if the user is logged in.
    # Preloads @current_user with the user model if they're logged in.
    def logged_in?
      !!current_user
    end

    # Accesses the current user from the session.  Set it to :false if login fails
    # so that future calls do not hit the database.
    def current_user
      #@current_user ||= (login_from_session || login_from_basic_auth || login_from_cookie || :false)
      @current_user ||= (login_from_session || login_from_basic_auth || login_from_cookie) unless @current_user == false
    end

    # Store the given user id in the session.
    def current_user=(new_user)
      session[:user_id] = new_user ? new_user.id : nil
      @current_user = new_user || false
    end

    # Check if the user is authorized
    #
    # Override this method in your controllers if you want to restrict access
    # to only a few actions or if you want to check if the user
    # has the correct rights.
    #
    # Example:
    #
    #  # only allow nonbobs
    #  def authorized?
    #    current_user.login != "bob"
    #  end
    def authorized?
      logged_in?
    end

    # Filter method to enforce a login requirement.
    #
    # To require logins for all actions, use this in your controllers:
    #
    #   before_filter :login_required
    #
    # To require logins for specific actions, use this in your controllers:
    #
    #   before_filter :login_required, :only => [ :edit, :update ]
    #
    # To skip this in a subclassed controller:
    #
    #   skip_before_filter :login_required
    #
    # ログインしていることを要求して、ログインしていなかったらaccess_deniedを処理する
    def login_required
      authorized? || access_denied
    end

    # ログインしていないことを要求して、ログインしていたらpermission_deniedを処理する
    def not_logged_in_required
      !logged_in? || permission_denied(_("An account is logged in now. Please logout at once."))
    end

    def check_role(role)
      unless logged_in? && @current_user.has_role?(role)
        if logged_in?
          permission_denied(_("You don't have permission to complete that action."))
        else
          store_referer
          access_denied
        end
      end
    end

    # そのユーザーがadministrator権限かどうか確認する
    def check_administrator_role
      check_role('administrator')
    end    

    # Redirect as appropriate when an access request fails.
    #
    # The default action is to redirect to the login screen.
    #
    # Override this method in your controllers if you want to have special
    # behavior in case the user is not authorized
    # to access the requested action.  For example, a popup window might
    # simply close itself.
    # ログインを要求されアクセスできない場合の処理
    def access_denied
      respond_to do |format|
        format.html do
          store_location
          flash[:error] = _("You must be logged in to access this feature.")
          redirect_to new_session_path
        end
        format.any do
          request_http_basic_authentication 'Web Password'
        end
      end
    end

    # 権限が認めらずアクセスできない場合の処理
    def permission_denied(message)
      respond_to do |format|
        format.html do
          flash.keep(:notice)
          flash[:error] = message
          redirect_last_or_default(root_path)  
        end
        format.xml do
          headers["Status"]           = "Unauthorized"
          headers["WWW-Authenticate"] = %(Basic realm="Web Password")
          render :text => "You don't have permission to complete this action.", :status => '401 Unauthorized'
        end
      end
    end

    # Store the URI of the current request in the session.
    #
    # We can return to this location by calling #redirect_back_or_default.
    def store_location
      session[:return_to] = request.request_uri
    end

    # リンクもとURLを記録する
    # request.env["HTTP_REFERER"]には、リンクをクリックして移動した時のみ記録される
    def store_referer
      session[:refer_to] = request.env["HTTP_REFERER"]
    end

    # request.env["HTTP_REFERER"]に記録されないリンクも記録する(URLを手入力した場合)
    # after_filter :store_last で呼び出され、記録する
    def store_last
      if logged_in? && controller_name != 'sessions'
        flash[:last_to] = request.request_uri
      end
    end

    # Redirect to the URI stored by the most recent store_location call or
    # to the passed default.
    def redirect_back_or_default(default)
      redirect_to(session[:return_to] || default)
      session[:return_to] = nil
    end

    def redirect_to_referer_or_default(default)
      redirect_to(session[:refer_to] || default)
      session[:refer_to] = nil
    end

    def redirect_last_or_default(default)
      redirect_to(flash[:last_to] || default)
    end

    # Inclusion hook to make #current_user and #logged_in?
    # available as ActionView helper methods.
    # current_user と logged_in? をビューで利用できるヘルパメソッドとして登録する
    def self.included(base)
      base.send :helper_method, :current_user, :logged_in?
    end

    # Called from #current_user.  First attempt to login by the user id stored in the session.
    def login_from_session
      #self.current_user = User.find(session[:user_id]) if session[:user_id]
      self.current_user = User.find(session[:user_id]) if session[:user_id] rescue nil
    end

    # Called from #current_user.  Now, attempt to login by basic authentication information.
    def login_from_basic_auth
      authenticate_with_http_basic do |username, password|
        self.current_user = User.authenticate(username, password)
      end
    end

    # Called from #current_user.  Finaly, attempt to login by an expiring token in the cookie.
    def login_from_cookie
      user = cookies[:auth_token] && User.find_by_remember_token(cookies[:auth_token])
      if user && user.remember_token?
        user.remember_me
        cookies[:auth_token] = { :value => user.remember_token, :expires => user.remember_token_expires_at }
        self.current_user = user
      end
    end
end
ルートの設定
ActionController::Routing::Routes.draw do |map|
  map.root :controller => "todos", :action => "index"
  map.signup '/signup', :controller => 'users', :action => 'new'
  map.login '/login', :controller => 'sessions', :action => 'new'
  map.logout '/logout', :controller => 'sessions', :action => 'destroy'
  map.activate '/activate/:id', :controller => 'activations', :action => 'show'
  map.reset_password '/reset_password/:id', :controller => 'forgot_passwords', :action => 'edit'
  
  # See how all your routes lay out with "rake routes"
  map.resources :todos

  map.resources :roles

  map.resources :users, :member => { :enable => :put } do |users|
    users.resource :change_password
    users.resources :permissions
  end

  map.resource :session
  map.resource :forgot_password
  map.resource :activation

  # Install the default routes as the lowest priority.
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end

ruby-gettextとobserverの設定
# 文字列処理で日本語を考慮した処理メソッドを利用可能にする。
# http://www.ruby-lang.org/ja/man/html/jcode.html
require 'jcode'
...(中略)...
Rails::Initializer.run do |config|
...(中略)...
  # Activate observers that should always be running
  # config.active_record.observers = :cacher, :garbage_collector
  
  # ruby-gettext と active_record.observers がコンフリクトするため
  # unless defined? GetTextによって回避
  #   ActiveRecord::Observer + GetTextで翻訳文字列が抽出できなくなる
  #   http://www.yotabanana.com/lab/20071024.html#p01
  #unless defined? GetText
  #  config.active_record.observers = :user_observer
  #end
  
  config.gem "gettext", :lib => "gettext/rails"
end
  • rakeタスクupdatepo、makemoの設定
# ---------- lib/tasks/gettext.rake ----------
desc "Update pot/po files."
task :updatepo do
  require 'gettext/utils'
  # Rails2.0から拡張子.erbもRubyとして解析する必要があるので、以下の追記が必要
  GetText::ErbParser.init(:extnames => ['.rhtml', '.erb'])

  GetText.update_pofiles(
    "todo",  #テキストドメイン名(init_gettextで使用した名前) 
    Dir.glob("{app,config,components,lib}/**/*.{rb,rhtml,rjs,erb}"),  #ターゲットとなるファイル(文字列内は余分なスペース無しで指定する)
    "todo_role 1.0.0")  #アプリケーションのバージョン
end

desc "Create mo-files"
task :makemo do
  require 'gettext/utils'
  GetText.create_mofiles(true, "po", "locale")
end
  • init_gettextの設定
# ---------- app/controllers/application.rb ----------
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.

class ApplicationController < ActionController::Base
  include AuthenticatedSystem

  helper :all # include all helpers, all the time

  # See ActionController::RequestForgeryProtection for details
  # Uncomment the :secret if you're not using the cookie session store
  protect_from_forgery # :secret => '552fb60b62989ce92df508ebd6a5XXXX'
  
  # See ActionController::Base for details 
  # Uncomment this to filter the contents of submitted sensitive data parameters
  # from your application log (in this case, all fields with names like "password"). 
  # filter_parameter_logging :password

  init_gettext 'todo' #rake updatepoのテキストドメイン名を指定する
end
日時表示の書式を設定
# ---------- config/initializers/datetime_formats.rb ----------
# 日時表示の書式を設定できる
# http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/DateTime/Calculations/ClassMethods.html
# >> user.created_at.to_s(:month_and_year)
# => August 2008
# >> user.updated_at.to_s(:short_ordinal)
# => August 7th
Time::DATE_FORMATS[:month_and_year] = "%B %Y"
Time::DATE_FORMATS[:short_ordinal] = lambda { |time| time.strftime("%B #{time.day.ordinalize}") }

Time::DATE_FORMATS[:time_only] = "%H:%M:%S"
Time::DATE_FORMATS[:simple] = "%Y-%m-%d<br />%H:%M:%S"
Time::DATE_FORMATS[:ja] = "%Y年%m月%d日 %H:%M:%S"
スタイルシートの設定
/* ---------- public/stylesheets/scaffold.css ---------- */
body { background-color: #fff; color: #333; }

body, p, ol, ul, td {
  font-family: verdana, arial, helvetica, sans-serif;
  font-size:   13px;
  line-height: 18px;
}

pre {
  background-color: #eee;
  padding: 10px;
  font-size: 11px;
}

a { color: #000; }
a:visited { color: #666; }
a:hover { color: #fff; background-color:#000; }

.fieldWithErrors input {
  background: pink;
  }
.formErrorMsg {
  font-size: x-small;
  color: red;
  margin-left: 2em;
  }

#errorExplanation {
  width: 400px;
  border: 2px solid red;
  padding: 7px;
  padding-bottom: 12px;
  margin-bottom: 20px;
  background-color: #f0f0f0;
}

#errorExplanation h2 {
  text-align: left;
  font-weight: bold;
  padding: 5px 5px 5px 15px;
  font-size: 12px;
  margin: -7px;
  background-color: #c00;
  color: #fff;
}

#errorExplanation p {
  color: #333;
  margin-bottom: 0;
  padding: 5px;
}

#errorExplanation ul li {
  font-size: 12px;
  list-style: square;
}
/* ---------- public/stylesheets/signup.css ---------- */
.signup label, 
.signup #cancel_link {
  font-weight: bold;
  font-size: smaller;
  width: 12em;
  float: left;
  text-align: right;
  margin-right: 0.5em;
}

.signup {
  padding: 0.2em 1em;
  margin: 0;
  margin-left: auto;
  margin-right: auto;
  background: #ccc;
  width: 30em;
}

.signup legend {
  font-weight: bold;
}

.signup a { color: #888; }
.signup a:visited { color: #888; }
.signup a:hover { color: #000; background:#ccc; }

.signup input[type=text], 
.signup input[type=password] {
  padding: 2px 0;
  margin: 0;
}

.signup input[type=submit] {
  padding: 0;
  margin: 0 2px;
}

.signup .formErrorMsg {
  font-size: x-small;
  color: red;
  margin-left: 14em;
}
/* ---------- public/stylesheets/login.css ---------- */
.login {
  padding: 0;
  margin: 0;
  margin-left: auto;
  margin-right: auto;
  background: #444;
  color: white;
  width: 14.2em;
}
.login a { color: #ccc; }
.login a:visited { color: #ccc; }
.login a:hover { color: #fff; background-color:#222; }

.login_form {
  padding: 0 1em;
  margin: 0;
  background: #444;
}
.login_link {
  background: #222;
}
.login input[type=text], 
.login input[type=password] {
  padding: 2px 0;
  margin: 0;
  width: 14em;
}
.login input[type=submit] {
  padding: 0;
  margin: 0 2px;
  width: 14em;
}
/* ---------- public/stylesheets/password.css ---------- */
.password label, 
.password #cancel_link {
  font-weight: bold;
  font-size: smaller;
}
.password:after {
  content: "";
  display: block;
  height: 0;
  clear: both;
}

.password {
  padding: 0.2em 1em;
  margin: 0;
  margin-left: auto;
  margin-right: auto;
  background: #ccc;
  width: 25em;
}

.password legend {
  font-weight: bold;
}

.password a { color: #888; }
.password a:visited { color: #888; }
.password a:hover { color: #000; background:#ccc; }

.password input[type=text], 
.password input[type=password] {
  padding: 2px 0;
  margin: 0;
  /*width: 15em;*/
}

.password input[type=submit] {
  padding: 0;
  margin: 0 2px;
}

.password .formErrorMsg {
  font-size: x-small;
  color: red;
  margin-left: 2em;
}
/* ---------- public/stylesheets/admin.css ---------- */
.admin {
  padding: 0;
  margin: 0 0.5em;
}

.admin table {
    /*width: 530px;*/
    width: 640px;
    border: 1px #E3E3E3 solid;
    border-collapse: collapse;
    border-spacing: 0;
    }

.admin table th {
    padding: 5px;
    border: #E3E3E3 solid;
    border-width: 0 0 1px 1px;
    background: #F5F5F5;
    font-weight: bold;
    line-height: 120%;
    text-align: center;
    white-space: nowrap;
    }
.admin table td {
    padding: 5px;
    border: 1px #E3E3E3 solid;
    border-width: 0 0 1px 1px;
    text-align: left;
    white-space: nowrap;
    }

.admin fieldset {
  background: #F5F5F5;
  width: 30em;
}

.admin .disable {
  color: #ddd;
}
/* ---------- public/stylesheets/menu.css ---------- */
body {
  margin: 0;
}

#page {
  margin: 0 0.5em;
}

.menu {
  padding: 0.5em 5em;
  margin: 0;
  background: #444;
}

#nav, #nav ul {
	padding: 0;
	margin: 0;
  color: #ddd;
	list-style: none;
}

#nav a { color: #ccc; }
#nav a:visited { color: #ccc; }
#nav a:hover { color: #fff; background-color:#222; }

#nav a {
	display: block;
}

#nav li {
	padding: 3px 0;
	margin: 0 2em 0 0;
	float: right;
	width: auto;
  background: #444;
  text-align: left;
}

#nav li ul li {
  margin: 0;
	width: 10em;
  text-align: left;
}

#nav li ul {
	position: absolute;
  background: #444;
	width: 10em;
	left: -999em;
}

#nav li:hover ul {
	left: auto;
}

#nav li:hover ul, #nav li.sfhover ul {
	left: auto;
}

.float-clear {
  clear: both;
}
/* ---------- public/stylesheets/footer.css ---------- */
html, body {
    height: 100%;
    }
.wrapper {
    min-height: 100%;
    height: auto !important;
    height: 100%;
    margin: 0 auto -20px; /* the bottom margin is the negative value of the footer's height */
    }
.footer, .push {
    height: 20px; /* .push must be the same height as .footer */
    }

.footer {
    padding: 0;
    margin: 0;
    color: #ddd;
    background: #444;
    font-size: x-small;
}
#footer {
    padding-top: 2px;
    padding-right: 10px;
}
poファイルを日本語に翻訳
  • ひたすら日本語に翻訳した。長過ぎるので未掲載。
データベースのリセット
  • 以上の設定をして、最後にDBを作り直しておいた。(今までのデーターはリセットされ、ユーザー'admin'だけが登録された状態になる。)
# ---------- ターミナルでの操作 ----------
$ rake db:migrate:reset

動作確認

  • 画面を確認してみると...