hiyoko-programingの日記

プログラミングを勉強したてのひよっ子。   エンジニア目指して勉強中。

映画レビューサイトのコード その2

STEP1:レビューの評価を星として表示

現在、レビューの評価がどこにも表示されていない。moooviではレビューの評価を星として表示する。

レビューの評価を表示させる画面はトップページ作品ページの2つ。

星の数はそれぞれ画像が用意されており、以下のようにして表示することができる。
トップページであるindex.html.erb、作品ページであるshow.html.erbを確認する。

index.html.erb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    <div id="main_cnt_wrapper">
    <div id="yjContentsBody">
      <div class="yjContainer">
        <span class="yjGuid"><a id="yjContentsStart" name="yjContentsStart"></a><img alt="ここから本文です" height="1" src="http://i.yimg.jp/yui/jp/tmpl/1.1.0/audionav.gif" width="1"></span>
        <div id="yjMain">
          <article class="section">
            <div class="container">
              <header class="header header--section">
                <h2 class="text-middle">
                  <i class="icon-movie color-gray-light"></i>新着作品
                </h2>
              </header>
              <ul class="thumbnails thumbnail--movies row grid4 js-lazy-load-images js-my-check-stats" id="list-module">
                <% @products.each do |product| %>
                  <li class="col">
                    <a href="/products/<%= product.id %>"><div class="thumbnail__figure" style="background-image:url(<%= product.image_url %>)"></div></a>
                    <div class="thumbnail__caption">
                      <h3 class="text-xsmall text-overflow" title="<%= product.title %>">
                        <%= product.title %>
                      </h3>
                      <p class="text-small">
                        <span class="rating-star">
                          <i class="star-actived rate-[ここに評価を表示]0"></i>
                        </span>
                      </p>
                    </div>
                  </li>
                <% end %>
              </ul>
            </div>
          </article>
        </div>
        <div id="yjSub">

星の数はi要素のクラスのrate-[ここに評価を表示]0と対応している。[ここに評価を表示]には評価の数字(1~10)が入る。各評価の星の数は以下の通り。

クラス名 星の数
rate-10
rate-20
rate-30
rate-40
rate-50
rate-60
rate-70
rate-80
rate-90
rate-100

それぞれのhtml.erbファイルにおいて、rate-[ここに評価を表示]0の[ここに評価を表示]に評価の数値が入るようにしていく。

 作業内容

1.個別の作品ページでレビューの星を表示する
2.映画一覧ページでそれぞれの映画の平均評価を取得する
3.product.rbにインスタンスメソッドを定義する

1.個別の作品ページでレビューの星を表示

作品ページでは、アソシエーションを用いることでコントローラで定義している@productから関連するreviewsテーブルのレコードを全て取得している。reviewsテーブルのレコードは、評価の数値が入るカラム、rateカラムを持っているので、それをそのまま取得し、先ほどの例の[ここに評価を表示]の部分で利用すれば良い。

app/views/products/show.html.erb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
  <div id="main_cnt_wrapper">
    <div id="yjContentsBody">
      <div class="yjContainer">
        <span class="yjGuid"><a id="yjContentsStart" name="yjContentsStart"></a><img alt="ここから本文です" height="1" src="http://i.yimg.jp/yui/jp/tmpl/1.1.0/audionav.gif" width="1"></span>
        <div id="yjMain">
          <article class="section">
            <div class="container">
              <header class="header header--section">
                <h2 class="text-middle">
                  <i class="icon-movie color-gray-light"></i><%= @product.title %>
                </h2>
              </header>
              <p style="text-align: center">
                <img src="<%= @product.image_url %>" alt="<%= @product.title %>">
              </p>
              <div style="text-align: right">
                <a href="/products/<%= @product.id %>/reviews/new">この作品を投稿する</a>
              </div>
              <header class="header header--section">
                <h2 class="text-middle">
                  <i class="icon-movie color-gray-light"></i>みんなのレビュー
                </h2>
              </header>
              <ul style="padding: 0">
                <% @product.reviews.each do |review| %>
                <li style="border-bottom: dotted 1px">
                  <div class="thumbnail__caption">
                    <h3 class="text-xsmall text-overflow" title="<%= review.nickname %>">
                      <%= review.nickname %>
                    </h3>
                    <p class="text-small">
                      <span class="rating-star"><i class="star-actived rate-<%= review.rate %>0"></i></span>
                    </p>
                    <p>
                      <%= review.review %>
                    </p>
                  </div>
                </li>
                <% end %>
              </ul>
            </div>
          </article>
        </div>
        <div id="yjSub">

作品詳細ページで、星がきちんと表示されていることを確認する。

 

2.映画一覧ページでそれぞれの映画の平均評価を取得する

トップページに表示されている作品については、その作品についたレビュー全ての評価の平均を表示する。

 ActiveRecord Relationクラス

whereメソッドやアソシエーションを利用してDBから複数のレコードをインスタンスとして取得した場合、取得した配列はActiveRecord Relationクラスに属す。

1
2
  products = Product.all
  products.class #=> ActiveRecord::Relation::ActiveRecord_Relation_Product

