コミュ障だから明日が僕らをよんだって返事もろくにしなかった

何かを創る人に憧れたからブログをはじめたんだと思うよ

社会のボトムズがRailsに手を出す #10

これまでのあらすじ

さて、突然ですが10回クイズの時間です。Rails って10回言ってください。えぇ…、RailsRailsRailsRailsRails...。はい、よくできました(オチなし)。

ということで今までの知識の総集編だと個人的に思ってる第10章はーじめーるよー‼︎前回はログイン機能などを作り込んでいきました。今回はログイン後のユーザー周りの編集機能を充実させていきます。

前回記事
inujini.hatenablog.com


ユーザー機能の充実化をはかるっぴ

とにもかくにもまずはガワを作ります。コントローラにeditを追加して、こんなの作ります。

f:id:andron:20190116223219p:plainf:id:andron:20190116230941p:plain

ガワ系の話、基本的にERBいじるだけだしコード載せる必要ないと思ってる。パーシャルつないでごちゃごちゃしてるけど、基本的にテンプレート行き来してるだけですし……。まあいいや、基本的な機能作ったのでテストを書きます。

参考:ERB
ビュー(view) - - Railsドキュメント

$ rails generate integration_test users_edit
# test/integration/users_edit_test.rb
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:michael)
  end

  test "編集失敗時" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    patch user_path(@user), params: { user: { name:  "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } }
    assert_template 'users/edit'
  end

  test "編集成功時" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name:  name, email: email, password: "", password_confirmation: "" } }
    assert_not flash.empty? # => validates に allow_nil: true 追加
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end

  test "フレンドリーフォワーディング" do
    get edit_user_path(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user)
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name:  name, email: email, password: "", password_confirmation: "" } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end  
end
# test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:michael)
    @other_user = users(:archer)
  end
  
  test "ログインしてない時にindexをリダイレクト" do
    get users_path
    assert_redirected_to login_url
  end  

  test "ログインしてない時にeditをリダイレクト" do
    get edit_user_path(@user)
    assert_not flash.empty?
    assert_redirected_to login_url
  end

  test "ログインしてないときにupdateをリダイレクト" do
    patch user_path(@user), params: { user: { name: @user.name, email: @user.email } }
    assert_not flash.empty?
    assert_redirected_to login_url
  end

  test "不正なユーザでのログインのときにeditをリダイレクト" do
    log_in_as(@other_user)
    get edit_user_path(@user)
    assert flash.empty?
    assert_redirected_to root_url
  end

  test "不正なユーザでのログインのときにupdateをリダイレクト" do
    log_in_as(@other_user)
    patch user_path(@user), params: { user: { name: @user.name, email: @user.email } }
    assert flash.empty?
    assert_redirected_to root_url
  end

  test "Web経由でのadmin属性の変更を禁止" do
  # 演習
    log_in_as(@other_user)
    assert_not @other_user.admin?
    patch user_path(@other_user), params: { user: { password: "password", password_confirmation: "password", admin: true } }
    assert_not @other_user.reload.admin?
  end
end

テストはこんなんで。カジュアルに日本語表記しているけど、多分よろしいやり方でないから真似しないで*1。テストの方もだいぶ肥大化してきたけど、このままべた書きするのとまとめるのどっちが良いんかね。まあ、多分僕はまとめるとわけわかんなくなるけどもね……。


そんで更新とか追加とかやって機能面はこんな感じに落ち着きます。

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  before_action :admin_user,     only: :destroy
  ### 略 ###
  def create
    @user = User.new(user_params)
    if @user.save
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  def edit
    @user = User.find(params[:id])
  end

  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

  ### ユーザ削除 ###
  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_to users_url
  end


  private
    def user_params
      params.require(:user).permit(:name, :email, :password,:password_confirmation)
    end
    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end    

    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end

    # 管理者かどうか確認
    def admin_user
      redirect_to(root_url) unless current_user.admin?
    end
end

before_actionってのを使うとユーザ権限ごとに良い感じに行動を振り分けられるとのこと。なんかCSS!important味を感じる。

とまあそんな感じで、ユーザの編集機能ができます。新たな学びがあるとしたらbefore_actionでフィルタリングできるとかその辺?


ユーザリストをつくるっぴ

今度はユーザの全リストをつくります。Ruby on Rails の醍醐味である「詳しい動作はよくわかんないがGemインスコしておけば動くサービスがつくれる」を体感できます。てなわけで以下を入れます。一応Gemのリンク貼っておきます。詳しい使い方はリンク先の「Homepage」ってところに飛べば載ってたりします。

faker
faker | RubyGems.org | your community gem host
ダミーの名前をつくってくれるやつ。

