restful_authentication + ruby-gettext + form_forで権限管理付きログインページの雛形を作る(日本語版)
restful_authenticationに権限(role)管理を追加して、実用的に利用するカスタマイズ例として、Restful Authentication with all the bells and whistlesを参考にいろいろ考えてみた。コードを読みながら、次第に以下のような疑問や考えが浮かんできた。
- メール送信処理のきっかけに、なぜobserverを利用するだろうか?
-
- observerはコールバック(before_createとかafter_save等)を外部のオブザーバーオブジェクトで処理するようなもの。
- モデル自身のコールバックとして処理しても良いはず。
- observerはコールバック(before_createとかafter_save等)を外部のオブザーバーオブジェクトで処理するようなもの。
- 結構コントローラーが一生懸命仕事をしている。もっとモデルに働いてもらうようにする。
- Change passwordアクションとか、Reset passwordアクションの部分。
- form_forの:builderオプションを活用して、ビューのコードをよりシンプルにする。
- コントローラーの命名をより分かり易いダイレクトな名前にしてしまう。
イベント | 以前のコントローラー名前 | 新しいコントローラー名前 |
---|---|---|
アクティベーションを有効にする | 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}
以上のようなことを考えながら、自分なりに修正してみた。
- restful_authenticationを以下の日記の状態まで進めたtodoプロジェクトを利用している。
- gettextはインストール済
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つのバッチ処理として区別したメソッドを設定した。
- 上記変更によって、オブザーバーは不要になり、その時の状態を判定するためのインスタンス変数やその判定メソッドも不要になった。
- 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
- ユーザー登録の後、アクティベーションのURLを送信するメール
<%# ---------- 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の設定
- ruby-gettext 1.92.0より、Rails::Initializer.runブロックの中で、config.gem "gettext", :lib => "gettext/rails"を指定するだけでOK。
- observerは利用しないので、config.active_record.observers = :user_observer はコメントアウトしておいた。
- もし、observerをruby-gettextを同時に利用する場合は、unless defined? GetText で条件判定しておく必要あり。
- ActiveRecord::Observer + GetTextで翻訳文字列が抽出できなくなる
# 文字列処理で日本語を考慮した処理メソッドを利用可能にする。 # 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
動作確認
- 画面を確認してみると...
- 以下のページでサンプルを操作できます。(メールの送信機能は制限されている)
- http://todo-797857.herokugarden.com/login
- ログイン名、パスワードは「admin」
- 以下のページからダウンロードできます。
- heroku_todo-797857ダウンロードページへ
- 起動方法をお読みください。
- restful_authentication + stateful(aasm) + email変更の認証手続き + emailによるログイン機能を追加しました。