classメソッドを利用すると、利用したインスタンスが属するクラスを知ることができる。(インスタンス・メソッド/クラス・メソッドの区分で言うクラス・メソッドではない。classというメソッド。)

ActiveRecord Relationクラスには、whereを始めとして複数のメソッドが準備されているため、続けてメソッドを実行することができる。

1
2
   #productsテーブルから最新の投稿を5件取得する
  products = Product.order("id DESC").limit(5)

これは、複数のレコードのインスタンスが格納されている配列のようなものなので、配列クラスのメソッドを利用することができる。

1
2
3
4
5
6
  #productsテーブルから最新の投稿を5件取得する
  products = Product.order("id DESC").limit(5)
  #productのtitleカラムの値を出力する処理を、eachメソッドで行う
  products.each do |product|
    puts product.title
  end

なお、whereメソッドやアソシエーションで、DBに当てはまるレコードが存在しない場合、返り値は空の配列になる。

 averageメソッド

averageメソッドは、ActiveRecord_Relationクラスのメソッド。averageメソッドは、averageメソッドを利用するインスタンス取得先のテーブルのカラムをシンボル型で引数にとる。その値の平均を、小数点ありの状態で返してくれる。


例えば、生徒の得点を記録するscoreカラムを持ったstudentsテーブルと関連するStudentクラスがあったとする。scoreカラムの平均を求めるには、以下のようにする。

1
2
3
  students = Student.all
  students.average(:score)
  #=> 小数点まで含んだ平均点

 roundメソッド

小数点ありの数字クラスのインスタンスが利用できる。利用した数字の小数点以下を四捨五入する。

1
2
3
4
5
  10.4.round
  #=> 10

  10.5.round
  #=> 11

例えばトップページのビュー(index.html.erb)で以下のようにすると、各作品についてレビューの個数を取得することができる。

1
2
3
4
5
  <% @products.each do |product| %>
  #(中略)
    <%= product.reviews.count %>
  #(中略)
  <% end %>

今回eachメソッドを利用しているのは作品の配列である。作品の平均評価は、作品のインスタンスから直接取得することはできない。

 問題1:トップページで各作品の平均評価点を出す

作業ファイル:
app/views/products/index.html.erb
ヒント①:まずはプロダクトに関するレビュー全てを取得し、その後評価の平均を求める
ヒント②:プロダクトに関するレビューがない場合を想定し、if文を用いて星を出す部分のクラス名が「rate-0」となっているビューに分岐する処理をする。そのために、配列の中身が空か判定しtrue/falseを返すメソッド present? を利用する
【例】
1
2
    ary = []
    ary.present? #=> false

作品一覧ページで星がきちんと表示されていることを確認。

 

3.product.rbにインスタンスメソッドを定義

2.までの作業で、productの平均評価を表示することができた。しかし、ビューファイルに長いロジックを書いてしまうとコードが読みづらくなってしまい、あとで管理が大変なので避けたほうが良い。そこで、この処理をまとめてインスタンスメソッドにする。

今回こちらのメソッドを利用しているのはProductクラスインスタンスである。そこで、上記の処理はProductクラスインスタンス・メソッドとして定義する。

 レシーバ

インスタンスメソッドを利用するインスタンス自身のこと。例えば以下のようなコードがあった場合、3行目の式におけるレシーバはstr

1
2
3
  str = "3"
  #以下の式のレシーバはstr
  str.to_i #=> 3

 self

インスタンスメソッドの中でselfと書くと、そのメソッドを利用したレシーバ自身が代入された変数のように扱うことができる。

Integerクラスに定義されているインスタンスメソッド、odd?を例に考えてみる。

odd?
レシーバが奇数かどうかを判定してtruefalseを返してくれるインスタンスメソッド。判定のためにはレシーバを利用した式が必要なので、odd?を自分で作るとすれば、以下の例のような実装になるる。selfを使ってレシーバ自身をメソッドの中で利用している。

sample
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  class Integer
    def odd?
      if self % 2 == 1
        return true
      else
        return false
      end
    end
  end
  2.odd? #=> false(%は剰余を求めるもので、余りが1ではないのでfalseが返ってくる)

上記の例では、レシーバである2がodd?メソッドの中でselfに代入され、判定が行われた結果返り値がfalseになっている。

今回index.html.erbに記述した処理をそのままProductクラスに移動しただけではエラーが起きてしまう。そこで、selfを利用しレシーバ自身であるproductインスタンス・メソッドの中に呼んであげる。

app/models/product.rb
1
2
3
4
5
6
7
class Product < ApplicationRecord
  has_many :reviews

  def review_average
    self.reviews.average(:rate).round
  end
end

さらに、selfは実は省略することが可能。上記の式を省略すると、以下のようになる。

product.rb
1
2
3
4
5
6
7
class Product < ApplicationRecord
  has_many :reviews

  def review_average
    reviews.average(:rate).round 
  end
end

だいぶスッキリとした式になる。

続いて、定義したメソッドを実際にビューで利用する。

