映画レビューサイトのコード その2
STEP1:レビューの評価を星として表示
現在、レビューの評価がどこにも表示されていない。moooviではレビューの評価を星として表示する。
レビューの評価を表示させる画面はトップページと作品ページの2つ。
星の数はそれぞれ画像が用意されており、以下のようにして表示することができる。
トップページであるindex.html.erb、作品ページである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 |
<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
カラムを持っているので、それをそのまま取得し、先ほどの例の[ここに評価を表示]
の部分で利用すれば良い。
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?
は
レシーバが奇数かどうかを判定してtrue
かfalse
を返してくれるインスタンスメソッド。判定のためにはレシーバを利用した式が必要なので、odd?
を自分で作るとすれば、以下の例のような実装になるる。self
を使ってレシーバ自身をメソッドの中で利用している。
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
をインスタンス・メソッドの中に呼んであげる。
1 2 3 4 5 6 7 |
class Product < ApplicationRecord
has_many :reviews
def review_average
self.reviews.average(:rate).round
end
end
|
さらに、self
は実は省略することが可能。上記の式を省略すると、以下のようになる。
1 2 3 4 5 6 7 |
class Product < ApplicationRecord
has_many :reviews
def review_average
reviews.average(:rate).round
end
end
|
だいぶスッキリとした式になる。
続いて、定義したメソッドを実際にビューで利用する。
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
を書けばいいのか。今回ランキングを表示するのはいまのところはすべてのビューである。よって、ProductsControllerとReviewsControllerの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を継承させている。
「投稿数が多い順に」という部分はこのあと実装するので、今回はProductsテーブルに入ってる作品を5件取得して変数@ranking
に入れておく。
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行目に、
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
- views
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
- layouts
- views
review_site.html.erbの29行目
29 |
<%= yield %>
|
<%= yield %>
は外部のHTMLファイルを読み込むためのerb記法。ここに今まで修正していたshow.html.erbなどのHTMLがアクションごとに差し込まれる。
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から番号が入った。
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メソッドを使うと出力は以下のようになる。
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件だけが表示されているが、プログラムが実行されている裏側ではすべてのレコードが指定したカラムでまとめられている。現在は、それぞれのまとまりが具体的に何個あるのかはわからない。
この状態でcountメソッドを使うとgroupメソッドでまとめられたレコードの数が取得できる。
countメソッド
countメソッドは配列などの要素数を返すメソッド。groupメソッドに続けて使うとまとめられたそれぞれのレコードの数が取得できる。
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メソッドを使う。
1 2 |
Review.group(:product_id).order('product_id DESC').count
=> {23=>4, 22=>1, 21=>2}
|
この書き方ではproduct_idでソートされるが、レコードの数ではソートされていない。groupメソッドでまとめたレコードの数でソートするには以下のように書く。
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メソッドを実行することになりエラーが起こる。そのため、その直前に付け加え、以下のようにする。
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というメソッドを持っている。これはハッシュのキーだけを取り出して配列として返すメソッド。
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してみる。
1 2 3 4 5 |
これで該当するidの値がすべて取得できた。
しかし、ここで1つ問題がある。それは取得したレコードの順番である。
以下のような配列ids
でwhereしたとき、レコードはどのような順番で取得できるのか。
1 2 3 4 5 |
出力結果は、配列ids
を上のids = [1, 2, 3]
とした場合と、いまのids = [3, 1, 2]
の出力結果は同じ。これはwhereで値を配列にした場合、並びがid順になってしまうため。
せっかくProductモデルのidをレビューの投稿が多い順に取得したのに、これでは意味がない。
- 期待した出力結果
1 2 3 |
- whereメソッドでの出力結果
1 2 3 |
この問題を解決するために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メソッドを使うと配列オブジェクトの各要素を使って新しい配列を生成することができる。product_idの配列に対してmapメソッドを使い、Productsテーブルからレコードを取得するのは以下のような方法になる。
1 2 3 4 5 |
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の最終行に以下のように追。
1 2 |
(省略)
gem 'devise' # 最終行に追記してください
|
bundle installコマンドを実行
1 2 3 |
$ pwd
#/Users/ユーザー名/projects/moooviであることを確認
$ bundle install
|
問題なくdeviseのgemがインストールできたらdeviseのセットアップをする。
まずは、deviseを使うのに必要なファイルを生成。
rails g devise:installコマンドをターミナルで実行
1 |
$ rails g devise:install
|
次にdeviseのサインアップやログインのviewファイルを生成。
rails g devise:viewsコマンドをターミナルで実行
1 |
$ rails g devise:views
|
これでdeviseのviewファイルが生成できた。以下のようにapp/viewsのディレクトリの下にdeviseというディレクトリが生成されていれば成功。
- app
- views
- devise
- views
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
- registrations
- devise
- views
新しく置き換えるファイル(ダウンロードしたrails2-4)
- rails2-4
- users
- registrations
- new.html.erb
- registrations
- users
sessionsディレクトリにあるnew.html.erbファイルをダウンロードしたファイルで置き換える
削除するファイル(mooovi)
- app
- views
- devise
- sessions
- new.html.erb
- sessions
- devise
- views
新しく置き換えるファイル(ダウンロードしたrails2-4)
- rails2-4
- users
- sessions
- new.html.erb
- sessions
- users
3.ユーザーのモデルを作成
あとはユーザーモデルの生成を行えばサインアップ機能が実装できる。ユーザーモデルの作成にはターミナルでdeviseのコマンドrails g devise
コマンドを使う。
まずはdeviseを利用してuserモデルを作成する。
1.
1.deviseのファイルをインストールするで間違いなくrails g devise:installコマンドを実行してから以下のコマンドを実行
1 |
$ rails g devise user
|
正常に実行できたらユーザーモデル作成のためのマイグレーションファイルが生成されるのでこれを実行。
2. 以下のようにマイグレーションを実行し、usersテーブルを作成
1 |
$ bundle exec rake db:migrate
|
3. deviseの設定を反映させる必要があるので、サーバーを再起動
1 2 3 4 5 6 |
# サーバーの立ち上がっているターミナルの画面に移動して
# サーバーの停止コマンド「control + c」
#####
# サーバーの停止
#####
$ rails s
|
ユーザーモデルが作成できたらブラウザでhttp://localhost:3000/users/sign_upにアクセスしてユーザーの作成画面に遷移するか確かめる。
新規登録画面
上記の画像のように表示されない場合
上記のようなデザインではなく、見た目が崩れてしまっている場合は以下の操作をする。
Finderで現在作成中のアプリケーションが存在するディレクリに移動。
その中のtmpというフォルダの中のcache
というフォルダをゴミ箱に入れる。
- mooovi
- tmp
- cache
- tmp
削除した後に、サーバーを再起動し、もう一度上記の画像のようなデザインになっているか確認してみる。
STEP4:サインアウト、ログイン機能をつける
ユーザーの登録ができるようになったが、この状態では既存のユーザーでログインしたり、アカウントを切り替えることができまない。そこで、サインアウト、ログインができるようにする。機能はすでにdeviseで実装できている。
作業内容
1.サインアウトボタンを設置する
2.サインアウト後のリダイレクトを設定する
3.サインインしていない場合はログイン画面にリダイレクトさせる
1.サインアウトボタンを設置する
ログインするにもサインアウトができなければならない。そこでまずはサインアウト機能から実装する。
サインアウトのボタンを投稿するボタンの横に設置する。
このサインアウトボタンを押したら/users/sign_outにリクエストを飛ばすようにする。
deviseで実装されるサインアウトのリクエストは初期の状態ではDELETEメソッド。
試しにmoooviのディレクトリでターミナルにrake routes
コマンドを打ち込んでリクエストとその種類が一覧で見る。
1 2 3 4 |
$ cd ~/projects/mooovi
# 「mooovi」ディレクトリに移動
$ bundle exec rake routes
|
1 2 3 4 |
/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を引数に渡す。
1 2 3 |
def after_sign_out_path_for(resource)
'???' # サインアウト後のリダイレクト先URL
end
|
5 6 7 |
実際に画面右上にある「サインアウト」をクリックして
ログイン画面が表示されるようになっていることを確認する。
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 |
上の例では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を見るとわかる)
実際に
「サインアウト」ボタンを押して、サインアウトして確認する。
サインアウトするとログイン画面が表示される。
次に、ログイン画面の左上の「mooovi」というロゴをクリックする。
作品一覧画面が表示される。
次に、作品一覧画面の右上にある「投稿する」をクリック。
そうすると、ログイン画面にリダイレクトされることを確認する。