書籍の情報を登録、編集、削除できるMVCアプリを作成します。
アプリごとにgemのバージョンが異なる場合が多いので、システム共通ディレクトリではなく、アプリごとのディレクトリにgemをインストールする手順で学習していきます。
# ホームディレクリ直下のruby-campディレクトリに移動します。$ cd ~/ruby-camp# アプリ用のディレクトリ(今回はbook_shelf)を作成し、作成したディレクトリに移動する。$ mkdir book_shelf$ cd book_shelf
gemを管理するGemfileの作成するために、bundle init
コマンドを実行します。
$ bundle init
Gemfileを以下のように修正します。 「# gem "rails"」の#
を削除します。 現状だとコメントアウトされているため。 「gem "rails"」の後ろに「, "5.2.1"」を追加します。(今回はバージョンを指定) 追加後は「gem "rails", "5.2.1"」となっていればOKです!
# Gemfile# frozen_string_literal: truesource "https://rubygems.org"git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }- # gem "rails"+ gem "rails", "5.2.1"
--path vendor/bundle
を指定し、railsアプリケーション用のディレクトリ配下のvendor/bundle
ディレクトリにgemをインストールします。 --jobs=4
オプションは、gemを並列処理でインストールするというオプジョンになります。(インストールが早く終わるようです) 毎回bundle install --path vendor/bundle --jobs=4
を実行するのは長いので、bi
とコマンドを入力したら、bundle install --path vendor/bundle --jobs=4
を実行するようにエイリアスを設定します。
$ vi ~/.bashrc
ファイルを開いたら以下の内容を記述します。
alias bi='bundle install --path vendor/bundle --jobs=4'
.bashrc
を読み込んでいる.bash_profile
を再度読み込みます。
$ source ~/.bash_profile
bi
コマンドを実行します。
$ bi
Railsアプリの作成します。 今回はカレントディレクトリ(.
)に、5.2.1
のRailsでアプリを作成します。
$ bundle exec rails _5.2.1_ new .# Gemfileがすでに存在するため、以下のメッセージが表示されるので、「y」を押した後にenterを押します。existcreate README.mdcreate Rakefilecreate .ruby-versioncreate config.rucreate .gitignoreconflict GemfileOverwrite /Users/kazuma/ruby-camp/book_shelf/Gemfile? (enter "h" for help) [Ynaqdh]
Gemfileが上書きされるので、再度バージョン部分を以下のように修正します。 sqlite3の1.4系がリリースされたが、Rails側が対応していないので、, '~> 1.3.6'
の指定をします。
# Gemfilesource 'https://rubygems.org'git_source(:github) { |repo| "https://github.com/#{repo}.git" }ruby '2.5.3'# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'- gem 'rails', '~> 5.2.1'+ gem 'rails', '5.2.1'# Use sqlite3 as the database for Active Record- gem 'sqlite3'+ gem 'sqlite3', '~> 1.3.6'# Use Puma as the app servergem 'puma', '~> 3.11'# 省略
Gemfileを修正したのでもう一度bundle installを実行。 オプション指定は2回目以降は不要。
$ bundle install
Gitで管理する場合は、.gitignore
ファイルに以下の行を追記(末尾に1行追記)します。 /vendor/bundle
内にGemファイルがインストールされているため、Gitの管理対象外とします。 bundle install
を実行することで、Gemをインストールできるので、管理する必要はありません。
/vendor/bundle
vi(vim)で編集してもいいですが、ターミナルで以下のコマンドを実行してすることで、.gitignoreファイルの末尾に追記することが可能です。
$ echo "/vendor/bundle" >> .gitignore
覚える事ができない場合はエイリアスを設定します。
$ vi ~/.bashrc
ファイルを開いたら以下の内容を記述します。
alias vbgi='echo "/vendor/bundle" >> .gitignore'
以下のコマンドを実行することで、.gitignoreに/vendor/bundle
を追記することが可能です。
$ vbgi
以下の内容でBookモデルを作成します。
カラム名 | カラム名(日本語) | データ型 |
name | 書籍名 | string |
price | 価格 | integer |
publish_date | 発売日 | date |
description | 説明 | text |
モデルを作成する場合は、bin/rails g model [モデル名] [カラム名]:[データ型]
で作成しますが、今回はbin/rails g resource
コマンドを使用します。 resource
コマンドを実行することで、関連するコントローラファイルと、Viewディレクトリ(app/views/XXXX)を作成してくれます。
$ bin/rails g resource book title:string price:integer publish_date:date description:text
データベースに反映する前に、マイグレーションスクリプトを修正します。 全てのカラムを必須入力とするため、null: false
とします。 また、titleとdescriptionは文字数を制限するため、length
オプションを指定します。
title: null: false, length: 50
price: null: false
publish_date: null: false
description: null: false, length: 1000
# db/migrate/yyyyMMddHHmmss_create_books.rbclass CreateBooks < ActiveRecord::Migration[5.2]def changecreate_table :books do |t|- t.string :title- t.integer :price- t.date :publish_date- t.text :description+ t.string :title, null: false, length: 50+ t.integer :price, null: false+ t.date :publish_date, null: false+ t.text :description, null: false, length: 1000t.timestampsendendend
マイグレーションスクリプトに追記したらデータベースに反映します。
$ bin/rails db:migrate
データベースに反映されたことを確認するために、bin/rails db:migrate:status
コマンドを実行します。 Statusが「up」になっていればOKです。 「down」の場合は、修正したマイグレーションスクリプトに問題がある可能性があるので確認してください。
$ bin/rails db:migrate:statusdatabase: /Users/kazuma/ruby-camp/book_shelf/db/development.sqlite3Status Migration ID Migration Name--------------------------------------------------up 20181214071252 Create books
Statusがupの状態でマイグレショーンスクリプトを修正し、bin/rails db:migrate
を実行してもデータベースに反映することができません。 修正する場合は一度「bin/rails db:rollback」で直前のマイグレーションを「down」にしてから修正します。
RailsアプリにBootstrapを導入します。
Bootstrapを使用するために、Gemを追加します。 BootstrapはjQuery
に依存するため、(デフォルトでjQueryがインストールされない)Rails5.1以上ではjquery-rails
もGemfileに追記します。
# Gemfile# 省略gem 'bootsnap', '>= 1.1.0', require: false+ gem 'bootstrap', '~> 4.1.3'+ gem 'jquery-rails'group :development, :test do# Call 'byebug' anywhere in the code to stop execution and get a debugger consolegem 'byebug', platforms: [:mri, :mingw, :x64_mingw]endgroup :development do# Access an interactive console on exception pages or by calling 'console' anywhere in the code.gem 'web-console', '>= 3.3.0'gem 'listen', '>= 3.0.5', '< 3.2'# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/springgem 'spring'gem 'spring-watcher-listen', '~> 2.0.0'end# 省略
Gemfileを修正したらbundle installを行います。
$ bi
application.jsに、jquery3、popper、bootstrap-sprocketsを記述します。
// app/assets/javascripts/application.js// 省略////= require rails-ujs//= require activestorage//= require turbolinks+ //= require jquery3+ //= require popper+ //= require bootstrap-sprockets//= require_tree .
app/assets/stylesheets/application.cssファイルのファイル名を、 mvコマンドを使用して、application.css
からapplication.scss
に変更します。 ファイル名変更後、bootstrapをインポートします。
$ mv app/assets/stylesheets/application.css app/assets/stylesheets/application.scss
app/assets/stylesheets/application.scss
でbootstrapをimportします。
注意点
Sassファイルでは*= require
、*= require_tree
を削除する
Sassファイルではインポートに@import
を利用する
Sassファイルで*= require
を利用すると他のスタイルシートではBootstrapのmixinや変数を利用できなくなる
/* app/assets/stylesheets/application.scss *//** This is a manifest file that'll be compiled into application.css, which will include all the files* listed below.** Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's* vendor/assets/stylesheets directory can be referenced here using a relative path.** You're free to add application-wide styles to this file and they'll appear at the bottom of the* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS* files in this directory. Styles in this file should be added after the last require_* statement.* It is generally better to create a new file per style scope.*-*= require_tree .-*= require_self*/+@import "bootstrap";
全てのページにヘッダーを表示するため、コードを書いていきます。複数のページに共通の内容を表示する場合は、特定のファイルに切り出し、そのファイルを読み込むようにします。 切り出したファイルの事を パーシャルフォーム(部分テンプレート) といいます。
パーシャルフォームを利用することで、個々のビューで同じコードを記述する必要がなくなります。 部分テンプレートのファイル名の先頭には 「_」 を付けることによりファイル名からパーシャルであることを明示します。
共通のパーシャルフォームはapp/views/の中にsharedディレクトリを作成し、その中にファイルを作ります。 今回はファイル名を「_header.html.erb」とします。
$ mkdir app/views/shared$ touch app/views/shared/_header.html.erb
bin/rails g resource book
を実行したので、config/routes.rb
にはresources :books
が追記されています。 http://localhost:3000
でアクセスした時にindexページが表示されるように追記します。
# config/routes.rbRails.application.routes.draw doresources :books+ root "books#index"# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.htmlend
以下のリンクを参考にBootstrapを使用してヘッダーを作成します。 https://getbootstrap.com/docs/4.1/components/navbar/#toggler
<!-- app/views/shared/_header.html.erb -->+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-3">+ <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarToggler" aria-controls="navbarTogglerDemo03" aria-expanded="false" aria-label="Toggle navigation">+ <span class="navbar-toggler-icon"></span>+ </button>+ <%= link_to "BookShelf", root_path, class: "navbar-brand" %>+ <div class="collapse navbar-collapse" id="navbarToggler">+ <ul class="navbar-nav mr-auto mt-2 mt-lg-0">+ <li class="nav-item">+ <%= link_to "Home", root_path, class: "nav-link" %>+ </li>+ <li class="nav-item">+ <%= link_to "New Book", new_book_path, class: "nav-link" %>+ </li>+ </ul>+ <form class="form-inline my-2 my-lg-0">+ <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">+ <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>+ </form>+ </div>+ </nav>
通常使用するindex.htmlやshow.htmlにはheadタグやdobyタグが存在しません。app/views/layouts/application.htmlのyieldの部分に、index.html.erbやshow.html.erbが呼び出されるイメージです。 ヘッダーは全てのビューに表示したいので、application.html.erbから呼び出します。
render [ファイル名]
で呼び出す事が可能です。今回は別のディレクトリにheader.html.erbが存在するため、render "shared/header"
で呼び出すことが可能です。 呼び出す場合は、ファイル名の「」を除いた名前で呼び出します。 Bootstrapのグリッドシステムを使用するため、yeildをcontainerとrowのdivタグで囲います。
<!-- application.html.erb --><!DOCTYPE html><html><head><title>BookShelf</title><%= csrf_meta_tags %><%= csp_meta_tag %><%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %><%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %></head><body>+ <%= render "shared/header" %>+ <div class="container">+ <div class="row"><%= yield %>+ </div>+ </div></body></html>
ヘッダーの表示確認をするため、indexアクションとindex.html.erbを作成します。
# app/controllers/books_controller.rbclass BooksController < ApplicationController+ def index+ @books = Book.all+ endend
ファイルは作成しますが、中身は何も記述しません。
$ touch app/views/books/index.html.erb
サーバを起動し、http://localhost:3000
にアクセスして、ヘッダーが表示されることを確認します。
$ bin/rails s
Bookの登録機能を作成します。
データを登録する場合は、newとcreateアクションが必要になります。 ストロングパラメータと一緒に作成します。
Railsには簡易的なメッセージを表示する、flashという機能があります。 今回は登録後にメッセージを表示するようにします。 redirect_toのnoticeオプションを指定します。 notice: [メッセージ]
で表示することが可能です。 「書籍を登録しました。」のメッセージを表示する部分は、照会機能を作成する際に追加します。
# app/controllers/books_controller.rbclass BooksController < ApplicationControllerdef index@books = Book.allend+ def new+ @book = Book.new+ end+ def create+ @book = Book.new(book_params)+ @book.save+ redirect_to @book, notice: "書籍を登録しました。"+ end+ private+ def book_params+ params.require(:book).permit(:title, :price, :publish_date, :description)+ endend
登録用フォームのnew.html.erbを作成します。登録と更新フォームは内容が一緒になるので、_form.html.erbを作成し、new.html.erbから呼び出します。
# new.html.erbの作成$ touch app/views/books/new.html.erb# _form.html.erbの作成$ touch app/views/books/_form.html.erb
今回は@bookを_form.html.erbで使用するので、パーシャルフォームに変数を渡す必要があります。 render partial: [ファイル名], locals: { [受け取る変数名]: [渡す変数名] }
の内容でパーシャルフォームに変数を渡す事が可能ですが、オプションがlocalsの時だけは、以下のように省略して記述することが可能です。 render [ファイル名], [受け取る変数名]: [渡す変数名]
<!-- app/views/books/new.html.erb -->+ <div class="col-md-10 offset-md-1 mt-3">+ <h2 class="text-center">書籍登録</h2>+ <%= render 'form', book: @book %>+ </div>
<!-- app/views/books/_form.html.erb -->+ <%= form_with model: book, local: true do |form| %>+ <div class="form-group">+ <%= form.label :title %>+ <%= form.text_field :title, class: "form-control" %>+ </div>+ <div class="form-group">+ <%= form.label :price %>+ <%= form.number_field :price, class: "form-control" %>+ </div>+ <div class="form-group">+ <%= form.label :publish_date %>+ <%= form.date_field :publish_date, class: "form-control" %>+ </div>+ <div class="form-group">+ <%= form.label :description %>+ <%= form.text_area :description, rows: 10, class: "form-control" %>+ </div>+ <%= form.submit class: "btn btn-primary" %>+ <% end %>
Bookの照会機能を作成します。
# app/controllers/books_controller.rbclass BooksController < ApplicationControllerdef index@books = Book.allenddef new@book = Book.newenddef create@book = Book.new(book_params)@book.saveredirect_to @book, notice: "書籍を登録しました。"end+ def show+ @book = Book.find(params[:id])+ endprivatedef book_paramsparams.require(:book).permit(:title, :price, :publish_date, :description)endend
照会用画面のshow.html.erbと、登録後のお知らせメッセージを表示する_flash.html.erbを作成します。 お知らせメッセージはパーシャルフォームとしてsharedディレクトリに作成します。
# show.html.erbの作成$ touch app/views/books/show.html.erb# _flash.html.erbの作成$ touch app/views/shared/_flash.html.erb
number_with_delimiterは、引数として渡された値をカンマ区切りにしてくれるメソッドです。 simple_formatは、引数として渡された値をpタグで囲って出力します。また改行をbrタグに変換します。
<!-- app/views/books/show.html.erb -->+ <div class="col-md-10 offset-md-1 mt-5">+ <h2><%= @book.title %></h2>+ <ul>+ <li><b>価格: </b><%= number_with_delimiter @book.price %>円</li>+ <li><b>発売日: </b><%= @book.publish_date %></li>+ </ul>+ <hr>+ <h4>商品の説明</h4>+ <%= simple_format @book.description %>+ </div>
お知らせ用パーシャルフォームを作成します。 flash.notice
で設定されている内容を表示することが可能です。
<!-- app/views/shared/_flash.html.erb -->+ <% if flash.notice %>+ <div class="alert alert-success" role="alert">+ <%= flash.notice %>+ <button type="button" class="close" data-dismiss="alert" aria-label="Close">+ <span aria-hidden="true">×</span>+ </button>+ </div>+ <% end %>+ <% if flash.alert %>+ <div class="alert alert-danger" role="alert">+ <%= flash.alert %>+ <button type="button" class="close" data-dismiss="alert" aria-label="Close">+ <span aria-hidden="true">×</span>+ </button>+ </div>+ <% end %>
flash用のパーシャルフォームは、色々な画面で使用する可能性があるので、application.html.erbから呼び出すようにします。
<!-- app/views/layouts/application.html.erb --><!DOCTYPE html><html><head><title>BookShelf</title><%= csrf_meta_tags %><%= csp_meta_tag %><%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %><%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %></head><body><%= render "shared/header" %><div class="container">+ <div class="row">+ <div class="col-md-12">+ <%= render "shared/flash" %>+ </div>+ </div><div class="row"><%= yield %></div></div></body></html>
データの検証、つまり正しいデータだけをデータベースに保存するための仕組みのことを言います。
クライアント側のバリデーションと、サーバー側のバリデーションがあります。
今回はサーバー側のバリデーションを実装します。
app/models/book.rb
にvalidates
を追加し、それぞれの項目を設定します。
presence
で必須入力のチェックを行います。
length
で文字数のチェックを行います。
numericality
で数値チェックを行います。
バリデーションについては、こちらが参考になります。
# app/models/book.rbclass Book < ApplicationRecord+ validates :title, presence: true, length: { maximum: 50 }+ validates :price, presence: true,+ numericality: {+ only_integer: true,+ greater_than: 1+ }+ validates :publish_date, presence: true+ validates :description, presence: true, length: { maximum: 1000 }end
books_controller.rbのcreate
アクションを、バリデーションでOK(true)の場合とNG(false)の時で処理を分岐します。 NGだった場合、入力内容を保持したままViewを表示したいので、render [:アクション名]
で画面を呼びます。 create
アクションを下記のように変更して下さい。 ※renderの詳しい使い方は以下を
# app/controllers/books_controller.rbclass BooksController < ApplicationControllerdef index@books = Book.allenddef new@book = Book.newenddef create@book = Book.new(book_params)- @book.save- redirect_to @book, notice: "書籍を登録しました。"+ if @book.save+ redirect_to @book, notice: "書籍を登録しました。"+ else+ render :new+ endenddef show@book = Book.find(params[:id])endprivatedef book_paramsparams.require(:book).permit(:title, :price, :publish_date, :description)endend
エラー表示用のパーシャルフォームをsharedディレクトリに作成します。
$ touch app/views/shared/_errors.html.erb
_form.html.erb
から_errors.html.erb
を呼び出します。 変数をbookをobjという名前で渡します。他のモデルのエラー内容を表示する可能性があるので、汎用性のあるobjという名前にしています。
<!-- app/views/books/_form.html.erb --><%= form_with model: book, local: true do |form| %>+ <%= render "shared/errors", obj: book %><div class="form-group"><%= form.label :title %><%= form.text_field :title, class: "form-control" %></div><div class="form-group"><%= form.label :new_image %><%= form.file_field :new_image, class: "form-control" %></div><div class="form-group"><%= form.label :price %><%= form.number_field :price, class: "form-control" %></div><div class="form-group"><%= form.label :publish_date %><%= form.date_field :publish_date, class: "form-control" %></div><div class="form-group"><%= form.label :description %><%= form.text_area :description, rows: 10, class: "form-control" %></div><%= form.submit class: "btn btn-primary" %><%= link_to '戻る', books_path, class: "btn btn-secondary" %><% end %>
obj.errorsの中に、エラーメッセージが配列として格納されています。 any?
メソッドで、配列の中に有効な値が存在するか確認します。 有効な値が存在した場合、エラーメッセージを表示します。 any?についてはこちら
<!-- app/views/shared/_errors.html.erb -->+ <% if obj.errors.any? %>+ <div class="alert alert-danger">+ <p><b><%= obj.errors.count %>件のエラーがあります</b></p>+ <ul>+ <% obj.errors.full_messages.each do |message| %>+ <li><%= message %></li>+ <% end %>+ </ul>+ </div>+ <% end %>
何も入力せずに登録ボタンを押します。
Railsの国際化機能を使うと、日本語や英語などさまざまな言語のテキストをブラウザー上に表示することが可能です。
日本語のロケールファイル(辞書ファイル)は、config/locales/ja.ymlに配置します。 YAML (YAML Ain’t Markup Language) とは、構造化されたデータを表現するためのフォーマットです。 YAMLはインデントで構造を表現します。インデントがずれると、正しい値を取得できません。 curlコマンドを使用して、ファイルを取得します。
curl [取得したいファイルのURL] -o [保存先のパス]
-o
オプション:ファイルに出力する場合に指定します。
$ curl https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml -o config/locales/ja.yml
プロジェクトの規模が大きくなるとロケールファイルを1つのYAMLで管理するのが困難になります。 今回は辞書ファイルをview単位で分割してみます。
locales│── en.yml│── ja.yml└── views└── books├── en.yml└── ja.yml
日本語のサイトにする場合は、config.i18n.default_locale = :ja
を指定します。 タイムゾーンも東京に変更します。(デフォルトはUTCなので東京との時差9時間) 複数のロケールファイルを読み込めるようにload_path
を追加します。
# config/application.rbrequire_relative 'boot'require 'rails/all'# Require the gems listed in Gemfile, including any gems# you've limited to :test, :development, or :production.Bundler.require(*Rails.groups)module BookShelfclass Application < Rails::Application# Initialize configuration defaults for originally generated Rails version.config.load_defaults 5.2+ config.time_zone = 'Tokyo' # タイムゾーンもついでに変更+ config.i18n.default_locale = :ja+ config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]config.action_view.field_error_proc = Proc.new do |html_tag, instance|%Q(#{html_tag}).html_safeend# Settings in config/environments/* take precedence over those specified here.# Application configuration can go into files in config/initializers# -- all .rb files in that directory are automatically loaded after loading# the framework and any gems in your application.endend
curlコマンドで取得したconfig/locales/ja.yml
に、以下の内容を追記します。
# config/locales/ja.ymlja:activerecord:+ models:+ book: 書籍情報+ attributes:+ book:+ title: タイトル+ price: 価格+ publish_date: 発売日+ description: 詳細errors:messages:
ビュー用のロケールファイル保存用ディレクトリを作成します。 作成したディレクトリの中に、ja.yml
ファイルを作成します。
$ mkdir -p config/locales/views/books/$ touch config/locales/views/books/ja.yml
作成したja.yml
ファイルに以下の内容を記述します。
# config/locales/views/books/ja.yml+ ja:+ books:+ new:+ title: 書籍登録+ show:+ price: 価格+ price_yen: "¥%{price}"+ publish_date: 発売日+ description: 商品の説明
ビューファイルでロケールファイルの内容を表示する場合は、t
メソッドを使用します。 本来であれば、t 'books.new.title'
で「書籍登録」を表示します。今回はディレクトリ構造と、YAMLファイルの構造が一致しているので、t '.title'
だけで「書籍登録」を表示することが可能です。 コントローラ等でロケールファイルの内容を表示する場合は、I18n.t
を使用します。 以下のように修正します。
<!-- app/views/books/new.html.erb --><div class="col-md-10 offset-md-1 mt-3">- <h2 class="text-center">書籍登録</h2>+ <h2 class="text-center"><%= t '.title' %></h2><%= render 'form', book: @book %></div>
同じように、書籍照会画面も修正します。
<!-- app/views/books/show.html.erb --><div class="col-md-10 offset-md-1 mt-5"><h2><%= @book.title %></h2><ul>- <li><b>価格:</b><%= number_with_delimiter @book.price %>円</li>- <li><b>発売日:</b><%= @book.publish_date %></li>+ <li><b><%= t ".price" %>: </b><%= t(".price_yen", price: (number_with_delimiter @book.price)) %></li>+ <li><b><%= t ".publish_date" %>: </b><%= @book.publish_date %></li></ul><hr>- <h4>商品の説明</h4>+ <h4><%= t ".description" %></h4><%= simple_format @book.description %></div>
書籍の画像も登録できるようにします。 Railsの5.2の新機能のActice Storageを使用します。 Active Storageが実装される前は、CarrierWaveというGemを使用していました。(現在でもCarrierWaveは使われています。)
Active Storegeを使用するにま、ImageMagickをインストールする必要があります。ImageMagickは画像を操作したり表示したりするためのソフトウェアです。 Homebrewを使用してインストールします。
$ brew install imagemagick
RailsでImageMagickを利用するために、Gemfileにmini_magickを追加します。
# Gemfile# -------- 省略 ----------gem 'bootsnap', '>= 1.1.0', require: falsegem 'bootstrap', '~> 4.1.3'gem 'jquery-rails'+ gem 'mini_magick'# -------- 省略 ----------
Gemをインストールします。
$ bi
bin/rails active_storage:install
を実行して、Active Storageを使用できるようにします。 bin/rails db:migrate
を実行して、データベースに反映します。
$ bin/rails active_storage:install$ bin/rails db:migrate
Bookモデルにhas_one_attached :image
を定義します。has_one_attachedはレコードと画像が1対1の関係性を表します。画像が複数ある場合はhas_many_attachedを指定します。 has_one_attached [:プロパティ名]
※プロパティ名はimageやavatar等の名前にします。
# app/models/book.rbclass Book < ApplicationRecord+ has_one_attached :imagevalidates :title, presence: true, length: { maximum: 50 }validates :price, presence: true,numericality: {only_integer: true,greater_than: 1}validates :publish_date, presence: truevalidates :description, presence: true, length: { maximum: 1000 }end
Railsアプリケーションを普通に操作すると、その内部でオブジェクトが作成されたり、更新されたりdestroyされたりします。Active Recordはこのオブジェクトライフサイクルへのフックを提供しており、これを使用してアプリケーションやデータを制御できます。
Active Recordで利用可能なコールバックの一覧を以下に示します。これらのコールバックは、実際の操作中に呼び出される順序に並んでいます。
オブジェクトの作成
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
オブジェクトの更新
before_validation
after_validation
before_save
around_save
before_update
around_update
after_update
after_save
オブジェクトのdestroy
before_destroy
around_destroy
after_destroy
今の状態で書籍登録をすると、バリデーションでエラーとなった場合でも、画像が登録されてしまいます。
attribute
メソッドを使用して、レコードに存在しないカラムを定義することが可能です。
attribute [:カラム名]
今回はattribute :new_image
とし、登録前の画像を一時的に保持する領域を確保します。 バリデーション通過後に実行されるbefore_saveを使用して、 new_image
が設定されている場合は、self.image
にnew_image
の値を設定します。
# app/controllers/books_controller.rbclass Book < ApplicationRecordhas_one_attached :image+ attribute :new_imagevalidates :title, presence: true, length: { maximum: 50 }validates :price, presence: true,numericality: {only_integer: true,greater_than: 1}validates :publish_date, presence: truevalidates :description, presence: true, length: { maximum: 1000 }+ before_save do+ self.image = new_image if new_image+ endend
書籍登録画面に、画像登録用のフィールドを追加します。
file_field
を使用します。file_field
に紐付けるプロパティはnew_image
となります。
<!-- app/views/books/_form.html.erb --><%= form_with model: book, local: true do |form| %><%= render "shared/errors", obj: book %><div class="form-group"><%= form.label :title %><%= form.text_field :title, class: "form-control" %></div>+ <div class="form-group">+ <%= form.label :new_image %>+ <%= form.file_field :new_image, class: "form-control" %>+ </div><div class="form-group"><%= form.label :price %><%= form.number_field :price, class: "form-control" %></div><div class="form-group"><%= form.label :publish_date %><%= form.date_field :publish_date, class: "form-control" %></div><div class="form-group"><%= form.label :description %><%= form.text_area :description, rows: 10, class: "form-control" %></div><%= form.submit class: "btn btn-primary" %><% end %>
ストロングパラメータにnew_image
を追加します。
# app/controllers/books_controller.rb# 省略privatedef book_params- params.require(:book).permit(:title, :price, :publish_date, :description)+ params.require(:book).permit(:title, :price, :publish_date,+ :description, :new_image)end
config/locales/ja.yml
に以下の内容を追記します。
# config/locales/ja.ymlja:activerecord:models:book: 書籍情報attributes:book:title: タイトルprice: 価格publish_date: 発売日description: 詳細+ new_image: 画像+ image: 画像
config/locales/views/books/ja.yml
に以下の内容を追記します。
# config/locales/views/books/ja.ymlja:books:new:title: 書籍登録edit:title: 書籍更新show:price: 価格price_yen: "¥%{price}"publish_date: 発売日description: 商品の説明+ edit: 編集+ return: 戻る
画像を登録しなかった場合に以下の画像を表示するため、curlコマンドで画像を取得し、app/assets/images
に保存します。
$ curl http://spartacamp.jp/img/no_image.png -o ./app/assets/images/no_image.png
attached?
メソッドを使用して、画像が登録されているかチェックをします。 画像が登録されている場合はvariant
メソッド(ImageMagicの機能)を使用して、サイズを変更します。画像が登録されていない場合は、no_image.png
を表示します。 後ほど作成する編集画面へ遷移するリンクと、一覧画面へ戻るリンクも追加します。
<!-- app/views/books/show.html.erb --><div class="col-md-10 offset-md-1 mt-5">- <h2><%= @book.title %></h2>- <ul>- <li><b><%= t ".price" %>: </b><%= t(".price_yen", price: (number_with_delimiter @book.price)) %></li>- <li><b><%= t ".publish_date" %>: </b><%= @book.publish_date %></li>- </ul>+ <div class="row">+ <div class="col-md-4 mb-4">+ <% if @book.image.attached? %>+ <%= image_tag @book.image.variant(resize: "300x300"), class: "img-thumbnail" %>+ <% else %>+ <%= image_tag "no_image.png", class: "img-thumbnail" %>+ <% end %>+ </div>+ <div class="col-md-8">+ <h2><%= @book.title %></h2>+ <ul>+ <li><b><%= t ".price" %>: </b><%= t(".price_yen", price: (number_with_delimiter @book.price)) %></li>+ <li><b><%= t ".publish_date" %>: </b><%= @book.publish_date %></li>+ </ul>+ </div>+ </div><hr><h4><%= t ".description" %></h4><%= simple_format @book.description %>+ <%= link_to t(".edit"), edit_book_path(@book), class: "btn btn-primary" %>+ <%= link_to t(".return"), books_path, class: "btn btn-secondary" %></div>
if @book.image.attached?
〜end
の部分が増えたことで、Viewが見辛くなってきました。DRYなViewにするために、ヘルパーモジュールに自作のヘルパーメソッドを作成します。 今回はshow_book_image
というメソッドを作成します。Bookクラスのインスタンスを受け取り、画像が登録されている場合と、登録されていない場合の処理を記述します。 ※変数は@book
からbook
に変わっているので注意して下さい。
# app/helpers/books_helper.rbmodule BooksHelper+ def show_book_image(book)+ if book.image.attached?+ image_tag book.image.variant(resize: "300x300"), class: "img-thumbnail"+ else+ image_tag "no_image.png", class: "img-thumbnail"+ end+ endend
照会画面の処理を以下のように変更します。(show_book_imageを呼ぶ)
<!-- app/views/books/show.html.erb --><div class="col-md-10 offset-md-1 mt-5"><div class="row"><div class="col-md-4 mb-4">- <% if @book.image.attached? %>- <%= image_tag @book.image.variant(resize: "300x300"), class: "img-thumbnail" %>- <% else %>- <%= image_tag "no_image.png", class: "img-thumbnail" %>- <% end %>+ <%= show_book_image @book %></div><div class="col-md-8"><h2><%= @book.title %></h2><ul><li><b><%= t ".price" %>: </b><%= t(".price_yen", price: (number_with_delimiter @book.price)) %></li><li><b><%= t ".publish_date" %>: </b><%= @book.publish_date %></li></ul></div></div><hr><h4><%= t ".description" %></h4><%= simple_format @book.description %><%= link_to t(".edit"), edit_book_path(@book), class: "btn btn-primary" %><%= link_to t(".return"), books_path, class: "btn btn-secondary" %></div>
登録機能と照会機能が出来たので、今度は編集機能を作成します。
データを更新する場合は、editとupdateアクションが必要になります。
# app/controllers/books_controller.rb# 省略def show@book = Book.find(params[:id])end+ def edit+ @book = Book.find(params[:id])+ end+ def update+ @book = Book.find(params[:id])+ if @book.update(book_params)+ redirect_to @book, notice: "書籍を更新しました。"+ else+ render :edit+ end+ endprivatedef book_paramsparams.require(:book).permit(:title, :price, :publish_date,:description, :new_image)endend
更新用フォームのedit.html.erbを作成します。
# edit.html.erbの作成$ touch app/views/books/edit.html.erb
ファイルの中身はnew.html.erbと同じになります。画面のタイトルはYAMLファイルで制御します。
+ <div class="col-md-10 offset-md-1 mt-3">+ <h2 class="text-center"><%= t '.title' %></h2>+ <%= render 'form', book: @book %>+ </div>
config/locales/views/books/ja.yml
ファイルに以下の内容を追記します。
# config/locales/views/books/ja.ymlja:books:new:title: 書籍登録+ edit:+ title: 書籍更新show:price: 価格price_yen: "¥%{price}"publish_date: 発売日description: 商品の説明edit: 編集return: 戻る
照会画面に削除ボタンを追加し、ボタンを押下することで初期を削除する機能を作成します。
データを削除する場合は、destroyアクションが必要になります。 削除後は書籍一覧ページに遷移するようにします。
# app/controllers/books_controller.rb# 省略+ def destroy+ @book = Book.find(params[:id])+ @book.destroy+ redirect_to books_path, notice: "書籍を削除しました。"+ endprivatedef book_paramsparams.require(:book).permit(:title, :price, :publish_date,:description, :new_image)endend
照会画面に削除用のボタンを追加します。削除ボタンを押した際にモーダルフォールを表示します。モーダルフォームはBootstrapのサイトを参考に作成します。 target: "#delete-book"
のdelete-book
と、<div class="modal fade" id="delete-book">
のdelete-book
が一致しなければモーダルフォームは表示されません。
t(".question_book", book: @book.title)
のように、tメソッドに引数を渡すことも可能です。YAML側ではquestion_book: "%{book}を削除しますか?"
と記述し、受け取った値を展開することが可能です。(次に出てきます。)
<!-- app/views/books/show.html.erb --><div class="col-md-10 offset-md-1 mt-5"><div class="row"><div class="col-md-4 mb-4"><%= show_book_image @book %></div><div class="col-md-8"><h2><%= @book.title %></h2><ul><li><b><%= t ".price" %>: </b><%= t(".price_yen", price: (number_with_delimiter @book.price)) %></li><li><b><%= t ".publish_date" %>: </b><%= @book.publish_date %></li></ul></div></div><hr><h4><%= t ".description" %></h4><%= simple_format @book.description %><%= link_to t(".edit"), edit_book_path(@book), class: "btn btn-primary" %>+ <%= button_tag t(".delete"), class: "btn btn-warning", data: { toggle: "modal", target: "#delete-book" } %><%= link_to t(".return"), books_path, class: "btn btn-secondary" %></div>+ <div class="modal fade" id="delete-book">+ <div class="modal-dialog">+ <div class="modal-content">+ <div class="modal-header">+ <h5 class="modal-title" id="exampleModalLabel"><%= t(".modal_title") %></h5>+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">+ <span aria-hidden="true">×</span>+ </button>+ </div>+ <div class="modal-body">+ <p><%= t(".question_book", book: @book.title) %></p>+ </div>+ <div class="modal-footer">+ <button type="button" class="btn btn-secondary" data-dismiss="modal"><%= t(".chancel") %></button>+ <%= link_to t(".delete_action"), book_path(@book), method: :delete, class: "btn btn-danger" %>+ </div>+ </div>+ </div>+ </div>
config/locales/views/books/ja.yml
ファイルに以下の内容を追記します。
# config/locales/views/books/ja.ymlja:books:new:title: 書籍登録edit:title: 書籍更新show:price: 価格price_yen: "¥%{price}"publish_date: 発売日description: 商品の説明edit: 編集return: 戻る+ delete: 削除+ modal_title: 書籍削除+ question_book: "%{book}を削除しますか?"+ delete_action: 削除する+ chancel: キャンセル
書籍の一覧表示機能を作成します。
一覧表示機能を作成する場合は、indexアクションが必要になります。 先程作成したので、今回はindenアクションを作成しません。
index.html.erbは先程仮作成していたので、以下の内容を記述していきます。
PCサイズ:書籍情報が4つ横に並ぶ
タブレットサイズ:書籍情報が2つ横に並ぶ
スマホサイズ:書籍情報が1つだけ表示され、縦に並ぶ
+ <% @books.each do |book| %>+ <div class="col-lg-3 col-md-4 col-sm-6 mb-3">+ <%= show_book_image book %>+ <h4 class="mt-2"><%= link_to book.title, book_path(book) %></h4>+ <div><%= t(".price_yen", price: (number_with_delimiter book.price)) %></div>+ <p><%= truncate(book.description, length: 20) %></p>+ </div>+ <% end %>
config/locales/views/books/ja.yml
ファイルに以下の内容を追記します。
# config/locales/views/books/ja.ymlja:books:new:title: 書籍登録edit:title: 書籍更新show:price: 価格price_yen: "¥%{price}"publish_date: 発売日description: 商品の説明edit: 編集return: 戻るdelete: 削除modal_title: 書籍削除question_book: "%{book}を削除しますか?"delete_action: 削除するchancel: キャンセル+ index:+ price_yen: "¥%{price}"
以上で基本的なCRUD機能の完成となります。
リファクタリングとは、ソフトウェアの外部的振る舞い(処理結果)を保ちつつ、理解や修正が簡単になるように、内部構造を改善することです。
コントローラを確認すると、同じ記述が存在します。
@book = Book.find(params[:id])が4つのメソッドで使用されています。 @book = Book.find(params[:id])を別のメソッド(アクション)に切り出します。 今回はset_bookというアクションを作成します。 そのメソッドを必要なときに読み込む処理を追加します。 before_actionというフィルタを使用することにより、各アクション(indexやshow)を呼ぶ前に特定の処理を実行することが可能です。 特定の処理の時だけ呼び出したい時はonlyオプションを指定します。 今回のset_bookは、show、edit、update、destroyで呼び出したいので、onlyオプションを追加しています。
※beforeフィルタ以外に、afterフィルタやaroundフィルタが存在します。詳しくはこちらを参照
今回はbefore_action :set_book, only: [:show, :edit, :update, :destroy]
としていますが、全てのアクションを実行する前に何か実行する場合は、before_action :XXXXX
とします。onlyオプションは使用しません。
# app/controllers/books_controller.rbclass BooksController < ApplicationController+ before_action :set_book, only: [:show, :edit, :update, :destroy]def index@books = Book.allenddef show- @book = Book.find(params[:id])enddef new@book = Book.newenddef create@book = Book.new(book_params)@book.saveredirect_to @book, notice: "書籍を登録しました。"enddef edit- @book = Book.find(params[:id])enddef update- @book = Book.find(params[:id])@book.update(book_params)redirect_to @book, notice: "書籍を更新しました。"enddef destroy- @book = Book.find(params[:id])@book.destroyredirect_to books_path, notice: "書籍を削除しました。"endprivatedef book_paramsparams.require(:book).permit(:name, :price, :publish, :publish_date)end+ def set_book+ @book = Book.find(params[:id])+ endend
書籍は必ず一つのカテゴリーに紐付くように機能を拡張します。 以下の内容でCategoryモデルを作成します。
カラム名 | カラム名(日本語) | データ型 |
name | 名前 | string |
Categoryはseedファイルを使用して初期データをセットアップするため、CRUDの機能は実装しません。 モデルのみ作成します。
$ bin/rails g model category name:string
データベースに反映します。
$ bin/rails db:migrate
開発用のダミーデータをデータベースに登録します。 ダミーデータを登録する際に使用するファイルをシードファイルといいます。 シードファイルはdb/seeds.rb
になります。
# db/seeds.rbcategories = %w(文学・評論 人文・思想 社会・政治・法律 ノンフィクション 歴史・地理 ビジネス 科学・テクノロジー 医学 コンピュータ・IT アート 趣味・実用 スポーツ・アウトドア 資格 暮らし 旅行ガイド コミック ライトノベル)categories.each do |category|Category.create(name: category)end
データベースに反映します。
$ bin/rails db:seed
実行後、bin/rails c
実行後にCategory.all
で確認するか、GUIツールでデータが反映されていることを確認します。
モデル同士を紐つける仕組みのことを言います。 関連するモデル(app/models/XXX.rb)で宣言することで、アソシエーションが設定され、便利なメソッドが利用可能となります。 今回はBookモデルとCategoryモデルを紐づけします。
Bookは1つのCategoryに紐付いているという状態を表す場合、以下の作業が必要になります。
booksテーブルにcategoriesテーブルに紐づくカラムを追加(外部キーと言います)
モデル(app/models/XXX.rb)にアソシエーションに関するコードを記述
カラムを追加する場合は、bin/rails g migration Addカラム名Toテーブル名 カラム名:データ型
となります。 今回はcategoryモデルへの外部キーを追加するため、カラム名: データ型
の部分は、category:references
となります。 category_id:integer
でも構いませんが、外部キーを追加する場合は、紐付けるモデル名:references
としましょう。
$ bin/rails g migration AddCategoriesIdToBooks category:references
カテゴリーで検索する場合もあるので、indexを追加します。追加するカラムにindexを付与する場合は、index: true
を指定します。 そのままデータベースに反映すると、テーブルの一番最後にカラムが追加されます。after: :カラム名
やbefore: :カラム名
を使用すると、追加する位置を指定することが可能です。 今回はdescriptionの後に追加するため、after: :description
とします。 ※SQLiteでは位置を指定しても反映されません。
# db/migrate/YYYYMMDDhhmmss_add_categories_id_to_books.rbclass AddCategoriesIdToBooks < ActiveRecord::Migration[5.2]def change- add_reference :books, :category, foreign_key: true+ add_reference :books, :category, foreign_key: true, index: true, after: :descriptionendend
DBに反映します。
$ bin/rails db:migrate
app/models/book.rb
にbelongs_to :category
を追記
belongs_toの時は関連するモデル名の単数形
app/models/category.rb
にhas_many :books
を追記
has_manyの時は関連するモデル名の複数形
以下の図を使用して解説します。
@book = Book.find(1)
で1番のbookを取得します。 @book.category
を実行すると、categoriesテーブルのidが1のデータを抽出することが可能です。 bookは一つのカテゴリーに紐付いているので、category
と単数形になります。 @book.category.name
で「文学・評論」を取得することが可能です。
@category = Category.find(1)
で1番のcategoryを取得します。 @category.books
を実行すると、booksテーブルのcategory_idが1のデータを抽出することが可能です。 categoryには複数のbookが存在するため、books
と複数形になります。 @category.books
には複数件のデータが存在するので、@category.books.each
で1件ずつ取り出す事が可能です。
Book.rbにbelongs_toを記述します。
# app/models/book.rbclass Book < ApplicationRecord+ belongs_to :categoryhas_one_attached :imageattribute :new_image# 以下省略
Category.rbにhas_manyを記述します。 has_manyの場合は複数形になるので注意して下さい。
# app/models/category.rbclass Category < ApplicationRecord+ has_many :booksend
書籍登録時、更新時に、カテゴリーを選択できるようにします。
<!-- app/views/books/_form.html.erb --><%= form_with model: book, local: true do |form| %><%= render "shared/errors", obj: book %><div class="form-group"><%= form.label :title %><%= form.text_field :title, class: "form-control" %></div><div class="form-group"><%= form.label :new_image %><%= form.file_field :new_image, class: "form-control" %></div><div class="form-group"><%= form.label :price %><%= form.number_field :price, class: "form-control" %></div><div class="form-group"><%= form.label :publish_date %><%= form.date_field :publish_date, class: "form-control" %></div>+ <div class="form-group">+ <%= form.label :category_id %>+ <%= form.collection_select :category_id, Category.all, :id, :name , { include_blank: t(".include_blank") }, { class: "form-control" } %>+ </div><div class="form-group"><%= form.label :description %><%= form.text_area :description, rows: 10, class: "form-control" %></div><%= form.submit class: "btn btn-primary" %><%= link_to '戻る', books_path, class: "btn btn-secondary" %><% end %>
カラムを追加したので、YAMLファイルにも追加します。 アソシエーションを設定しているので、バリデーションがかかります。その場合は関連するモデル名をカラムと同じように追加しないと、エラーメッセージが日本語に変わりせん。 今回はcategory_idとcategoryを追加します。
# config/locales/ja.ymlja:activerecord:models:book: 書籍情報attributes:book:title: タイトルprice: 価格publish_date: 発売日description: 詳細new_image: 画像+ category_id: カテゴリー+ category: カテゴリーerrors:
コレクションセレクトで使用するinclude_blankについてもYAMLファイルに追加します。 _form.html.erbで使用するので、YAMLファイルにはformで記述します。
# config/locales/views/books/ja.ymlja:books:new:title: 書籍登録edit:title: 書籍更新+ form:+ include_blank: カテゴリーを選択して下さいshow:price: 価格
ここまでの作業で登録画面と編集画面でカテゴリーを入力できるようになりました。ただし、このままではカテゴリーを選択しても登録されません。カラムを追加した場合は、必ずストロングパラメータも追加しましょう。
# app/controllers/books_controller.rb# 省略privatedef book_paramsparams.require(:book).permit(:title, :price, :publish_date,- :description, :new_image)+ :description, :new_image, :category_id)enddef set_book@book = Book.find(params[:id])endend
実際にカテゴリーが登録できるか確認してみましょう。 詳細ページにカテゴリーを表示していないので、登録後はデータベースのデータを直接確認します。
詳細ページにカテゴリーを表示します。
アソシエーションを使用すると、@book.category.name
をカテゴリー名を取得することが可能です。 ただし、すでに登録されているデータのcategory_idがnullのため、以下のようなエラーが発生します。
&.
(ぼっち演算子)を使用することで、メソッドを呼び出せない(nameを呼び出すことができない)場合に、nilを返してくれます。 @book.category.name
を@book.category&.name
に変更することでエラーが発生しなくなります。
<!-- app/views/books/show.html.erb --><div class="col-md-10 offset-md-1 mt-5"><div class="row"><div class="col-md-4 mb-4"><%= show_book_image @book %></div><div class="col-md-8"><h2><%= @book.title %></h2><ul><li><b><%= t ".price" %>: </b><%= t(".price_yen", price: (number_with_delimiter @book.price)) %></li><li><b><%= t ".publish_date" %>: </b><%= @book.publish_date %></li>+ <li><b><%= t ".category" %>: </b><%= @book.category&.name %></li></ul></div></div><!-- 省略 -->
YAMLファイルに追記します。
# config/locales/views/books/ja.ymlja:books:new:title: 書籍登録edit:title: 書籍更新form:include_blank: カテゴリーを選択して下さいshow:price: 価格price_yen: "¥%{price}"publish_date: 発売日+ category: カテゴリーdescription: 商品の説明edit: 編集return: 戻るdelete: 削除modal_title: 書籍削除question_book: "%{book}を削除しますか?"delete_action: 削除するchancel: キャンセルindex:price_yen: "¥%{price}"
以上でカテゴリーに関する処理は完了です。
本のレビュー機能を追加する前に、ログイン機能を実装します。
ログイン管理はdeviseというgemをします。
# Gemfile# 省略# Reduces boot times through caching; required in config/boot.rbgem 'bootsnap', '>= 1.1.0', require: falsegem 'bootstrap', '~> 4.1.3'gem 'jquery-rails'gem 'mini_magick'+ gem 'devise'group :development, :test do# Call 'byebug' anywhere in the code to stop execution and get a debugger consolegem 'byebug', platforms: [:mri, :mingw, :x64_mingw]end# 省略
gemのインストール後、devise関連ファイルをプロジェクトに追加するために、devise:install
を実行します。
$ bundle install$ bin/rails g devise:install# deviseをインストールした時に、deviseに関係するメッセージが表示されます。===============================================================================Some setup you must do manually if you haven't yet:1. Ensure you have defined default url options in your environments files. Hereis an example of default_url_options appropriate for a development environmentin config/environments/development.rb:config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }In production, :host should be set to the actual host of your application.2. Ensure you have defined root_url to *something* in your config/routes.rb.For example:root to: "home#index"3. Ensure you have flash messages in app/views/layouts/application.html.erb.For example:<p class="notice"><%= notice %></p><p class="alert"><%= alert %></p>4. You can copy Devise views (for customization) to your app by running:rails g devise:views
deviseを導入した場合、ログアウト時のリダイレクト先として、rootパスの設定が必要となります。 すでに設定しているのでOKです。
#config/routes/rbRails.application.routes.draw doresources :booksroot "books#index"# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.htmlend
deviseで管理されるモデルを追加します。 今回はdeviseを使用してUserモデルを作成します。deviseを使用してモデルを作成すると、ログイン管理に必要なカラムが自動で追加されます。ユーザーを識別するキーとしてemailが使用されます。emailカラムは自動で追加されます。 後ほど追加するレビュー機能でユーザーの名前を表示したいので、nick_nameカラムを追加します。
$ bin/rails g devise User nick_name:string
マイグレーションファイルを確認すると、初期段階で複数のカラムが自動で設定されていることが確認できます。
# db/migrate/XXXXXXXXXX_devise_create_users.rb# frozen_string_literal: trueclass DeviseCreateUsers < ActiveRecord::Migration[5.2]def changecreate_table :users do |t|## Database authenticatablet.string :email, null: false, default: ""t.string :encrypted_password, null: false, default: ""## Recoverablet.string :reset_password_tokent.datetime :reset_password_sent_at## Rememberablet.datetime :remember_created_at## Trackable# t.integer :sign_in_count, default: 0, null: false# t.datetime :current_sign_in_at# t.datetime :last_sign_in_at# t.string :current_sign_in_ip# t.string :last_sign_in_ip## Confirmable# t.string :confirmation_token# t.datetime :confirmed_at# t.datetime :confirmation_sent_at# t.string :unconfirmed_email # Only if using reconfirmable## Lockable# t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts# t.string :unlock_token # Only if unlock strategy is :email or :both# t.datetime :locked_att.string :nick_namet.timestamps null: falseendadd_index :users, :email, unique: trueadd_index :users, :reset_password_token, unique: true# add_index :users, :confirmation_token, unique: true# add_index :users, :unlock_token, unique: trueendend
データベースに反映します。
$ bin/rails db:migrate
ルーティングファイルを確認すると、ルーティングが自動で追加されていることが確認できます。
# config/routes.rbRails.application.routes.draw do+ devise_for :usersresources :booksroot "books#index"# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.htmlend
Devise用の日本語ファイルも用意されているので、ダウンロードします。 以下のコマンドでダウンロード後、サーバを再起動します。(落としている方はOK)
$ curl https://gist.githubusercontent.com/kaorumori/7276cec9c2d15940a3d93c6fcfab19f3/raw/a8c4f854988391dd345f04ff100441884c324f2a/devise.ja.yml -o config/locales/devise.ja.yml
手動でダウンロードした方は以下の手順で行って下さい。
公式ページのここバージョン毎の日本語化ファイルがある
4.2.0以上のリンクから、devise.ja.yml
をダウンロード
config/locales
以下に設置する
サーバーを再起動
下準備はこれで完了。サーバーを起動している方は再起動してください。
次はヘッダーにログインしていなければログインへのリンク、ログインしていたらログアウトへのリンクを表示するように修正します。
便利なdevise関連のビューヘルパー
モデル名_signed_in?
で、ログインしているかをどうか返してくれます。
今回はUserモデルを使用しているので、user_signed_in?
となります。
以下で使用している、xxxx_xxx_xx_path
は、bin/rails routes
で確認することが可能です。 今回は非ログインの場合は「新規登録」、「ログイン」を表示し、ログイン済みの場合は「設定変更」、「ログアウト」を表示します。 ビューの文言はYAMLファイルに定義します。 追加するメニューは、上から準備に「設定変更」、「ログアウト」、「新規登録」、「ログイン」となります。
<!-- app/views/shared/_header.html.erb --><nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-3"><button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarToggler" aria-controls="navbarTogglerDemo03" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>- <%= link_to "BookShelf", root_path, class: "navbar-brand" %>+ <%= link_to t(".title"), root_path, class: "navbar-brand" %><div class="collapse navbar-collapse" id="navbarToggler"><ul class="navbar-nav mr-auto mt-2 mt-lg-0"><li class="nav-item">- <%= link_to "Home", root_path, class: "nav-link" %>+ <%= link_to t(".home"), root_path, class: "nav-link" %></li><li class="nav-item">- <%= link_to "New Book", new_book_path, class: "nav-link" %>+ <%= link_to t(".new_book"), new_book_path, class: "nav-link" %></li>+ <% if user_signed_in? %>+ <li class="nav-item">+ <%= link_to t(".edit_registration"), edit_user_registration_path, class: "nav-link" %>+ </li>+ <li class="nav-item">+ <%= link_to t(".log_out"), destroy_user_session_path, method: :delete, class: "nav-link" %>+ </li>+ <% else %>+ <li class="nav-item">+ <%= link_to t(".new_registration"), new_user_registration_path, class: "nav-link" %>+ </li>+ <li class="nav-item">+ <%= link_to t(".log_in"), new_user_session_path, class: "nav-link" %>+ </li>+ <% end %></ul><form class="form-inline my-2 my-lg-0"><input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search"><button class="btn btn-info my-2 my-sm-0" type="submit">Search</button></form></div></nav>
YAMLファイルにヘッダーで使用する内容を定義します。 まずは今回ユーザーモデルを追加したので、モデルに関するカラムを追加します。
# config/locales/ja.ymlja:activerecord:models:book: 書籍情報attributes:book:title: タイトルprice: 価格publish_date: 発売日description: 詳細new_image: 画像category_id: カテゴリーcategory: カテゴリー+ user:+ email: メールアドレス+ nick_name: ニックネーム+ avatar: プロフィール画像+ password: パスワードerrors:messages:record_invalid: "バリデーションに失敗しました: %{errors}"
ヘッダーで使用する項目をYAMLファイルに追加します。
$ mkdir config/locales/views/shared/$ touch config/locales/views/shared/ja.yml
# config/locales/views/shared/ja.yml+ ja:+ shared:+ header:+ title: BookShelf+ home: Home+ new_book: New Book+ new_registration: 新規登録+ edit_registration: 設定変更+ log_out: ログアウト+ log_in: ログイン
サーバを立ち上げ確認してみると、新規登録やログイン画面が表示されます。 ただし、ニックネーム用のフィールドが無かったり、デザインを変更したいため、カスタマイズします。
Deviseの画面をカスタマイズしたい場合は、以下のコマンド実行して、画面をローカルに取得してくる必要があります。
$ bin/rails g devise:views
画面名 | パス |
ログイン画面 |
|
新規登録画面 |
|
登録情報変更画面 |
|
パスワードを変更するための メールを送信する画面 |
|
パスワード変更画面 |
|
メール認証画面 |
|
アカウントのアンロック画面 |
|
# app/models/user.rbclass User < ApplicationRecord# Include default devise modules. Others available are:# :confirmable, :lockable, :timeoutable, :trackable and :omniauthabledevise :database_authenticatable, :registerable,:recoverable, :rememberable, :validatable+ has_one_attached :avatarend
以下の内容に修正します。
bootstrapのclassを適用する
新規登録画面を枠線で囲うためのdivタグを追加する
タイトルやボタンに表示する文言をYAMLで管理する
ニックネームとプロフィール画像のフィールドを追加する
<!-- app/views/devise/registrations/new.html.erb -->+<div class="col-md-8 offset-md-2 mt-3">+ <div class="user-box">+ <h3 class="text-center"><%= t ".title" %></h3>+ <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>+ <%= devise_error_messages! %>+ <div class="form-group">+ <%= f.label :email %><br />+ <%= f.email_field :email, autofocus: true, class: "form-control" %>+ </div>+ <div class="form-group">+ <%= f.label :nick_name %><br />+ <%= f.text_field :nick_name, class: "form-control" %>+ </div>+ <div class="form-group">+ <%= f.label :avatar %>+ <%= f.file_field :avatar, class: "form-control" %>+ </div>+ <div class="form-group">+ <%= f.label :password %>+ <% if @minimum_password_length %>+ <em>(<%= t(".minimum_password_length", length: @minimum_password_length) %>)</em>+ <% end %><br />+ <%= f.password_field :password, autocomplete: "off", class: "form-control" %>+ </div>+ <div class="form-group">+ <%= f.label :password_confirmation %><br />+ <%= f.password_field :password_confirmation, autocomplete: "off", class: "form-control" %>+ </div>+ <div class="form-group">+ <%= f.submit t(".submit"), class: "btn btn-primary" %>+ </div>+ <% end %>+ <%= render "devise/shared/links" %>+ </div>+</div>
新規登録画面のフィールドを囲う枠をCSSでデザインします。
/* app/assets/stylesheets/application.scss */@import "bootstrap";+ .user-box {+ padding: 30px;+ border: 1px solid #dcdcdc;+ background-color: #f8f8ff;+ }
新規登録画面で使用する内容を、YAMLファイルに追記します。
# config/locales/ja.ymlja:activerecord:models:book: 書籍情報attributes:book:title: タイトルprice: 価格publish_date: 発売日description: 詳細new_image: 画像image: 画像category_id: カテゴリーcategory: カテゴリーuser:email: メールアドレスnick_name: ニックネームavatar: プロフィール画像password: パスワード+ password_confirmation: パスワード再入力errors:messages:
devise関連の画面で使用する項目をYAMLファイルに追加します。
$ mkdir config/locales/views/devise/$ touch config/locales/views/devise/ja.yml
# config/locales/views/devise/ja.yml+ ja:+ devise:+ registrations:+ new:+ title: ユーザー新規登録+ submit: 新規登録+ minimum_password_length: "%{length} 文字以上を入力して下さい"
deviseで管理されているモデルに、カラムを追加した場合(今回はnick_nameとavatar)は、devise用のストロングパラメータには追加をします。 追加したカラムを、application_controller.rb
に独自のメソッドで定義します。
:devise_contoller?
とは、deviseを生成した際にできるヘルパーメソッドの一つで、deviseにまつわる画面に行った時に、という意味があります。 全ての画面でconfigure_permitted_parameters
をするのを防いでいます。 :sign_up
は新規登録用、:account_update
は設定変更用となります。
# app/controllers/application_controller.rbclass ApplicationController < ActionController::Base+ before_action :configure_permitted_parameters, if: :devise_controller?+ protected+ def configure_permitted_parameters+ devise_parameter_sanitizer.permit(:sign_up, keys: [:nick_name, :avatar])+ devise_parameter_sanitizer.permit(:account_update, keys: [:nick_name, :avatar])+ endend
アカウントが登録できる事を確認します。
以下の内容に修正します。
bootstrapのclassを適用する
新規登録画面を枠線で囲うためのdivタグを追加する
タイトルやボタンに表示する文言をYAMLで管理する
<!-- app/views/devise/sessions/new.html.erb -->+ <div class="col-md-8 offset-md-2 mt-3">+ <div class="user-box">+ <h3 class="text-center"><%= t ".title" %></h3>+ <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>+ <div class="form-group">+ <%= f.label :email %><br />+ <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %>+ </div>+ <div class="form-group">+ <%= f.label :password %><br />+ <%= f.password_field :password, autocomplete: "current-password", class: "form-control" %>+ </div>+ <% if devise_mapping.rememberable? -%>+ <div class="form-group">+ <%= f.check_box :remember_me %>+ <%= f.label :remember_me %>+ </div>+ <% end -%>+ <div class="form-group">+ <%= f.submit t(".submit"), class: "btn btn-primary" %>+ </div>+ <% end %>+ <%= render "devise/shared/links" %>+ </div>+ </div>
ログイン画面で使用する内容を、YAMLファイルに追記します。
# config/locales/ja.ymlja:activerecord:models:book: 書籍情報attributes:book:title: タイトルprice: 価格publish_date: 発売日description: 詳細new_image: 画像category_id: カテゴリーcategory: カテゴリーuser:email: メールアドレスnick_name: ニックネームavatar: プロフィール画像password: パスワードpassword_confirmation: パスワード再入力+ remember_me: ログイン情報を記録するerrors:messages:record_invalid: "バリデーションに失敗しました: %{errors}"
タイトルとボタンに表示する内容を追記します。
# config/locales/views/devise/ja.ymlja:devise:registrations:new:title: ユーザー新規登録submit: 新規登録minimum_password_length: "%{length} 文字以上を入力して下さい"+ sessions:+ new:+ title: ログイン+ submit: ログイン
ログインとログアウトが出来ること確認します。
以下の内容に修正します。
bootstrapのclassを適用する
新規登録画面を枠線で囲うためのdivタグを追加する
タイトルやボタンに表示する文言をYAMLで管理する
ニックネームとプロフィール画像のフィールドを追加する
<!-- app/views/devise/registrations/edit.html.erb-->+ <div class="col-md-8 offset-md-2 mt-3">+ <div class="user-box">+ <h3 class="text-center"><%= t ".title" %></h3>+ <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>+ <%= devise_error_messages! %>+ <div class="form-group">+ <%= f.label :email %><br />+ <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %>+ </div>+ <div class="form-group">+ <%= f.label :nick_name %><br />+ <%= f.text_field :nick_name, class: "form-control" %>+ </div>+ <% if current_user.avatar.attached? %>+ <%= image_tag current_user.avatar.variant(resize: "300x300"), class: "img-thumbnail" %>+ <% end %>+ <div class="form-group">+ <%= f.label :avatar %>+ <%= f.file_field :avatar, class: "form-control" %>+ </div>+ <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>+ <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>+ <% end %>+ <div class="form-group">+ <%= f.label :password %> <i>(<%= t ".change_email" %>)</i><br />+ <%= f.password_field :password, autocomplete: "new-password", class: "form-control" %>+ <% if @minimum_password_length %>+ <br />+ <em><%= t(".minimum_password_length", length: @minimum_password_length) %></em>+ <% end %>+ </div>+ <div class="form-group">+ <%= f.label :password_confirmation %><br />+ <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "form-control" %>+ </div>+ <div class="form-group">+ <%= f.label :current_password %> <i>(<%= t ".current_password" %>)</i><br />+ <%= f.password_field :current_password, autocomplete: "current-password", class: "form-control" %>+ </div>+ <div class="form-group">+ <%= f.submit t(".submit"), class: "btn btn-primary" %>+ </div>+ <% end %>+ <div class="form-inline mt-2">+ <%= button_to t(".account_delete"), registration_path(resource_name), data: { confirm: t(".confirm") }, method: :delete, class: "btn btn-warning" %>+ <%= link_to t(".return"), :back, class: "btn btn-secondary ml-2" %>+ </div>+ </div>+ </div>
アカウント変更画面で使用する内容を、YAMLファイルに追記します。
# config/locales/ja.ymlja:activerecord:models:book: 書籍情報attributes:book:title: タイトルprice: 価格publish_date: 発売日description: 詳細new_image: 画像category_id: カテゴリーcategory: カテゴリーuser:email: メールアドレスnick_name: ニックネームavatar: プロフィール画像password: パスワードpassword_confirmation: パスワード再入力+ current_password: 現在のパスワードremember_me: ログイン情報を記録する
# config/locales/views/devise/ja.ymlja:devise:registrations:new:title: ユーザー新規登録password_confirmation: パスワード再入力submit: 新規登録minimum_password_length: "%{length} 文字以上を入力して下さい"+ edit:+ title: ユーザー編集+ change_email: 変更したくない場合は空白のままにしてください。+ submit: 更新+ minimum_password_length: "%{length} 文字以上を入力して下さい"+ current_password: 変更を確認するために現在のパスワードが必要です+ account_delete: アカウントを削除する+ return: 戻る+ confirm: アカウント削除sessions:new:title: ログインremember_me: ログイン情報を記録するsubmit: ログイン
実際に変更できるか確認します。
画面送り機能(ページネーション)を実装します。 gem kaminari
を使用します。
Gemfileに以下の内容を追記します。
# Gemfile# 省略gem 'bootsnap', '>= 1.1.0', require: falsegem 'bootstrap', '~> 4.1.3'gem 'jquery-rails'gem 'mini_magick'gem 'devise'+ gem 'kaminari'group :development, :test do# Call 'byebug' anywhere in the code to stop execution and get a debugger consolegem 'byebug', platforms: [:mri, :mingw, :x64_mingw]end# 省略
bi
コマンドを実行します。(gemのインストール)
$ bi
コントローラを以下のように修正します。
allメソッドからpageメソッドに変更
pageメソッドの引数にparams[:page]
を指定する
rails kaminari 使い方
等で検索すると、詳しい使い方が出てきます。 本家のサイトはこちら
# app/controllers/books_controller.rbclass BooksController < ApplicationControllerbefore_action :set_book, only: [:show, :edit, :update, :destroy]def index- @books = Book.all+ @books = Book.page(params[:page])enddef new@book = Book.newend# 省略
初期設定は1ページに20件となっております。 現在はデータが少ないので、1ページ4件とします。1ページの表示件数を変更する場合はperメソッドを使用します。
class BooksController < ApplicationControllerbefore_action :set_book, only: [:show, :edit, :update, :destroy]def index- @books = Book.page(params[:page])+ @books = Book.page(params[:page]).per(4)enddef new@book = Book.newend
コントローラを修正したら、今度は書籍一覧(index.html.erb)ビューを修正します。 kaminariにpaginateメソッドが用意されているので、paginateメソッドを使用します。引数は、本の情報を保持している、@booksになります。
ページネーションは常にBootstrapの12カラム分使用したいので、classにcol-12
を指定します。
<!-- app/views/books/index.html.erb --><% @books.each do |book| %><div class="col-md-3 col-sm-6 mb-3"><%= show_book_image book %><h4 class="mt-2"><%= link_to book.title, book_path(book) %></h4><div><%= t(".price_yen", price: (number_with_delimiter book.price)) %></div><p><%= truncate(book.description, length: 20) %></p></div><% end %>+ <div class="col-12">+ <%= paginate @books %>+ </div>
サーバを起動して確認します。
$ bin/rails s
見た目があまり良くないので、Bootstrapを適用します。
以下のコマンドでBootstrap4のデザインを適用します。
$ bin/rails g kaminari:views bootstrap4# 以下のコマンドが表示されたら成功ですRunning via Spring preloader in process 58006downloading app/views/kaminari/_first_page.html.erb from kaminari_themes...create app/views/kaminari/_first_page.html.erbdownloading app/views/kaminari/_gap.html.erb from kaminari_themes...create app/views/kaminari/_gap.html.erbdownloading app/views/kaminari/_last_page.html.erb from kaminari_themes...create app/views/kaminari/_last_page.html.erbdownloading app/views/kaminari/_next_page.html.erb from kaminari_themes...create app/views/kaminari/_next_page.html.erbdownloading app/views/kaminari/_page.html.erb from kaminari_themes...create app/views/kaminari/_page.html.erbdownloading app/views/kaminari/_paginator.html.erb from kaminari_themes...create app/views/kaminari/_paginator.html.erbdownloading app/views/kaminari/_prev_page.html.erb from kaminari_themes...create app/views/kaminari/_prev_page.html.erb
サーバを起動して確認します。
$ bin/rails s
Bootstrapのデザインが適用されていることを確認します。
bootstrapのclassを指定することで中央寄せにすることが可能です。
<!-- app/views/books/index.html.erb --><% @books.each do |book| %><div class="col-md-3 col-sm-6 mb-3"><%= show_book_image book %><h4 class="mt-2"><%= link_to book.title, book_path(book) %></h4><div><%= t(".price_yen", price: (number_with_delimiter book.price)) %></div><p><%= truncate(book.description, length: 20) %></p></div><% end %>+ <div class="col-12"><%= paginate @books %>+ </div>
<!-- app/views/kaminari/_paginator.html.erb --><%= paginator.render do %><nav>- <ul class="pagination">+ <ul class="pagination justify-content-center"><%= first_page_tag unless current_page.first? %><%= prev_page_tag unless current_page.first? %><% each_page do |page| %><% if page.left_outer? || page.right_outer? || page.inside_window? %><%= page_tag page %><% elsif !page.was_truncated? -%><%= gap_tag %><% end %><% end %><%= next_page_tag unless current_page.last? %><%= last_page_tag unless current_page.last? %></ul></nav><% end %>
以下のように中央に寄っていればOKです。
ログイン機能が完成したので、ユーザーがレビューを登録できるように、レビュー投稿機能を追加します。 ユーザーは1つの本に対して1つのレビューしか投稿できません。
以下の内容でReviewモデルを作成します。 book_idはbook:references
で作成します。 user_idはuser:references
で作成します。
カラム名 | カラム名(日本語) | データ型 |
title | タイトル | string |
body | 本文 | text |
evaluation | 評価 | integer |
book_id | BookID | integer |
user_id | ユーザーID | integer |
今回も、bin/rails g resource
コマンドを使用して作成します。
$ bin/rails g resource review title:string body:text evaluation:integer book:references user:references
以下のカラムに成約を付けます。
タイトル:必須。50文字まで。
本文:必須。500文字まで。
評価:必須。
book_idとuser_idの複合インデックスを作成し、ユニーク制約を設定します。
# db/migrate/yyyyMMddHHmmss_create_reviews.rbclass CreateReviews < ActiveRecord::Migration[5.2]def changecreate_table :reviews do |t|- t.string :title- t.text :body- t.integer :evaluation+ t.string :title, null: false, length: 50+ t.text :body, null: false, length: 500+ t.integer :evaluation, null: falset.references :book, foreign_key: truet.references :user, foreign_key: truet.timestampsend+ add_index :reviews, [:book_id, :user_id], unique: trueendend
データベースに反映します。
$ bin/rails db:migrate
Reviewモデルに関連するカラムを追加します。
# config/locales/ja.ymlja:activerecord:models:book: 書籍情報attributes:book:title: タイトルprice: 価格publish_date: 発売日description: 詳細new_image: 画像category_id: カテゴリーcategory: カテゴリーuser:email: メールアドレスnick_name: ニックネームavatar: プロフィール画像password: パスワード+ review:+ title: タイトル+ body: 本文+ evaluation: 評価+ book_id: 書籍+ book: 書籍+ user_id: ユーザー+ user: ユーザー
レビューは、ある本に必ず紐付いています。その場合、ルーティングをネストさせることで、book_idをパラメータとして取得することが可能です。 今回はレビューの一覧は本の詳細ページの下部に表示させるため、except: index
を指定します
# config/routes.rbRails.application.routes.draw do- resources :reviewsdevise_for :users- resources :books+ resources :books do+ resources :reviews, except: :index+ endroot "books#index"# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.htmlend
bin/rails routes
に|
(パイプ)を使用して引数を渡すことで、検索結果を絞る事が可能となります。
$ bin/rails routes | grep reviewsbook_reviews POST /books/:book_id/reviews(.:format) reviews#createnew_book_review GET /books/:book_id/reviews/new(.:format) reviews#newedit_book_review GET /books/:book_id/reviews/:id/edit(.:format) reviews#editbook_review GET /books/:book_id/reviews/:id(.:format) reviews#showPATCH /books/:book_id/reviews/:id(.:format) reviews#updatePUT /books/:book_id/reviews/:id(.:format) reviews#updateDELETE /books/:book_id/reviews/:id(.:format) reviews#destroy
本の詳細ページの下部にレビューの登録画面へ遷移するボタンを追加します。
ログインしている時は、「レビューを投稿する」ボタンを表示しますが、ログインしていな時は、ボタンを非表示にします。
user_signed_in?
メソッドを使用して、ログイン状態を判断し、ボタンを制御します。
<!-- app/views/books/show.html.erb --><div class="col-md-10 offset-md-1 mt-5"><div class="row"><div class="col-md-4 mb-4"><%= show_book_image @book %></div><div class="col-md-8"><h2><%= @book.title %></h2><ul><li><b><%= t ".price" %>: </b><%= t(".price_yen", price: (number_with_delimiter @book.price)) %></li><li><b><%= t ".publish_date" %>: </b><%= @book.publish_date %></li><li><b><%= t ".category" %>: </b><%= @book.category&.name %></li></ul></div></div><hr><h4><%= t ".description" %></h4><%= simple_format @book.description %><%= link_to t(".edit"), edit_book_path(@book), class: "btn btn-primary" %><%= button_tag t(".delete"), class: "btn btn-warning", data: { toggle: "modal", target: "#delete-book" } %>+ <% if user_signed_in? %>+ <%= link_to t(".review_new"), new_book_review_path(@book), class: "btn btn-info" %>+ <% end %><%= link_to t(".return"), books_path, class: "btn btn-secondary" %></div><!-- 省略 -->
本の詳細ページに追加するボタンの文言を追加します。
# config/locales/views/books/ja.ymlja:books:new:title: 書籍登録edit:title: 書籍更新form:include_blank: カテゴリーを選択して下さいshow:price: 価格price_yen: "¥%{price}"publish_date: 発売日category: カテゴリーdescription: 商品の説明edit: 編集return: 戻るdelete: 削除modal_title: 書籍削除question_book: "%{book}を削除しますか?"delete_action: 削除するchancel: キャンセル+ review_new: レビューを投稿するindex:price_yen: "¥%{price}"# 省略
アソシエーションとバリデーションを設定します。
1件のReviewに関連する書籍は1件なので、belongs_to :book
1件のReviewに関連するユーザーは1件なので、belongs_to :user
評価必須
タイトル必須
本文必須
booK_idとuser_idはセットでユニーク(validates_uniqueness_ofメソッドを使用する)
# app/models/review.rbclass Review < ApplicationRecord+ belongs_to :book+ belongs_to :user+ validates :evaluation, presence: true+ validates :title, presence: true, length: { maximum: 50 }+ validates :body, presence: true, length: { maximum: 500 }+ validates_uniqueness_of :book_id, scope: :user_idend
書籍が削除された場合、関連するレビューデータも一緒に削除します。 書籍情報が削除されたレビューデータは、残っていても表示される事はないので同じタイミングで削除します。
dependent
オプションにdestroy
を指定します。 :dependent => :destroy
を省略してdependent: :destroy
とします。
# app/models/book.rbclass Book < ApplicationRecordbelongs_to :category+ has_many :reviews, dependent: :destroyhas_one_attached :imageattribute :new_image# 省略
データを登録する場合は、newとcreateアクションが必要になります。 ストロングパラメータと一緒に作成します。
# app/controllers/reviews_controller.rbclass ReviewsController < ApplicationController+ before_action :set_book, only: [:new, :create]+ def new+ @review = Review.new+ end+ def create+ @review = Review.new(review_params)+ @review.attributes = {+ book_id: params[:book_id],+ user_id: current_user.id+ }+ if @review.save+ redirect_to @review.book, notice: "レビューを登録しました。"+ else+ render :new+ end+ end+ private+ def review_params+ params.require(:review).permit(:title, :body, :evaluation)+ end+ def set_book+ @book = Book.find(params[:book_id])+ endend
レビュー登録画面と、登録画面、更新画面で使用するパーシャルフォームを作成します。
$ touch app/views/reviews/new.html.erb$ touch app/views/reviews/_form.html.erb
今回はパーシャルフォームに、@bookと@reviewを渡す
<!-- app/views/reviews/new.html.erb -->+ <div class="col-md-8 offset-md-2 mt-3">+ <h2 class="text-center"><%= t '.title' %></h2>+ <div class="mb-3">+ <%= show_book_small_image @book %>+ <span><%= @book.title %></span>+ </div>+ <hr>+ <%= render partial: 'form', locals: { book: @book, review: @review } %>+ </div>
# app/helpers/books_helper.rbmodule BooksHelperdef show_book_image(book)if book&.image&.attached?image_tag book.image.variant(resize: "280x280"), class: "img-thumbnail"elseimage_tag "no_image.png", class: "img-thumbnail", width: 280endend+ def show_book_small_image(book)+ if book&.image&.attached?+ image_tag book.image.variant(resize: "50x50"), class: "img-thumbnail"+ else+ image_tag "no_image.png", class: "img-thumbnail", width: 50+ end+ endend
レビュー登録画面で評価を1〜5まで選択できるようにします。 railsのenum(列挙型)を使用します。モデルファイルにenum カラム名: { 項目名1: 値1, 項目名2: 値2 ... }
と定義します。 カラム名は複数形となります。今回はevaluationsとなります。
# app/models/review.rbclass Review < ApplicationRecordbelongs_to :bookbelongs_to :user+ enum evaluations: { one: 1, two: 2, three: 3, four: 4, five: 5 }validates :evaluation, presence: truevalidates :title, presence: true, length: { maximum: 50 }validates :body, presence: true, length: { maximum: 500 }validates_uniqueness_of :book_id, scope: :user_idend
評価: selectメソッドを使用
タイトル: text_filedメソッドを使用
本文: text_areaメソッドを使用
selectメソッドを使用して、enumで定義した内容を選択できるようにします。 フォームオブジェクト.select :カラム名, モデル名.定義したカラム名.values, { inclede_blank等のオプション }, clsss: "指定したいクラス名"
となります。 enumの詳しい使い方はこちら
<!-- app/views/reviews/_form.html.erb -->+ <%= form_with model: [book, review], local: true do |form| %>+ <%= render "shared/errors", obj: review %>+ <div class="form-group">+ <%= form.label :evaluation %>+ <%= form.select :evaluation, Review.evaluations.values, {}, class: "form-control" %>+ </div>+ <div class="form-group">+ <%= form.label :title %>+ <%= form.text_field :title, class: "form-control" %>+ </div>+ <div class="form-group">+ <%= form.label :body %>+ <%= form.text_area :body, rows: 10, class: "form-control" %>+ </div>+ <%= form.submit class: "btn btn-primary" %>+ <%= link_to t(".back"), book, class: "btn btn-secondary" %>+ <% end %>
ビュー用のYAMLファイルを保存するディレクトリを作成します。 作成したディレクトリの中に、ja.yml
ファイルを作成します。
$ mkdir config/locales/views/reviews/$ touch config/locales/views/reviews/ja.yml
# config/locales/views/reviews/ja.yml+ ja:+ reviews:+ new:+ title: レビュー登録+ form:+ back: 戻る
以下のようなレビュー登録画面が表示されていることを確認します。
書籍照会画面の下部に、レビューの一覧を表示します。
レビューは複数件存在する可能性があるので、以下のようなコードになります。
@reviews.each do |review|review.title... 省略end
do 〜 end
の中をパーシャルフォームに切り出し、collectionオプションを使用して呼び出す事によって、eachメソッドが不要となります。 まずはパーシャルフォームを作成します。
$ touch app/views/reviews/_review.html.erb
each〜do
の中に記述しようとしていた内容をパーシャルフォームに記述します。
<!-- app/views/reviews/_review.html.erb -->+ <div>+ <div>+ <% if review.user.avatar.attached? %>+ <%= image_tag review.user.avatar.variant(resize: "60x60"), class: "img-thumbnail" %>+ <% else %>+ <%= image_tag "no_image.png", class: "img-thumbnail", width: 60 %>+ <% end %>+ <span><%= review.user.nick_name %></span>+ </div>+ <div>+ <div>+ <span class="text-warning"><%= "★" * review.evaluation %></span>+ <b><%= link_to review.title, book_review_path(review.book, review) %></b>+ </div>+ <small><%= review.created_at.strftime("%Y年%m月%d日") %></small>+ </div>+ <%= simple_format review.body %>+ </div>
書籍紹介画面から先程作成したパーシャルフォームを呼び出します。collectionを使用することで、1件1件パーシャルフォームを呼ぶことが可能となります。 パーシャルフォームの使い方はこちら
<!-- app/views/books/show.html.erb --><div class="col-md-10 offset-md-1 mt-5"><div class="row"><div class="col-md-4 mb-4"><%= show_book_image @book %></div><div class="col-md-8"><h2><%= @book.title %></h2><ul><li><b><%= t ".price" %>: </b><%= t(".price_yen", price: (number_with_delimiter @book.price)) %></li><li><b><%= t ".publish_date" %>: </b><%= @book.publish_date %></li><li><b><%= t ".category" %>: </b><%= @book.category&.name %></li></ul></div></div><hr><h4><%= t ".description" %></h4><%= simple_format @book.description %><%= link_to t(".edit"), edit_book_path(@book), class: "btn btn-primary" %><%= button_tag t(".delete"), class: "btn btn-warning", data: { toggle: "modal", target: "#delete-book" } %><% if user_signed_in? %><%= link_to t(".review_new"), new_book_review_path(@book), class: "btn btn-info" %><% end %><%= link_to t(".return"), books_path, class: "btn btn-secondary" %>+ <hr>+ <%= render partial: "reviews/review", collection: @book.reviews %></div><div class="modal fade" id="delete-book"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="exampleModalLabel"><%= t(".modal_title") %></h5><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button></div><div class="modal-body"><p><%= t(".question_book", book: @book.title) %></p></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal"><%= t(".chancel") %></button><%= link_to t(".delete_action"), book_path(@book), method: :delete, class: "btn btn-danger" %></div></div></div></div>
N+1問題とは、Tree状の情報をデータベースから読み出す際、全レコードの取得に一つ+各レコード分だけSQLを発行してしまう問題です。 こちらが参考になります。 現状、書籍一覧にアクセスすると、以下のSQLが実行されます。
最初に本の情報を4件取得する
本1冊に対して画像を取得するためのSQLが、2件ずつ発行されている
1ページに表示する本の情報を増やした場合(例えば4件→8件)にすると、画像を取得するSQLがその分多く実行される
ActiveStorage用のN+1
対策として、with_attached_image
が用意されているので、以下の通り修正します。
# app/controllers/books_controller.rbclass BooksController < ApplicationControllerbefore_action :set_book, only: [:show, :edit, :update, :destroy]def index- @books = Book.page(params[:page]).per(4)+ @books = Book.with_attached_image.page(params[:page]).per(4)enddef new@book = Book.newend# 省略end
同じように、書籍照会画面のSQLも確認します。
(例) 照会する書籍に、レビューが2件あった場合。
usersテーブルへSQLを2回発行している
書籍に紐付くレビューのユーザー情報を、その都度取得しにいくのではなく先読みする必要がある(メモリ上に保持しておく)
includesメソッドを使用することで、関連する情報を先読みすることが可能です。 書籍に紐付くユーザー情報を取得するにはレビューを経由する必要があります。
以下のように書くことで取得することが可能です。 Book.with_attached_image.includes(reviews: :user).find(params[:id])
書籍に紐付くユーザー情報を取得する際、毎回レビューを経由するとコードの記述量が増えます。その場合は、アソシエーションのhas_many :through
を使用します。
has_many :through
関連付けについては、こちらを参照下さい。
# app/models/book.rbclass Book < ApplicationRecordbelongs_to :categoryhas_many :reviews, dependent: :destroy+ has_many :users, through: :reviewshas_one_attached :imageattribute :new_image
has_many :users, through: :reviews
を追加することで以下のように記述することが可能となります。 Book.with_attached_image.includes(:users).find(params[:id])
以下のように修正します。show
アクションではset_book
を使用しないので、before_actionも修正します。
# app/controllers/books_controller.rbclass BooksController < ApplicationController- before_action :set_book, only: [:show, :edit, :update, :destroy]+ before_action :set_book, only: [:edit, :update, :destroy]def index@books = Book.with_attached_image.page(params[:page]).per(4)enddef new@book = Book.newenddef create@book = Book.new(book_params)if @book.saveredirect_to @book, notice: "書籍を登録しました。"elserender :newendenddef show+ @book = Book.with_attached_image.includes(reviews: :user).find(params[:id])end# 省略privatedef book_paramsparams.require(:book).permit(:title, :price, :publish_date,:description, :new_image, :category_id)enddef set_book@book = Book.find(params[:id])endend
レビュー照会画面を作成します。
set_reviewメソッドを作成し、before_actionで実行します。
# app/controllers/reviews_controller.rbclass ReviewsController < ApplicationController- before_action :set_book, only: [:new, :create]+ before_action :set_book, only: [:show, :new, :create]+ before_action :set_review, only: :showdef new@review = Review.newenddef create@review = Review.new(review_params)@review.attributes = {book_id: params[:book_id],user_id: current_user.id}if @review.saveredirect_to @review.book, notice: "レビューを登録しました。"elserender :newendend+ def show+ endprivatedef review_paramsparams.require(:review).permit(:title, :body, :evaluation)enddef set_book@book = Book.find(params[:book_id])end+ def set_review+ @review = Review.find(params[:id])+ endend
レビュー詳細画面用のビューファイルを作成します。
$ touch app/views/reviews/show.html.erb
<!-- app/views/reviews/show.html.erb -->+ <div class="col-md-8 offset-md-2 mt-5">+ <h2 class="text-center"><%= t ".title" %></h2>+ <div>+ <% if @review.user.avatar.attached? %>+ <%= image_tag @review.user.avatar.variant(resize: "60x60"), class: "img-thumbnail" %>+ <% else %>+ <%= image_tag "no_image.png", class: "img-thumbnail", width: 60 %>+ <% end %>+ <span><%= @review.user.nick_name %></span>+ </div>+ <hr>+ <div>+ <div>+ <span class="text-warning"><%= "★" * @review.evaluation %></span>+ <b><%= @review.title %></b>+ </div>+ <small><%= @review.created_at.strftime("%Y年%m月%d日") %></small>+ </div>+ <%= simple_format @review.body %>+ <% if user_signed_in? && current_user.id == @review.user_id %>+ <%= link_to t(".edit"), edit_book_review_path(@book, @review), class: "btn btn-info" %>+ <% end %>+ <%= link_to t(".back"), @book, class: "btn btn-secondary" %>+ </div>
YAMLファイルに追記します。
# config/locales/views/reviews/ja.ymlja:reviews:new:title: レビュー登録+ show:+ title: カスタマーレビュー+ edit: 編集+ back: 戻る
# app/controllers/reviews_controller.rbclass ReviewsController < ApplicationController- before_action :set_book, only: [:show, :new, :create]+ before_action :set_book, only: [:show, :new, :create, :edit]- before_action :set_review, only: :show+ before_action :set_review, only: [:show, :edit, :update]def new@review = Review.newenddef create@review = Review.new(review_params)@review.attributes = {book_id: params[:book_id],user_id: current_user.id}if @review.saveredirect_to @review.book, notice: "レビューを登録しました。"elserender :newendenddef showend+ def edit+ end+ def update+ if @review.update(review_params)+ redirect_to @review.book, notice: "レビューを更新しました。"+ else+ render :edit+ end+ endprivatedef review_paramsparams.require(:review).permit(:title, :body, :evaluation)enddef set_book@book = Book.find(params[:book_id])enddef set_review@review = Review.find(params[:id])endend
レビュー更新用のビューファイルを作成します。
$ touch app/views/reviews/edit.html.erb
レビュー登録画面と同じパーシャルフォームを呼びます。
<!-- app/views/reviews/edit.html.erb -->+ <div class="col-md-8 offset-md-2 mt-3">+ <h2 class="text-center"><%= t '.title' %></h2>+ <div class="mb-3">+ <%= show_book_small_image @book %>+ <span><%= @book.title %></span>+ </div>+ <hr>+ <%= render partial: 'form', locals: { book: @book, review: @review } %>+ </div>
YAMLファイルに追記します。
# config/locales/views/reviews/ja.ymlja:reviews:new:title: レビュー登録+ edit:+ title: レビュー更新show:title: カスタマーレビューedit: 編集back: 戻る
このまま戻るボタンを押すと、書籍の照会画面に戻ってしまいます。レビューの登録時は書籍の照会画面から遷移するため、同じパーシャルフォームを使用するとそのような動きになります。 遷移元の画面に戻りたい場合はlink_to
で:back
を指定します。
<%= form_with model: [book, review], local: true do |form| %><%= render "shared/errors", obj: review %><div class="form-group"><%= form.label :evaluation %><%= form.select :evaluation, Review.evaluations.values, {}, class: "form-control" %></div><div class="form-group"><%= form.label :title %><%= form.text_field :title, class: "form-control" %></div><div class="form-group"><%= form.label :body %><%= form.text_area :body, rows: 10, class: "form-control" %></div><%= form.submit class: "btn btn-primary" %>- <%= link_to '戻る', book, class: "btn btn-secondary" %>+ <%= link_to '戻る', :back, class: "btn btn-secondary" %><% end %>
レビューの削除機能を、レビュー照会画面に追加します。
削除時のメッセージはモーダルで表示します。
# app/controllers/reviews_controller.rbclass ReviewsController < ApplicationControllerbefore_action :set_book, only: [:show, :new, :create, :edit]- before_action :set_review, only: [:show, :edit, :update]+ before_action :set_review, only: [:show, :edit, :update, :destroy]def new@review = Review.newenddef create@review = Review.new(review_params)@review.attributes = {book_id: params[:book_id],user_id: current_user.id}if @review.saveredirect_to @review.book, notice: "レビューを登録しました。"elserender :newendenddef showenddef editenddef updateif @review.update(review_params)redirect_to @review.book, notice: "レビューを更新しました。"elserender :editendend+ def destroy+ @review.destroy+ redirect_to @review.book, notice: "レビューを削除しました。"+ endprivatedef review_paramsparams.require(:review).permit(:title, :body, :evaluation)enddef set_book@book = Book.find(params[:book_id])enddef set_review@review = Review.find(params[:id])endend
レビュー照会画面に削除ボタンとモーダルウィンドウを追加します。
<!-- app/views/reviews/show.html.erb --><div class="col-md-8 offset-md-2 mt-5"><h2 class="text-center"><%= t ".title" %></h2><div><% if @review.user.avatar.attached? %><%= image_tag @review.user.avatar.variant(resize: "60x60"), class: "img-thumbnail" %><% else %><%= image_tag "no_image.png", class: "img-thumbnail", width: 60 %><% end %><span><%= @review.user.nick_name %></span></div><hr><div><div><span class="text-warning"><%= "★" * @review.evaluation %></span><b><%= @review.title %></b></div><small><%= @review.created_at.strftime("%Y年%m月%d日") %></small></div><%= simple_format @review.body %><% if user_signed_in? && current_user.id == @review.user_id %><%= link_to t(".edit"), edit_book_review_path(@book, @review), class: "btn btn-info" %>+ <%= button_tag t(".delete"), class: "btn btn-warning", data: { toggle: "modal", target: "#delete-review" } %><% end %><%= link_to t(".back"), @book, class: "btn btn-secondary" %></div>+ <div class="modal fade" id="delete-review">+ <div class="modal-dialog">+ <div class="modal-content">+ <div class="modal-header">+ <h5 class="modal-title" id="exampleModalLabel"><%= t(".modal_title") %></h5>+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">+ <span aria-hidden="true">×</span>+ </button>+ </div>+ <div class="modal-body">+ <p><%= t(".question_review", review: @review.title) %></p>+ </div>+ <div class="modal-footer">+ <button type="button" class="btn btn-secondary" data-dismiss="modal"><%= t(".chancel") %></button>+ <%= link_to t(".delete_action"), book_review_path(@book, @review), method: :delete, class: "btn btn-danger" %>+ </div>+ </div>+ </div>+ </div>
# config/locales/views/reviews/ja.ymlja:reviews:new:title: レビュー登録edit:title: レビュー更新show:title: カスタマーレビューedit: 編集back: 戻る+ delete: 削除+ modal_title: レビュー削除+ question_review: "%{review}を削除しますか?"+ delete_action: 削除する+ chancel: キャンセル
scopeとはコントローラ等に記述した複数のクエリ(where等を複数結合した記述)を、モデルのメソッドとして切り出す仕組みのことを言います。
書籍一覧画面に本を取得する際、発売日(publish_date)の降順でデータを取得するようにします。 データを並び替える場合はorder
を使用します。
降順: order(カラム名: :desc)
。
昇順: order(カラム名: :asc)
。:asc
は省略可能なので、order(:カラム名)
でも可。
今回はBook.with_attached_image.page(params[:page]).per(4).order(publish_date: :desc)
となります。
# app/controllers/books_controller.rbclass BooksController < ApplicationControllerbefore_action :set_book, only: [:edit, :update, :destroy]def index- @books = Book.with_attached_image.page(params[:page]).per(4)+ @books = Book.with_attached_image.page(params[:page]).per(4).order(publish_date: :desc)end# 省略end
今後画面が増えてきた場合、他の画面でも発売日の降順(新発売の商品を優先)で表示する事が増える可能性あります。 その場合はスコープとして外に切り出して、コントローラではスコープを呼ぶようにします。
# app/models/book.rbclass Book < ApplicationRecordbelongs_to :categoryhas_many :reviews, dependent: :destroyhas_many :users, through: :reviewshas_one_attached :imageattribute :new_imagevalidates :title, presence: true, length: { maximum: 50 }validates :price, presence: true,numericality: {only_integer: true,greater_than: 1}validates :publish_date, presence: truevalidates :description, presence: true, length: { maximum: 1000 }+ scope :find_newest_books, -> { order(publish_date: :desc) }before_save doif new_imageself.image = new_imageendendend
先程作成したスコープに置き換えます。 まだリファクタリングできそうなので、page(params[:page])
の部分もスコープ内に含めます。 params[:page]
はコントローラ内でしか参照できないので、スコープにパラメータとしてparams[:page]
を渡す必要があります。
# app/controllers/books_controller.rbclass BooksController < ApplicationControllerbefore_action :set_book, only: [:edit, :update, :destroy]def index- @books = Book.with_attached_image.page(params[:page]).per(4).order(publish_date: :desc)+ @books = Book.with_attached_image.page(params[:page]).per(4).find_newest_booksend# 省略end
スコープではparams[:page]
をp
という変数を受け取ります。
# app/models/book.rbclass Book < ApplicationRecordbelongs_to :categoryhas_many :reviews, dependent: :destroyhas_many :users, through: :reviewshas_one_attached :imageattribute :new_imagevalidates :title, presence: true, length: { maximum: 50 }validates :price, presence: true,numericality: {only_integer: true,greater_than: 1}validates :publish_date, presence: truevalidates :description, presence: true, length: { maximum: 1000 }- scope :find_newest_books, -> { order(publish_date: :desc) }+ scope :find_newest_books, -> (p) { page(p).per(4).order(publish_date: :desc) }before_save doif new_imageself.image = new_imageendendend
コントローラ
# app/controllers/books_controller.rbclass BooksController < ApplicationControllerbefore_action :set_book, only: [:edit, :update, :destroy]def index- @books = Book.with_attached_image.page(params[:page]).per(4).find_newest_books+ @books = Book.with_attached_image.find_newest_books(params[:page])end# 省略end
書籍一覧画面に検索機能を追加します。 ransackというgemを使用することで、簡単に検索機能を実装することが可能です。
Gemfilemにransackを追加します。
# Gemfile# 省略gem 'bootsnap', '>= 1.1.0', require: falsegem 'bootstrap', '~> 4.1.3'gem 'jquery-rails'gem 'mini_magick'gem 'devise'+ gem 'ransack'group :development, :test do# Call 'byebug' anywhere in the code to stop execution and get a debugger consolegem 'byebug', platforms: [:mri, :mingw, :x64_mingw]end# 省略
Gemfileを修正したらbundle installを行います。
$ bi
仮で作成していたヘッダーの検索フォームを修正します。 通常のフォームはform_with
を使用しますが、ransackの場合はsearch_form_for
というビューヘルパーが用意されているので、そちらを使用します。 search_form_for
の引数として、@q
を渡します。
今回は検索フォームに入力された内容が、「タイトル、もしくは本文に含まれる」書籍を検索するので、search_field
にはtitle_or_description_cont
を設定します。 仮に検索フォームに入力された内容が、「タイトル含まれる」といった条件で検索する場合は、title_cont
となります。
<!-- app/views/shared/_header.html.erb --><nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-3"><button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarToggler" aria-controls="navbarTogglerDemo03" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><%= link_to t(".title"), root_path, class: "navbar-brand" %><div class="collapse navbar-collapse" id="navbarToggler"><ul class="navbar-nav mr-auto mt-2 mt-lg-0"><li class="nav-item"><%= link_to t(".home"), root_path, class: "nav-link" %></li><li class="nav-item"><%= link_to t(".new_book"), new_book_path, class: "nav-link" %></li><% if user_signed_in? %><li class="nav-item"><%= link_to t(".edit_registration"), edit_user_registration_path, class: "nav-link" %></li><li class="nav-item"><%= link_to t(".log_out"), destroy_user_session_path, method: :delete, class: "nav-link" %></li><% else %><li class="nav-item"><%= link_to t(".new_registration"), new_user_registration_path, class: "nav-link" %></li><li class="nav-item"><%= link_to t(".log_in"), new_user_session_path, class: "nav-link" %></li><% end %></ul>- <form class="form-inline my-2 my-lg-0">- <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">- <button class="btn btn-info my-2 my-sm-0" type="submit">Search</button>- </form>+ <%= search_form_for @q, class: "form-inline my-2 my-lg-0" do |f| %>+ <%= f.search_field :title_or_description_cont, class: "form-control mr-sm-2" %>+ <%= f.submit t(".search"), class: "btn btn-info my-2 my-sm-0" %>+ <% end %></div></nav>
検索フォームはログイン画面やアカウント設定画面の場合でも表示されるため、books_contoroller.rb
ではなくapplication_controller.rbに処理を記述します。
set_search
メソッドを作成し、before_actionで処理を呼びます。
params[:q]
とすることで、入力された検索条件を取得する事が可能
@q
に検索結果がセットされるので、その値をもとに、books_controller.rb
のindexアクションと同じ処理を記述する
# app/controllers/application_controller.rbclass ApplicationController < ActionController::Basebefore_action :configure_permitted_parameters, if: :devise_controller?+ before_action :set_search+ def set_search+ @q = Book.ransack(params[:q])+ @books = @q.result.with_attached_image.find_newest_books(params[:page])+ endprotecteddef configure_permitted_parametersdevise_parameter_sanitizer.permit(:sign_up, keys: [:nick_name, :avatar])devise_parameter_sanitizer.permit(:account_update, keys: [:nick_name, :avatar])endend
# app/controllers/books_controller.rbclass BooksController < ApplicationControllerbefore_action :set_book, only: [:show, :edit, :update, :destroy]def index- @books = Book.with_attached_image.find_newest_books(params[:page])enddef new@book = Book.newend# 省略
もし検索フォームがヘッダーではなく、app/views/books/index.html.erb
の中に存在した場合は、application_controller.rbを修正せずにbooks_contorollerを以下のように修正します。
# app/controllers/books_controller.rbclass BooksController < ApplicationControllerbefore_action :set_book, only: [:edit, :update, :destroy]def index- @books = Book.with_attached_image.find_newest_books(params[:page])+ @q = Book.ransack(params[:q])+ @books = @q.result.with_attached_image.find_newest_books(params[:page])enddef new@book = Book.newend# 省略
YAMLファイルに追加します。
# config/locales/views/shared/ja.yml---ja:shared:header:title: BookShelfhome: Homenew_book: New Booknew_registration: 新規登録edit_registration: 設定変更log_out: ログアウトlog_in: ログイン+ search: 検索