app/views/products/index.html.erb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
    <div id="main_cnt_wrapper">
    <div id="yjContentsBody">
      <div class="yjContainer">
        <span class="yjGuid"><a id="yjContentsStart" name="yjContentsStart"></a><img alt="ここから本文です" height="1" src="http://i.yimg.jp/yui/jp/tmpl/1.1.0/audionav.gif" width="1"></span>
        <div id="yjMain">
          <article class="section">
            <div class="container">
              <header class="header header--section">
                <h2 class="text-middle">
                  <i class="icon-movie color-gray-light"></i>新着作品
                </h2>
              </header>
              <ul class="thumbnails thumbnail--movies row grid4 js-lazy-load-images js-my-check-stats" id="list-module">
                <% @products.each do |product| %>
                <li class="col">
                  <a href="/products/<%= product.id %>"><div class="thumbnail__figure" style="background-image:url(<%= product.image_url %>)"></div></a>
                  <div class="thumbnail__caption">
                    <h3 class="text-xsmall text-overflow" title="<%= product.title %>">
                      <%= product.title %>
                    </h3>
                    <p class="text-small">
                      <% if product.reviews.present? %>
                      <span class="rating-star">
                        <i class="star-actived rate-<%= product.review_average %>0"></i>
                      </span>
                      <% else %>
                      <span class="rating-star">
                        <i class="star-actived rate-0"></i>
                      </span>
                      <% end %>
                    </p>
                  </div>
                </li>
                <% end %>
              </ul>
            </div>
          </article>
        </div>
        <div id="yjSub">

この段階で、2. の作業終了時と同じように平均の値が星の数で表示されていれば成功。

  

STEP2:ランキング機能を実装

見本となるmoooviのサイトを確認してみると、トップページの右側に投稿ランキングが表示されている。

このランキングは投稿数が多いものを上から順にトップ5で表示している。ランキングを取得して表示させる。

 作業内容

1.before_actionを設定する
2.ランキングを表示させる
3.productsテーブルから、レビュー数が多い順に5件レコードを取得する

1.before_actionを設定する

ランキングの条件は「投稿数の多いものから順番に」「上から5件取得」の2つ。ランキングが表示されるのはすべての画面。つまり、すべてのコントローラのアクションでランキングの情報を取得しなければならない。
こういった場合、ランキングを取得する処理をbefore_actionで記述する。

 before_action

あるコントローラのすべてのアクションで実行の前に共通の処理を行いたいときがある。before_actionを使用すると全てのアクションが実行される前に指定したメソッドを呼び出すことができる。

1
2
  class コントローラ名 < ApplicationController
    before_action :処理させたいメソッドの名前

例えば、ProductsControllerという名前のコントローラで、毎回インスタンス変数@page_titleに「作品ページ」という文字列を代入する処理を各アクションの前に実行したいとする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  class ProductsController < ApplicationController
    before_action :configure_title

    def index
      @products = Product.all
    end

    def configure_title
      @page_title = '作品ページ'
    end
  end

このソースコードではProductsControllerのindexアクションが呼ばれる前に、configure_titleが実行される。さらにindexアクションだけでなく、ProductsControllerの他すべてのアクションでconfigure_titleが最初に呼ばれる。

before_actionの特徴
① before_actionを書いたコントローラのすべてのアクションの前に処理を行える
② before_actionを書いたコントローラで共通の処理を行える


ではどのコントローラにbefore_actionを書けばいいのか。今回ランキングを表示するのはいまのところはすべてのビューである。よって、ProductsControllerReviewsControllerの2つに書けばいいがこれら2つのコントローラはRankingControllerを継承している

 コントローラの継承

コントローラは別のコントローラを継承することができる。継承をすると継承元のコントローラの持つメソッドや特徴を引き継ぐことができる。

コントローラの継承はコントローラの定義で以下のように書く。

1
2
class コントローラ < 継承元のコントローラ
end

例えば、AnimalControllerを継承したDogControllerをつくる場合は以下のようになる。

1
2
class DogController < AnimalController
end

このとき、AnimalControllerでbefore_actionを以下のように定義しているとする。

1
2
3
4
5
6
7
class AnimalController < ApplicationController
  before_action :say_hello

  def say_hello
    puts "Hello Animal"
  end
end

すると、AnimalControllerを継承したDogControllerのすべてのアクションの前にもbefore_actionのsay_helloメソッドが呼ばれる。

1
2
3
4
5
6
7
class DogController < AnimalController
  def show
    @dog = Dog.find(params[:id])
  end
end

# => showアクションの前にsay_helloが呼ばれる

すべてのコントローラで共通のbefore_actionを定義したい場合はApplicationControllerに記述する。これは、すべてのコントローラ(ApplicationController以外)がApplicationControllerを継承しているため。

今回はRankingControllerにランキングを取得するbefore_actionを記述する。
RankingControllerを作らず直接ApplicationControllerを作ればいいのでは?、と思うかもしれないが、あくまでランキングを表示したいのはトップページと作品の個別ページで、この後マイページを実装していくが、ApplicationControllerに記載するとマイページにもランキングが表示されてしまう。よって、RankingControllerを作成し、ランキングを表示させたいReviewsControllerとProductsControllerを継承させている。

https://tech-master.s3.amazonaws.com/uploads/curriculums//542bb99f3f1317966994be60bb9921fc.png