# db/seeds.rb
### 略
99.times do |n|
  name  = Faker::Name.name # Faker
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name, email: email, password: password, password_confirmation: password)
end

使用方法は上のような感じだそうです*2Faker::Internet.emailなんてのも用意されているからemailもつくれちゃう。


will_paginate
will_paginate | RubyGems.org | your community gem host
ページネーションを実装するやつ。

$ rails console
>> User.paginate(page: 1)

使用方法は上のような感じです。


bootstrap-will_paginate
bootstrap-will_paginate | RubyGems.org | your community gem host
Bootstrap用ページネーションを生成してくれるやつ。内部動作はどうなっているのかしらない。Bootstrap4系で確認する場合は以下のように使えばいけるらしい。

<%= will_paginate(@things, :renderer => WillPaginate::ActionView::Bootstrap4LinkRenderer) %>


そんで全部実装したらこう!!
f:id:andron:20190117005443p:plain
まじで何もコードいじらない。jQueryプラグイン感覚で使える!!


そんでテストはこう!!

$ rails generate integration_test users_index
# test/integration/users_index_test.rb
require 'test_helper'
class UsersIndexTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:michael)
  end
  test "index including pagination" do
    log_in_as(@user)
    get users_path
    assert_template 'users/index'
    assert_select 'nav.pagination', count: 2 #Bootstrap4仕様
    User.paginate(page: 1).each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
    end
  end
end

Bootstrap4仕様でやると生成タグも変化するんですね……。assert_selectでの判定結構ボロボロだ。


とりあえずユーザリストの生成はそんな感じでできます。やってみた学びとしてはGem便利ってのとページネーションの使い方ググっても「Kaminari」薦める人間しかいないせいで公式ドキュメントしか参考にならないってことがわかった。


ユーザの削除機能つくるっぴ

最後は削除機能を実装します。まずは削除用の管理ユーザを追加します。今更この機能を追加されると行き当たりばったり感がしてならない……。

$ rails generate migration add_admin_to_users admin:boolean
$ rails db:migrate
# app/views/users/_user.html.erb
<!-- 管理者のみ対応 -->
  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, method: :delete, data: { confirm: "You sure?" } %>
  <% end %>

削除ボタンは上みたいな感じで、そんでコントローラは一番上の方で書いたようにdestroyを追加します。

そうするとこんな機能追加できます。
f:id:andron:20190117025933p:plain


テストは以下。

# test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end
  ### 略 ###
  test "ログインしてないときにdestroyをリダイレクト" do
    assert_no_difference 'User.count' do
      delete user_path(@user)
    end
    assert_redirected_to login_url
  end

  test "管理者でないログインのときにdestroyをリダイレクト" do
    log_in_as(@other_user)
    assert_no_difference 'User.count' do
      delete user_path(@user)
    end
    assert_redirected_to root_url
  end
end
# test/integration/users_index_test.rb
require 'test_helper'
class UsersIndexTest < ActionDispatch::IntegrationTest
  def setup
    @admin     = users(:michael)
    @non_admin = users(:archer)
  end

  test "管理者のindexではページネーションと削除リンクを含む" do
    log_in_as(@admin)
    get users_path
    assert_template 'users/index'
    assert_select 'nav.pagination'
    first_page_of_users = User.paginate(page: 1)
    first_page_of_users.each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
      unless user == @admin
        assert_select 'a[href=?]', user_path(user), text: 'delete'
      end
    end
    assert_difference 'User.count', -1 do
      delete user_path(@non_admin)
    end
  end

  test "非管理者のindex" do
    log_in_as(@non_admin)
    get users_path
    assert_select 'a', text: 'delete', count: 0
  end
end

結合テストってここまでがっつりいるんですかね?そしてテストに関しては割といわれるがままに書いていて応用できる技術が身についたって感じがしない。まあ、それでも僕はカバレッジ100%のテストコード書ける自信ありますけどね(不正はない。いいね?)。

第十章の感想

すげーボリュームになった。ところどころRails分かった気になって内容に文句言ってたりするけども、実際のところ僕はフレームワークの機能なんて全部わかんないです(爆)。ぶっちゃけメジャーアップデートのたびに機能覚えなおしなんで大雑把に使えますでいいでしょとか思ってる。

内容としては、ユーザ画面での更新、削除などなどの仕組みを実装したのと、管理者権限の実装とそのテストの仕方とかです。ボリュームの割には新しいことやっている実感はあんまりない…。


まあそんな感じ。次回へ続く。

*1:それで動くRailsが悪いんだ僕は悪くない

*2:本番用にseeds.rbを利用、確認だけのデータならfixtureで