前 基礎Ruby on Rails Chapter8 アクション・コールバック/マイアカウントページの作成
次 基礎Ruby on Rails Chapter9 ActiveRecord/スコープ/ページネーション
ニュース記事の表示と編集
Articleモデルの作成
- ニュース記事を表示するためのモデル
article
を作成する。
$ bin/rails g model article
invoke active_record
create db/migrate/20180929222512_create_articles.rb
create app/models/article.rb
- マイグレーションスクリプトができるので、カラムを定義する。
db/migrate/20180929222512_create_articles.rb
class CreateArticles < ActiveRecord::Migration[5.2]
def change
create_table :articles do |t|
t.string :title, null: false # タイトル
t.text :body, null: false # 記事
t.datetime :released_at, null:false # 掲載開始日時
t.datetime :expired_at # 掲載終了日時
t.boolean :member_only, null: false, default: false # 会員のみフラグ
t.timestamps null:false
end
end
end
- マイグレーションを実行する。
$ bin/rails db:migrate
- シードデータを読み込ませるための設定をする。
-
articles
を追記する。(現在の開発モード(develop等)の下にファイルがあればシードデータを投入する)
db/seeds.rb(一部)
table_names = %w(members articles)
(省略)
- シードデータの新規作成をする。
-
8.days.ago.advance(days: idx)
は、現在日時の8日前のidx日後を返す。
db/seeds/development/articles.rb
body =
"Morning Gloryが4対2でSunflowerに勝利。\n\n" +
"2回表、6番渡辺の二塁打から7番山田、8番高橋の連続タイムリーで2点先制。" +
"9回表、ランナー一二塁で2番田中の二塁打で2点を挙げ、ダメを押しました。\n\n" +
"投げては初先発の山本が7回を2失点に抑え、伊藤、中村とつないで逃げ切りました。"
0.upto(9) do |idx|
Article.create(
title: "練習試合の結果#{idx}",
body: body,
released_at: 8.days.ago.advance(days: idx),
expired_at: 2.days.ago.advance(days: idx),
member_only: (idx % 3 == 0)
)
end
0.upto(29) do |idx|
Article.create(
title: "Article#{idx+10}",
body: "blah, blah, blah...",
released_at: 100.days.ago.advance(days: idx),
expired_at: nil,
member_only: false
)
end
- 開発用のシードデータを投入する。
$ bin/rails db:rebuild
バリデーションの追加
- タイトル、本文、掲載開始日時は空ではならない。
- タイトルは80文字以下。
- 本文は2000文字以下。
app/models/article.rb
class Article < ApplicationRecord
validate :title, :body, :released_at, presence: true
validate :title, length: { maximum: 80 }
validate :body, length: { maximum: 2000 }
end
- ロケールファイルに、articleのモデル名とカラム名を日本語で追加。
config/locales/ja.yml(一部)
ja:
activerecord:
models:
member: 会員情報
article: ニュース記事
attributes:
#(省略)
article:
title: タイトル
body: 本文
released_at: 掲載開始日時
expired_at: 掲載終了日時
member_only: 会員限定
errors:
#(省略)
- バリデーションが正しく機能していることを確認する。
$ bin/rails c
irb(main):004:0> article = Article.first
Article Load (0.3ms) SELECT "articles".* FROM "articles" ORDER BY "articles"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Article id: 1, title: "練習試合の結果0", body: "Morning Gloryが4対2でSunflowerに勝利。\n\n2回表、6番渡辺の二塁打か ら7番山...", released_at: "2018-09-21 23:06:51", expired_at: "2018-09-27 23:06:51", member_only: true, created_at: "2018-09-29 23:06:51", updated_at: "2018-09-29 23:06:51">
irb(main):008:0> article.title = "A" * 80
=> "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
irb(main):009:0> article.valid?
=> true
irb(main):011:0> article.title = "A" * 81
=> "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
irb(main):013:0> article.valid?
=> false
irb(main):014:0> article.errors.full_messages_for(:title)
=> ["タイトルは80文字以内で入力してください"]
irb(main):015:0>
ルーティングの設定
-
articles
をルーティングに設定する。複数形に気を付ける。 - 7つのアクションなのでオプションは指定しない。
config/routes.rb(一部)
#(省略)
resource :session, only: [:create, :destroy]
resource :account, only: [:show, :edit, :update]
resource :password, only: [:show, :edit, :update]
resources :articles
end
-
routes
コマンドで、ルーティングを確認する。
$ bin/rails routes -c article
Prefix Verb URI Pattern Controller#Action
articles GET /articles(.:format) articles#index
POST /articles(.:format) articles#create
new_article GET /articles/new(.:format) articles#new
edit_article GET /articles/:id/edit(.:format) articles#edit
article GET /articles/:id(.:format) articles#show
PATCH /articles/:id(.:format) articles#update
PUT /articles/:id(.:format) articles#update
DELETE /articles/:id(.:format) articles#destroy
ArticlesControllerの作成
-
bin/rails g controller articles
でコントローラを作成する。
$ bin/rails g controller articles
create app/controllers/articles_controller.rb
invoke erb
create app/views/articles
- アクセス制限する。indexとshowはログインしていない状態でも見えるようにする。
app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
before_action :login_required, except: [:index, :show]
end
indexアクション
- 掲載開始、終了日時は考えずに、まずは全てを新しいもの順に表示する。
app/controllers/articles_controller.rb(一部)
def index
@article = Article.order(released_at: :desc)
end
- indexのviewを作成する。new、show、edit、destoroyアクションへのリンクも付ける。
app/views/articles/index.html.erb
<% @page_title = "ニュース記事一覧" %>
<h1><%= @page_title %></h1>
<div class="toolbar"><%= link_to "新規作成", :new_article %></div>
<% if @articles.present? %>
<table class="list">
<thead>
<tr>
<th>タイトル</th>
<th>日時</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<% @articles.each do |article| %>
<tr>
<td><%= link_to article.title, article %></td>
<td><%= article.released_at.strftime("%Y/%m/%d %H:%M") %></td>
<td>
<%= link_to "編集", [:edit, article] %> |
<%= link_to "削除", article, method: :delete, data: { confirm: "本当に削除しますか?" } %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>ニュースがありません。</p>
<% end %>
- ヘッダから
articles
へのindexのリンクを付ける。
app/views/shared/_header.html.erb(一部)
<%= menu_link_to "ニュース", :articles %>
- ヘッダのリンクをクリックすると、indexが表示された。
showアクション
app/controllers/articles_controller.rb(一部)
def show
@article = Article.find(params[:id])
end
- showのviewを作成する。
-
simple_format
は文字列を次のルールで変換する。- 2個以上の連続する改行を段落とみなし、
<p>
タグで囲む。 - 単独の改行は
<br>
タグにする。 - 危険なタグを取り除く。(
<script>
など) - HTMLで特殊な意味を持つ記号をエスケープする。(
&<>
)
- 2個以上の連続する改行を段落とみなし、
app/views/articles/show.html.erb
<% @page_title = @article.title %>
<h1><%= @page_title %></h1>
<% if current_member %>
<div class="toolbar"><%= link_to "編集", [:edit, @article] %></div>
<% end %>
<table class="attr">
<tr>
<th width="100">タイトル</th>
<td><%= @article.title %></td>
</tr>
<tr>
<th>本文</th>
<td><%= simple_format(@article.body) %></td>
</tr>
<tr>
<th>掲載開始日時</th>
<td><%= @article.released_at.strftime("%Y/%m/%d %H:%M") %></td>
</tr>
<tr>
<th>掲載終了日時</th>
<td><%= @article.expired_at.try(:strftime, "%Y/%m/%d %H:%M") %></td>
</tr>
<tr>
<th>会員限定</th>
<td><%= @article.member_only? ? "○" : "-" %></td>
</tr>
</table>
newアクションとeditアクション
- コントローラのnewアクションとeditアクションを作成する。
app/controllers/articles_controller.rb(一部)
def new
@article = Article.new
end
def edit
@article = Article.find(params[:id])
end
- newアクションのviewを作成する。部分テンプレート
_form
を使用する。
app/views/articles/new.html.erb
<% @page_title = "ニュース記事の新規登録" %>
<h1><%= @page_title %></h1>
<%= form_for @article do |form| %>
<%= render "form", form: form %>
<div><%= form.submit %></div>
<% end %>
- editアクションのviewを作成する。部分テンプレート
_form
を使用する。
app/views/articles/edit.html.erb
<% @page_title = "ニュース記事の編集" %>
<h1><%= @page_title %></h1>
<p><%= link_to "記事の詳細に戻る", @article %></p>
<%= form_for @article do |form| %>
<%= render "form", form: form %>
<div><%= form.submit %></div>
<% end %>
- 部分テンプレート
_form
を作成する。
app/views/articles/_form.html.erb
<%= render "shared/errors", obj: @article %>
<table class="attr">
<tr>
<th><%= form.label :title %></th>
<td><%= form.text_field :title, size: 20 %></td>
</tr>
<tr>
<th><%= form.label :body %></th>
<td><%= form.text_area :body, rows: 10, cols: 45 %></td>
</tr>
<tr>
<th><%= form.label :released_at, for: "article_released_at_li" %></th>
<td><%= form.datetime_select :released_at, start_year: 2000, end_year: Time.current.year + 1, use_month_numbers: true %></td>
</tr>
<tr>
<th><%= form.label :expired_at, for: "article_expired_at_at_li" %></th>
<td><%= form.datetime_select :expired_at, start_year: 2000, end_year: Time.current.year + 1, use_month_numbers: true %></td>
</tr>
<tr>
<tr>
<th><%= Article.human_attribute_name(:member_only) %></th>
<td>
<%= form.check_box :member_only %>
<%= form.label :member_only %>
</td>
</tr>
</table>
- 記事が編集可能なことを確認する。
createアクションとupdateアクションとdestroyアクション
- createアクションとupdateアクションとdestroyアクションを実装する。
app/controllers/articles_controller.rb(一部)
def create
@article = Article.new(params[:article])
if @article.save
redirect_to @article, notice: "ニュース記事を登録しました。"
else
render "new"
end
end
def update
@article = Article.find(params[:id])
@article.assign_attributes(params[:article])
if @article.save
redirect_to @article, notice: "ニュース記事を更新しました。"
else
render "edit"
end
end
def destroy
@article = Article.find(params[:id])
@article.destroy
redirect_to :articles
end
Active Recordコールバック
Active Recordコールバックとは
- アクション・コールバックと同様に、Active Recordコールバックはバリデーションや保存の前後に処理を行うことができる。
- モデルにコールバックを設定する書き方は2種類ある。
- ブロックで登録する方法
class Article < ApplicationRecord
before_save do
# 保存する前に行う処理
end
end
- プライベートメソッドのシンボルを登録する方法
class Article < ApplicationRecord
before_save :do_something
private def do_something
# 保存する前に行う処理
end
end
メソッド | 実行されるタイミング |
---|---|
before_validation | バリデーション前 |
after_validation | バリデーション後 |
before_save | 保存前(新規作成、更新の両方) |
before_create | 新規作成前 |
before_update | 更新前 |
after_update | 更新後 |
after_create | 新規作成後 |
after_save | 保存後(新規作成、更新の両方) |
- before_validationとafter_validationは、onオプションに、:createまたは:updateを指定すると、新規作成または更新の場合だけコールバックを行う。
before_validation on: :create do
# 新規作成のバリデーションの前に行う処理
end
no_expiration属性
- 上記で作成した掲載終了日時は、空にすることができない。
- そこで、「掲載終了日時を設定しない」チェックボックスを追加して、チェックされている場合掲載終了日時を保存しない。
- チェックボックスに対応する属性
no_expiration
は、articleテーブルのカラムではなく、クラスの属性として実装する。 - no_expirationのゲッターは、expired_atがnilがとうか。
- no_expirationのセッターは、trueか1の時trueにして、それ以外はfalseにする。
app/models/article.rb(一部)
def no_expiration
expired_at.nil?
end
def no_expiration=(val)
@no_expiration = val.in?([true, "1"])
end
- テーブルのカラムにないフィールドを作るには、フィールド名と同名のアクセサメソッドをモデルクラスに加えて属性を作る。
コールバックを使って掲載終了日時を消す
- validationの前に、
@no_expiration
がtrueの場合は、expired_atにnilを入れる。
app/models/article.rb(一部)
before_validation do
self.expired_at = nil if @no_expiration
end
フォームの書き換え
- 掲載終了日時を設定しないのチェックボックスを追加する。
app/views/articles/_form.html.erb(一部)
<tr>
<th><%= form.label :expired_at, for: "article_expired_at_at_li" %></th>
<td>
<div>
<%= form.check_box :no_expiration %>
<%= form.label :no_expiration %>
</div>
<div>
<%= form.datetime_select :expired_at, start_year: 2000, end_year: Time.current.year + 1, use_month_numbers: true %>
</div>
</td>
</tr>
- ロケールデータに、no_expirationを追加する。
config/locales/ja.yml(一部)
expired_at: 掲載終了日時
no_expiration: 掲載終了日時を設定しない
- チェックボックスが追加され、保存すると空になることを確認。