「投稿数が多い順に」という部分はこのあと実装するので、今回はProductsテーブルに入ってる作品を5件取得して変数@rankingに入れておく。

app/controllers/ranking_controller.rb
1
2
3
4
5
6
7
class RankingController < ApplicationController
  layout 'review_site'
  before_action :ranking
  def ranking
    @ranking = Product.limit(5)
  end
end

limitメソッドもorderメソッドと同じように、allメソッドを省略することができる。 

要点チェック

 

2.ランキングを表示させる

先ほど定義した変数@rankingをビューに反映させる。どのファイルにランキングのHTMLが記述されているのか。ランキングの情報を取得するbefore_actionはRankingControllerに実装した。RankingControllerの2行目に、

ranking_controller.rb
1
2
3
4
5
6
7
class RankingController < ApplicationController
  layout 'review_site'
  before_action :ranking
  def ranking
    @ranking = Product.limit(5)
  end
end

layout 'review_site'という記述がある。これはビューのレイアウトファイルを指定するもの。

 レイアウトファイル

レイアウトファイルとはapp/views/layouts/の下に入っているHTMLファイル。レイアウトファイルはURLにアクセスして対応するコントローラが呼ばれたあと、最初に表示されるHTMLのこと。実は今まで修正していたshow.html.erbなどのファイルはレイアウトファイルの中に呼び出されている。

  •  app
    •  views
      •  layouts

 layout 'レイアウトファイル名'

コントローラ内でlayout 'レイアウトファイル名'と書くと、そのコントローラでのアクションが呼ばれたあと表示するビューのレイアウトファイルを指定できる。

1
2
3
4
5
6
7
class MovieController < ApplicationController
  layout 'movie'

  def index
    @movies = Movie.all
  end
end

例えばこのように指定すると、MovieControllerのindexアクションが呼ばれたときに表示されるレイアウトはmovie.html.erbとなる。

 なにも指定しないとレイアウトファイルはapplication.html.erbとなる。


今回、RankingControllerではlayout 'review_site'が指定されているので表示されるレイアウトファイルはreview_site.html.erbになる。

  •  app
    •  views
      •  layouts
        •  review-site.html.erb

review_site.html.erbの29行目

29
<%= yield %>

<%= yield %>は外部のHTMLファイルを読み込むためのerb記法。ここに今まで修正していたshow.html.erbなどのHTMLがアクションごとに差し込まれる。

review_site.html.erbを修正して、ランキングが表示されるようにする。

review_site.html.erb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<!DOCTYPE html>
<html class="pc" lang="ja" xmlns:fb="http://ogp.me/ns/fb#" xmlns:og="http://ogp.me/ns#">
<head>
  <meta charset="utf-8">
  <title>映画レビューサイト</title>
  <link href='http://fonts.googleapis.com/css?family=Signika:700,300,400,600' rel='stylesheet' type='text/css'>
  <%= stylesheet_link_tag    "application", media: "all", "data-turbolinks-track" => true %>
  <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
  <%= csrf_meta_tags %>
</meta>
</head>
<body class="yj950-2">
  <div id="wrapper">
    <div id="yjContentsHeader">
      <nav class="globalnav">
        <div class="globalnav__menu">
          <ul class="gmenu">
            <li class="logo" style="float: left">
              <a href="/">mooovi</a>
            </li>
            <li class="entry_button" style="float: right">
              <a href="/products/search">投稿する</a>
            </li>
          </ul>
        </div>
      </nav>
    </div>
    <div class="bgcolor-white pt1em pb1em" id="contents">
      <%= yield %>
      <aside class="section">
        <h4 class="text-small hr-bottom--thin no-space-bottom">
          <i class="icon-crown color-gray-light"></i>投稿ランキング
        </h4>
        <ul class="listview listview--condensed text-small">
          <% @ranking.each.with_index(1) do |product, i| %>
          <li data-cinema-id="346394">
            <a href="/products/<%= product.id %>">
              <div class="box">
                <div class="box__cell w40 align-center">
                  <p class="label bgcolor-gray-lighter align-center">
                    <%= i %>
                  </p>
                </div>
                <div class="box__cell pl1em">
                  <p class="text-xsmall no-space">
                    <%= product.title %>
                  </p>
                  <img src="<%= product.image_url %>" alt="">
                </div>
              </div>
            </a>
          </li>
          <% end %>
        </ul>
      </aside>
    </div>
  </div>
</div>
</div>

<div class="copyright">
  Copyright (C) 2015  XXX Corporation. All Rights Reserved.
</div>
</div>
</div>
</body>
</html>

@rankingはActiveRecordRelationクラスのインスタンス。なので、配列クラス(Arrayクラス)に定義されているメソッドを利用することができる。ランキングの変数@rankingに対して繰り返し処理を行って表示をしている。

35行目の内容に、each.with_index という記述がある。こちらでは、eachメソッドに with_indexメソッドをあわせて使っている。 eachメソッドと with_indexメソッドを併用すると、要素の数だけブロックを繰り返し実行し、繰り返しごとに | で囲われている部分の i に番号が入る。デフォルトでは、iには0から入る。今回は with_index(1)と引数を渡した事で、1から番号が入った。

