Rodhos Soft

備忘録を兼ねた技術的なメモです。Rofhos SoftではiOSアプリ開発を中心としてAndroid, Webサービス等の開発を承っております。まずはご相談下さい。

チュートリアルをやってみる。ユーザ更新編

これはチュートリアルをやってみたログである。railstutorial.jp


editアクションに編集画面をつくる。

  1. 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パーシャルも流用。
内部的にはrailsActiveRecordの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

ページ分割

Railsのwill_paginateメソッドを用いる。

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