チュートリアルをやってみる。ユーザ更新編
これはチュートリアルをやってみたログである。railstutorial.jp
editアクションに編集画面をつくる。
- Name,Email,Password,Confirm,の各フォームと、saveボタンを入れる。一番下に画像変更も。
app/controllers/users_controller.rb
class UsersController < ApplicationController ... def edit @user = User.find(params[:id]) end ....
idを取り出し、そこからユーザを取得しておく。
editビュー側は
app/views/users/edit.html.erb
<% provide(:title, "Edit user") %> <h1>Update your profile</h1> <div class="row"> <div class="span6 offset3"> <%= form_for(@user) do |f| %> <%= render 'shared/error_messages' %> <%= f.label :name %> <%= f.text_field :name %> <%= f.label :email %> <%= f.text_field :email %> <%= f.label :password %> <%= f.password_field :password %> <%= f.label :password_confirmation, "Confirm Password" %> <%= f.password_field :password_confirmation %> <%= f.submit "Save changes", class: "btn btn-large btn-primary" %> <% end %> <%= gravatar_for @user %> <a href="http://gravatar.com/emails">change</a> </div> </div>
登録画面とほぼ同じ、error_messagesパーシャルも流用。
内部的にはrailsはActiveRecordのnew_record?かどうかで新規ユーザか既存ユーザか見分ける。
ヘッダーのeditにリンクをつなぐ
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse"> <div class="navbar-inner"> <div class="container"> <%= link_to "sample app", root_path, id: "logo" %> <nav> <ul class="nav pull-right"> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Help", help_path %></li> <% if signed_in? %> <li><%= link_to "Users", '#' %></li> <li id="fat-menu" class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Account <b class="caret"></b> </a> <ul class="dropdown-menu"> <li><%= link_to "Profile", current_user %></li> <li><%= link_to "Settings", edit_user_path(current_user) %></li> <li class="divider"></li> <li> <%= link_to "Sign out", signout_path, method: "delete" %> </li> </ul> </li> <% else %> <li><%= link_to "Sign in", signin_path %></li> <% end %> </ul> </nav> </div> </div> </header>
これは
<li><%= link_to "Settings", edit_user_path(current_user) %></li>
が変わっただけ。
更新
ユーザを更新
app/controllers/users_controller.rb
class UsersController < ApplicationController ...... def update @user = User.find(params[:id]) if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end .... end
user_paramsはUsersControllerで定義したメソッド
認可
このままではサインインすれば誰もが他人を編集できてしまう。
サインインしてないユーザをリダイレクト
signed_inしてない限りsignin_urlにリダイレクトするアクションをつくる。
これらはbeforeアクションとして特定のアクション(edit,update)の前にsigned_in_userが呼ばれるようにする。
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :signed_in_user, only: [:edit, :update] .... # Before actions def signed_in_user redirect_to signin_url, notice: "Please sign in." unless signed_in? end end
notice:"Please sign in."はflashに格納される。
他のユーザから編集されていないかをチェック
編集画面が他人のものでないかをチェックするようにする。
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :signed_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] .... # Before actions def signed_in_user redirect_to signin_url, notice: "Please sign in." unless signed_in? end def correct_user @user = User.find(params[:id]) redirect_to(root_path) unless current_user?(@user) end end
current_user?はヘルパーメソッドとして定義
app/helpers/sessions_helper.rb
module SessionsHelper ... def current_user remember_token = User.encrypt(cookies[:remember_token]) @current_user ||= User.find_by(remember_token: remember_token) end ... def current_user?(user) user == current_user end end
beforeアクション内で@userに値が入るので
editアクションとupdateアクションの重複コード
@user = User.find(params[:id])
を削除する。
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :signed_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] ...... def edit end def update if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end private .... # Before actions def signed_in_user redirect_to signin_url, notice: "Please sign in." unless signed_in? end def correct_user @user = User.find(params[:id]) redirect_to(root_path) unless current_user?(@user) end end
signinしたら元のページに戻って欲しい。フレンドフォワーディング
Railsのセッション機能を利用する。
module SessionsHelper .... def redirect_back_or(default) redirect_to(session[:return_to] || default) session.delete(:return_to) end def store_location session[:return_to] = request.url end end
store_locationで戻り先を保存
redirect_back_orで戻り先へリダイレクト
これらを適切なアクション内で呼ぶようにする。
store_locationは
app/controllers/users_controller.rb
class UsersController < ApplicationController ...... # Before actions def signed_in_user store_location redirect_to signin_url, notice: "Please sign in." unless signed_in? end .... end
redirect_back_orは
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController ... def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) sign_in user redirect_back_or user else flash.now[:error] = 'Invalid email/password combination' render 'new' end end .... end
ローカルサーバで確認した。
rails server
全ユーザの表示
indexアクションで全ユーザ表示を行う。
表示から溢れる分はページ分割されるようにする。
コントローラ側
まず全部もってくる。
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :signed_in_user, only: [:index, :edit, :update] ... def index @users = User.all end ... end
ビュー側
ユーザごとにliで囲まれたビューをつくる。
app/views/users/index.html.erb
<% provide(:title, 'All users') %> <h1>All users</h1> <ul class="users"> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 52 %> <%= link_to user.name, user %> </li> <% end %> </ul>
gravatar_forはセクション7.6演習で作るはずだったヘルパーの関数。
作っていなかったので今追加する。
app/helpers/users_helper.rb
module UsersHelper # 与えられたユーザーのGravatar (http://gravatar.com/) を返す。 def gravatar_for(user) gravatar_id = Digest::MD5::hexdigest(user.email.downcase) gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}" image_tag(gravatar_url, alt: user.name, class: "gravatar") end # Returns the Gravatar (http://gravatar.com/) for the given user. def gravatar_for(user, options = { size: 50 }) gravatar_id = Digest::MD5::hexdigest(user.email.downcase) size = options[:size] gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}" image_tag(gravatar_url, alt: user.name, class: "gravatar") end end
スタイルも調整しておく
app/assets/stylesheets/custom.css.scss
.... /* users index */ .users { list-style: none; margin: 0; li { overflow: auto; padding: 10px 0; border-top: 1px solid $grayLighter; &:last-child { border-bottom: 1px solid $grayLighter; } } }
ヘッダーにユーザー一覧のリンクをつなげる。
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse"> <div class="navbar-inner"> <div class="container"> <%= link_to "sample app", root_path, id: "logo" %> <nav> <ul class="nav pull-right"> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Help", help_path %></li> <% if signed_in? %> <li><%= link_to "Users", users_path %></li> <li id="fat-menu" class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Account <b class="caret"></b> </a> <ul class="dropdown-menu"> <li><%= link_to "Profile", current_user %></li> <li><%= link_to "Settings", edit_user_path(current_user) %></li> <li class="divider"></li> <li> <%= link_to "Sign out", signout_path, method: "delete" %> </li> </ul> </li> <% else %> <li><%= link_to "Sign in", signin_path %></li> <% end %> </ul> </nav> </div> </div> </header>
ローカルサーバでチェック
rails server
確認用のユーザを自動登録する
Faker gemを用いて適当なユーザを100人追加する。
Gemfile
source 'https://rubygems.org' ruby '2.0.0' #ruby-gemset=railstutorial_rails_4_0 gem 'rails', '4.0.5' gem 'bootstrap-sass', '2.3.2.0' gem 'sprockets', '2.11.0' gem 'bcrypt-ruby', '3.1.2' gem 'faker', '1.1.2' ...
インストール
bundle install
rakeタスク populateを定義
lib/tasks/sample_data.rake
namespace :db do desc "Fill database with sample data" task populate: :environment do User.create!(name: "Example User", email: "example@railstutorial.jp", password: "foobar", password_confirmation: "foobar") 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.jp" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password) end end end
rakeタスクの呼び出し
bundle exec rake db:reset bundle exec rake db:populate bundle exec rake db:test:prepare
ページ分割
gemの追加、will_paginate gem とbootstrap-will_paginate gem
Gemfile
source 'https://rubygems.org' ruby '2.0.0' #ruby-gemset=railstutorial_rails_4_0 gem 'rails', '4.0.5' gem 'bootstrap-sass', '2.3.2.0' gem 'sprockets', '2.11.0' gem 'bcrypt-ruby', '3.1.2' gem 'faker', '1.1.2' gem 'will_paginate', '3.0.4' gem 'bootstrap-will_paginate', '0.0.9' ....
インストール
bundle install
will_paginateを画面に埋め込む
app/views/users/index.html.erb
<% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 52 %> <%= link_to user.name, user %> </li> <% end %> </ul> <%= will_paginate %>
これで@usersを自動的にみつけてページネーションをやってくれる。
そのためには、user.allでとっていた@usersを.paginateでとるようにする。
class UsersController < ApplicationController ... def index @users = User.paginate(page: params[:page]) end ...
リファクタリング、表示をパーシャルで作成
app/views/users/index.html.erb
<% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <% @users.each do |user| %> <%= render user %> <% end %> </ul> <%= will_paginate %>
表を作っていた部分をrenderにした。renderはuserという変数を参照している。
このときパーシャル名_user.html.erbをRailsは探すのでそれを用意してやる。
app/views/users/_user.html.erb
<li> <%= gravatar_for user, size: 52 %> <%= link_to user.name, user %> </li>
さらに先ほどのrenderはさらに簡略して
app/views/users/index.html.erb
<% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <%= render @users %> </ul> <%= will_paginate %>
とできる。
ユーザ削除
管理権限を持つユーザがリストからユーザを削除できる機能を持たせる。
管理ユーザとしてadmin属性をUserモデルに付与
usersテーブルにboolean値のadminカラムを付け加える。
マイグレーションの作成
rails generate migration add_admin_to_users admin:boolean
class AddAdminToUsers < ActiveRecord::Migration def change add_column :users, :admin, :boolean, default: false end end
デフォルトをfalseにしてデフォルトでは管理者にならないようにする。
マイグレーションの実行
bundle exec rake db:migrate bundle exec rake db:test:prepare
コンソールで確認
rails console --sandbox
>user = User.first >user.admin? => false > user.toggle!(:admin) > user.admin? => true
user_paramsメソッドにadminをいれていないのは防御上の理由。
サンプルデータに管理者を一人入れる。
lib/tasks/sample_data.rake
namespace :db do desc "Fill database with sample data" task populate: :environment do admin = User.create!(name: "Example User", email: "example@railstutorial.jp", password: "foobar", password_confirmation: "foobar", admin: true) 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.jp" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password) end end end
そしてdb更新
bundle exec rake db:reset bundle exec rake db:populate bundle exec rake db:test:prepare
destroyアクションの作成
管理者だけユーザの横に削除リンクがでるようにする。
app/views/users/_user.html.erb
<li> <%= gravatar_for user, size: 52 %> <%= link_to user.name, user %> <% if current_user.admin? && !current_user?(user) %> | <%= link_to "delete", user, method: :delete, data: { confirm: "You sure?" } %> <% end %> </li>
destroyアクションを設定
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :signed_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] .... def destroy User.find(params[:id]).destroy flash[:success] = "User destroyed." redirect_to users_url end .... end
さらに、管理者でないとdestroyできないようにガードをかける。
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :signed_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] before_action :admin_user, only: :destroy ... # Before actions ... def admin_user redirect_to(root_path) unless current_user.admin? end end
ローカルサーバで管理者が削除できることを確認
rails server