each.with_index使用例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  array = ["abe", "takahashi", "hirata"]

  array.each.with_index do |name, i|
    puts i
    puts name
  end
  #=>(実行結果)
  0
  abe
  1
  takahashi
  2
  hirata

  array.each.with_index(5) do |name, i|
    puts i
    puts name
  end
  #=>(実行結果)
  5
  abe
  6
  takahashi
  7
  hirata

 要点チェック

 

3.productsテーブルから、レビュー数が多い順に5件レコードを取得する

RankingControllerで取得したランキングの情報@rankingは今はProductsテーブルから5件取得しているだけ。ランキングとして、レビューの投稿数が多い作品を5件取得する。
ここはActiveRecordを上手く使わないと実装できない。必要な処理は以下。

① Reviewsテーブルのレコードをproduct_idごとにまとめて、数の多い5件を取得する

② 取得した5件のReviewsテーブルのproduct_idを取得する

③ 取得したproduct_id(5件分)と同値のidを持つproductsテーブルのレコードをそれぞれ取得する

①の「Reviewsテーブルのレコードをproduct_idごとにまとめて、数の多い5件を取得する。」がもっとも難しい処理。Reviewモデルはどの作品のレビューかわかるようにカラムとしてproduct_idを持っている。レビューの投稿数が多いとはつまり、product_idが同じReviewsテーブルのレコードの数が多い、ということになる。

そこでまずは、Reviewsテーブルのレコードをproduct_idでまとめる。あるテーブルを特定のカラムでまとめるにはgroupメソッドを使う。

 groupメソッド

groupメソッドはテーブルのレコードを指定したカラムでまとめることができる。以下のように使う。

1
モデル.group(カラム名)

groupメソッドを使うと出力は以下のようになる。

terminal
1
2
3
4
5
Review.group(:product_id)
  Review Load (10.5ms)  SELECT `reviews`.* FROM `reviews` GROUP BY product_id
=> [#<Review id: 1, nickname: "まいき", rate: 1, review: "おもしろい", product_id: 21, created_at: "2014-11-05 16:03:39", updated_at: "2014-11-05 16:03:39", user_id: 4>,
  #<Review id: 3, nickname: "えいちゃん", rate: 10, review: "感動した!また見たい", product_id: 22, created_at: "2014-11-05 16:03:39", updated_at: "2014-11-05 16:03:39", user_id: 4>,
  #<Review id: 13, nickname: "ごとう", rate: 10, review: "思っていたより良かった", product_id: 23, created_at: "2014-11-05 16:03:39", updated_at: "2014-11-05 16:03:39", user_id: 4>

groupメソッドを使うと、指定したカラムでレコードがまとめられる。まとめられたレコードの内、idが一番小さいレコードの1件だけが表示されているが、プログラムが実行されている裏側ではすべてのレコードが指定したカラムでまとめられている。現在は、それぞれのまとまりが具体的に何個あるのかはわからない。

https://tech-master.s3.amazonaws.com/uploads/curriculums//108191458f6c7e304a8f8e4d1f2aae9d.png

この状態でcountメソッドを使うとgroupメソッドでまとめられたレコードの数が取得できる。

 countメソッド

countメソッドは配列などの要素数を返すメソッド。groupメソッドに続けて使うとまとめられたそれぞれのレコードの数が取得できる。

terminal
1
2
Review.group(:product_id).count
=> {21=>2, 22=>1, 23=>4}

ハッシュが返ってきているのがわかる。このハッシュのキーはgroupメソッドで指定したカラムの値。この例ではproduct_idになる。ハッシュの値はgroupメソッドでまとめられたレコード数である。

例ではproduct_idが21のレコードが2つ、product_idが22のレコードが1つ、product_idが23のレコードが4つということになる。


この状態で、レコード数でソートしたい。ソートにはorderメソッドを使う。

 モデル.group(カラム名).countはハッシュが返ってくるのでcountメソッドより前にorderメソッドを使う。

terminal
1
2
Review.group(:product_id).order('product_id DESC').count
=> {23=>4, 22=>1, 21=>2}

この書き方ではproduct_idでソートされるが、レコードの数ではソートされていない。groupメソッドでまとめたレコードの数でソートするには以下のように書く。

terminal
1
2
Review.group(:product_id).order('count_product_id DESC').count(:product_id)
=> {23=>4, 21=>2, 22=>1}

 order('count_カラム名').count(カラム名)

countメソッドの引数にカラム名を指定することができる。するとorderメソッドでcount_カラム名でのソートが可能となる。これはそのカラムを持つレコードの数でソートするという意味。

つまり上の例では、product_idでまとめたレコードをレコード数でソートして、カラム名とレコード数のハッシュで返す、という処理になっている。

取得したいのは5件なので、limit(5)を付け加える。limitメソッドは複数のレコードの配列のような形であるActiveRecord::Relationに対するメソッド。count(:product_id)の時点ではハッシュになっているため、その後に付け加えるとハッシュに対してlimitメソッドを実行することになりエラーが起こる。そのため、その直前に付け加え、以下のようにする。

terminal
1
2
Review.group(:product_id).order('count_product_id DESC').limit(5).count(:product_id)
=> {23=>4, 21=>2, 22=>1}

・product_idでまとめたレコードをレコード数でソート

カラム名とレコード数のハッシュで返す

 

最後に、並び替えたidだけが入った配列を生成する。

 keysメソッド

ハッシュはkeysというメソッドを持っている。これはハッシュのキーだけを取り出して配列として返すメソッド。

terminal
1
2
Review.group(:product_id).order('count_product_id DESC').limit(5).count(:product_id).keys
=> [23, 21, 22]

以上でproduct_idの配列を投稿数が多い順に取得できる。


product_idの配列からidに該当するProductsテーブルのレコードを取得するにはどうすればいいのか。

まず、思いつくのはwhereメソッドである。whereメソッドはカラムと値を指定して、指定した値のカラムを持つレコードを取得する。このとき、渡す値は配列でも問題ない。
そこで、Productsテーブルのidの配列idsを使ってwhereしてみる。

terminal
1
2
3
4
5
  ids = [1, 2, 3] # product_idの配列
  products = Product.where(id: ids)
  => [#<Product id: 1, title: "美女と野獣">,
      #<Product id: 2, title: "アオハライド">,
      #<Product id: 3, title: "ホビット 決戦のゆくえ">]

これで該当するidの値がすべて取得できた。
しかし、ここで1つ問題がある。それは取得したレコードの順番である。
以下のような配列idsでwhereしたとき、レコードはどのような順番で取得できるのか。

terminal
1
2
3
4
5
  ids = [3, 1, 2] # Productsテーブルのidの配列
  products = Product.where(id: ids)
  => [#<Product id: 1, title: "美女と野獣">,
      #<Product id: 2, title: "アオハライド">,
      #<Product id: 3, title: "ホビット 決戦のゆくえ">]

出力結果は、配列idsを上のids = [1, 2, 3]とした場合と、いまのids = [3, 1, 2]の出力結果は同じ。これはwhereで値を配列にした場合、並びがid順になってしまうため。

せっかくProductモデルのidをレビューの投稿が多い順に取得したのに、これでは意味がない。

  • 期待した出力結果
terminal
1
2
3
  => [#<Product id: 3, title: "ホビット 決戦のゆくえ">,
      #<Product id: 1, title: "美女と野獣">,
      #<Product id: 2, title: "アオハライド">]
  • whereメソッドでの出力結果
terminal
1
2
3
  => [#<Product id: 1, title: "美女と野獣">,
      #<Product id: 2, title: "アオハライド">,
      #<Product id: 3, title: "ホビット 決戦のゆくえ">]

この問題を解決するためにmapメソッドを使用する。

 mapメソッド

mapメソッドは配列オブジェクトのインスタンスメソッド。mapメソッドは配列の中身を1つずつ取り出してブロックという構文を繰り返し実行する。そして、ブロックの返り値を集めた新しい配列を作成する。

mapメソッドは配列オブジェクトに対して以下のように使用する。

1
2
3
  配列オブジェクト.map {|ele| ブロックの処理}
  # eleには配列の要素が1つずつ代入される
  # ブロックの処理は配列の要素の数だけ繰り返し実行される


配列に入っている全ての数値を2乗した新しい配列を取得したい場合、mapを使うと以下のように書ける

1
2
3
4
  numbers = [2, 5, 9]
  squares = numbers.map {|number| number * number}
  p squares
  => [4, 25, 81]

ちゃんと配列numbersの各値が2乗された数値が配列squaresに代入されている。

これはmapメソッドによって以下のように処理が行われたため。

mapメソッド

mapメソッドを使うと配列オブジェクトの各要素を使って新しい配列を生成することができる。product_idの配列に対してmapメソッドを使い、Productsテーブルからレコードを取得するのは以下のような方法になる。

terminal
1
2
3
4
5
  ids = [3, 1, 2] # Productsテーブルのidの配列
  products = ids.map {|product_id| Productsテーブルからidがproduct_idのレコードを取得する}
  => [#<Product id: 3, title: "ホビット 決戦のゆくえ">,
      #<Product id: 1, title: "美女と野獣">,
      #<Product id: 2, title: "アオハライド">]

mapメソッドを使うと順番はそのままでProductsテーブルからレコードを取得することができる。こちらのブロックの処理ではfindメソッドを使い、Productsテーブルからidがproduct_idのレコードを一本ずつ取得する。

これらをヒントにランキング機能を実装する。

 問題2:レビューの投稿が多い作品を上から順に5件取得し、ランキングの変数@rankingに代入する

作業ファイル:app/controllers/ranking_controller.rb
ヒント①:まず、レビュー数の多いproductのid上位5つが、多い順に並んだ配列を用意する
ヒント②:次にmapメソッドを利用し、配列の中身をProductクラスのインスタンスに変換する

画面右側に、ランキング機能が実装されていることを確認。

 

要点チェック

 

STEP3:deviseを使ってユーザーのサインアップ画面を作る

いまのmoooviにはユーザーという概念がない。自分で書いたレビューをあとで見られるように、サインアップ画面をつくってユーザーを生成するようにする。

サインアップ画面とユーザーを作るには、PicTweetでも使ったgemのdeviseを利用。

 作業内容

1.deviseのファイルをインストールする
2.必要なファイルを入れ替える
3.ユーザーのモデルを作成する

1.deviseのファイルをインストールする

まずはdeviseのgemをインストールする必要がある。

deviseをインストールするために、Gemfileの最終行に以下のように追。

Gemfile
1
2
(省略)
  gem 'devise'  # 最終行に追記してください

 bundle installコマンドを実行

terminal
1
2
3
$ pwd
#/Users/ユーザー名/projects/moooviであることを確認
$ bundle install

問題なくdeviseのgemがインストールできたらdeviseのセットアップをする。

まずは、deviseを使うのに必要なファイルを生成。

 rails g devise:installコマンドをターミナルで実行

terminal
1
$ rails g devise:install

次にdeviseのサインアップやログインのviewファイルを生成。

 rails g devise:viewsコマンドをターミナルで実行

terminal
1
$ rails g devise:views

これでdeviseのviewファイルが生成できた。以下のようにapp/viewsディレクトリの下にdeviseというディレクトリが生成されていれば成功。

  •  app
    •  views
      •  devise

2.必要なファイルを入れ替える

deviseで生成されるファイルのうち2つのファイルを準備のときにダウンロードしたファイルと置き換えましょう。

 

置き換えるファイルは以下の2つのファイルです。

  • app/views/devise/registrations/new.html.erb
  • app/views/devise/sessions/new.html.erb

 registrationsディレクトリにあるnew.html.erbファイルをダウンロードしたファイルで置き換えましょう

削除するファイル(mooovi)

  •  app
    •  views
      •  devise
        •  registrations
          •  new.html.erb

新しく置き換えるファイル(ダウンロードしたrails2-4)

  •  rails2-4
    •  users
      •  registrations
        •  new.html.erb

  sessionsディレクトリにあるnew.html.erbファイルをダウンロードしたファイルで置き換える

削除するファイル(mooovi)

  •  app
    •  views
      •  devise
        •  sessions
          •  new.html.erb

新しく置き換えるファイル(ダウンロードしたrails2-4)

  •  rails2-4
    •  users
      •  sessions
        •  new.html.erb

 

3.ユーザーのモデルを作成

あとはユーザーモデルの生成を行えばサインアップ機能が実装できる。ユーザーモデルの作成にはターミナルでdeviseのコマンドrails g deviseコマンドを使う。

 

まずはdeviseを利用してuserモデルを作成する。

1. 
 1.deviseのファイルをインストールする間違いなくrails g devise:installコマンドを実行してから以下のコマンドを実行

terminal
1
$ rails g devise user

正常に実行できたらユーザーモデル作成のためのマイグレーションファイルが生成されるのでこれを実行。

2. 以下のようにマイグレーションを実行し、usersテーブルを作成

terminal
1
$ bundle exec rake db:migrate

3. deviseの設定を反映させる必要があるので、サーバーを再起動

terminal
1
2
3
4
5
6
# サーバーの立ち上がっているターミナルの画面に移動して
# サーバーの停止コマンド「control + c」
#####
# サーバーの停止
#####
$ rails s

ユーザーモデルが作成できたらブラウザでhttp://localhost:3000/users/sign_upにアクセスしてユーザーの作成画面に遷移するか確かめる。

新規登録画面

SignUp画面

 上記の画像のように表示されない場合

上記のようなデザインではなく、見た目が崩れてしまっている場合は以下の操作をする。

Finderで現在作成中のアプリケーションが存在するディレクリに移動。
その中のtmpというフォルダの中のcacheというフォルダをゴミ箱に入れる。

  •  mooovi
    •  tmp
      • cache

削除した後に、サーバーを再起動し、もう一度上記の画像のようなデザインになっているか確認してみる。

 

STEP4:サインアウト、ログイン機能をつける

ユーザーの登録ができるようになったが、この状態では既存のユーザーでログインしたり、アカウントを切り替えることができまない。そこで、サインアウト、ログインができるようにする。機能はすでにdeviseで実装できている。

 作業内容

1.サインアウトボタンを設置する
2.サインアウト後のリダイレクトを設定する
3.サインインしていない場合はログイン画面にリダイレクトさせる

1.サインアウトボタンを設置する

ログインするにもサインアウトができなければならない。そこでまずはサインアウト機能から実装する。

サインアウトのボタンを投稿するボタンの横に設置する。

このサインアウトボタンを押したら/users/sign_outにリクエストを飛ばすようにする。

deviseで実装されるサインアウトのリクエストは初期の状態ではDELETEメソッド。

試しにmoooviのディレクトリでターミナルにrake routesコマンドを打ち込んでリクエストとその種類が一覧で見る。

terminal
1
2
3
4
  $ cd ~/projects/mooovi
  # 「mooovi」ディレクトリに移動

  $ bundle exec rake routes
terminal
1
2
3
4
 Prefix Verb   URI Pattern                       Controller#Action
        new_user_session GET    /users/sign_in(.:format)          devise/sessions#new
            user_session POST   /users/sign_in(.:format)          devise/sessions#create
    destroy_user_session DELETE /users/sign_out(.:format)         devise/sessions#destroy

/users/sign_outの横にはDELETEと書いてある。よってサインアウトのリクエストの種類はDELETEということである。

サインアウトボタンをaタグにした場合、/users/sign_outのリクエストはデフォルトではGETメソッドとなってしまう。そこで、メソッドがDELETEとなるようにHTMLを修正する必要がある。

 問題3: サインアウトボタンを設置する

作業ファイル:app/views/layouts/review_site.html.erb
ヒント①:link_toメソッドを利用する 

 

2.サインアウト後のリダイレクトを設定

サインアウトができるようになったが、サインアウトボタンを押してもなにもおきない(しかし実際にサインアウトはできている)。サインアウト後にはログイン画面に遷移させるのが自然。

ここではサインアウト後のリダイレクト先の設定をする。ログイン画面はdeviseの機能によってすでに実装されている。

ログイン画面

ログイン画面

サインアウト後のリダイレクト先のURLを設定するにはdeviseのメソッドafter_sign_out_path_forを使う。

 after_sign_out_path_forメソッド

deviseでサインアウトしたあとのリダイレクト先を指定するメソッドとしてafter_sign_out_path_forがある。このメソッドでは返り値にサインアウト後のリダイレクト先URLを指定する。deviseのメソッドを上書きしている関係上resourceを引数に渡さなけらばならないので、resourceを引数に渡す。

application_controller.rb
1
2
3
  def after_sign_out_path_for(resource)
    '???' # サインアウト後のリダイレクト先URL
  end
application_controller.rb
5
6
7
  def after_sign_out_path_for(resource)
    '/users/sign_in' # サインアウト後のリダイレクト先URL
  end

実際に画面右上にある「サインアウト」をクリックして

ログイン画面が表示されるようになっていることを確認する。

https://tech-master.s3.amazonaws.com/uploads/curriculums//8e4819419f8ff3ad38855ee223014cb8.png

 

3.サインインしていない場合はログイン画面にリダイレクトさせる

この状態ではhttp://localhost:3000/users/sign_inにアクセスするか、サインアウトボタンを押さないとログイン画面に移動しない。レビューの投稿はログインしている状態でないとできないようにする。

レビューの投稿画面(作品の検索画面もふくめて)に移動したらログイン画面にリダイレクトさせる。逆に言えばトップページや作品ページはログインしていなくてもアクセスできる。

ログインしていない状態

「投稿する」を押したらリダイレクト

 

レビューを投稿する画面に遷移するためのボタンはヘッダーの「投稿するボタン」作品ページの「この作品を投稿するボタン」の2つ。それぞれを押すとどのコントローラのどのアクションが呼ばれるのか確認する。

 authenticate_user!

deviseをインストールすると、ログイン画面とサインアップ画面を自動で用意してくれる。authenticate_user!はdeviseをインストールすることで使えるメソッド。ユーザーがログインしているかどうかを確認し、ログインしていない場合はログインページにリダイレクトする。通常、before_actionを合わせて使用する。before_actionのexceptやonlyオプションを組み合わせると特定のアクションを指定することもできる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class TopController < ReviewController
  before_action :authenticate_user!, except: :index

  def index
    @tweets = Tweet.includes(:user).page(params[:page]).per(5).order("created_at DESC")
  end

  def new
  end

  def create
    Tweet.create(image: params[:image], text: params[:text], user_id: current_user.id)
  end

上の例ではbefore_actionにauthenticate_user!を記載している。さらにexcept以下にindexアクションを記述することでindexアクション以外にだけauthenticate_ user!が適用されるように指定をしている。

今回の場合は、「投稿するボタン」作品ページの「この作品を投稿するボタン」を押した際に動くアクションにauthenticate_user!をbefore_actionを使って記述する。ただ、コントローラのすべてのアクションにそのbefore_actionを適応させるとレビューを投稿する時以外(ホームや作品ページ)でもリダイレクトの処理が実行されてしまう。

そこで、onlyオプションを使って、before_actionをどのアクションのときに実行させるか選択する

 問題4:ログインしていない状態でレビューの投稿をしようとすると、ログイン画面にリダイレクトされるようにする

作業ファイル:どのファイルを編集すれば良いか、考える
ヒント①:レビューを投稿する画面に移動できるのはヘッダーの「投稿するボタン」と作品ページの「この作品を投稿するボタン」の2つ
ヒント②:レビューを投稿する画面のURLを見て、どのコントローラのどのアクションが呼ばれているか確認する(rake routesを使うか、routes.rbを見るとわかる)

実際に
「サインアウト」ボタンを押して、サインアウトして確認する。

https://tech-master.s3.amazonaws.com/uploads/curriculums//a0f7261d602b4f71a3ca27fb84aff80d.png

サインアウトするとログイン画面が表示される。

次に、ログイン画面の左上の「mooovi」というロゴをクリックする。

https://tech-master.s3.amazonaws.com/uploads/curriculums//b3a997b801a3825a96221be0105095a5.png

作品一覧画面が表示される。
次に、作品一覧画面の右上にある「投稿する」をクリック。

そうすると、ログイン画面にリダイレクトされることを確認する。

https://tech-master.s3.amazonaws.com/uploads/curriculums//b5130e16f879798977f10a0808f83b31.png