Railsチュートリアル 第13章

13.1 Micropostモデル

13.1.1

演習1

インスタンスを作成しただけなので, マジックカラムの中身はnilですね.

>> micropost = Micropost.new(content: "Lorem ipsum", user_id: User.first.id)
>> micropost.created_at
=> nil

演習2

関連付けがされているモデル名がアクセサになっているんですね.

>> micropost.user
  User Load (0.5ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", (省略)>

>> micropost.user.name
=> "Example User"

演習3

データベースに保存すれば, そこでタイムスタンプが押されます.

>> micropost.save
>> micropost.created_at
=> Wed, 04 Jul 2018 04:11:06 UTC +00:00

そういえばこの場合はリロードしなくていいんですね.

13.1.2

演習1

>> micropost = Micropost.new
>> micropost.valid?
=> false
>> micropost.errors.full_messages
=> ["User must exist", "User can't be blank", "Content can't be blank"]

演習2

>> micropost.content = "a"*141
>> micropost.valid?
=> false
>> micropost.errors.full_messages
=> ["User must exist", "User can't be blank", "Content is too long (maximum is 140 characters)"]

13.1.3

演習1

マイクロポストが保存されました.

>> user = User.first

>> micropost = user.microposts.create(content: "Lorem ipsum")
   (0.3ms)  BEGIN
  Micropost Create (0.5ms)  INSERT INTO `microposts` (`content`, `user_id`, `created_at`, `updated_at`) VALUES ('Lorem ipsum', 1, '2018-07-04 04:44:53', '2018-07-04 04:44:53')
   (0.8ms)  COMMIT
=> #<Micropost id: 2, content: "Lorem ipsum", user_id: 1, created_at: "2018-07-04 04:44:53", updated_at: "2018-07-04 04:44:53">

演習2

検索する場合はidを渡さなくてはなりません.

>> user.microposts.find(micropost.id)
=> #<Micropost id: 2, content: "Lorem ipsum", user_id: 1, created_at: "2018-07-04 04:44:53", updated_at: "2018-07-04 04:44:53">

>> user.microposts.find(micropost)
ArgumentError (You are passing an instance of ActiveRecord::Base to `find`. Please pass the id of the object by calling `.id`.)

演習3

13.1.2の演習ですでにUser.firstを使ってしまったので, 最後のマイクロポストを取得して比較しました.

>> user == micropost.user
=> true
>> user.microposts.last == micropos
=> true

13.1.4

演習1, 2

以下のようになりました.

>> Micropost.first.created_at
  Micropost Load (2.6ms)  SELECT  `microposts`.* FROM `microposts` ORDER BY `microposts`.`created_at` DESC LIMIT 1
=> Wed, 04 Jul 2018 04:44:53 UTC +00:00

>> Micropost.last.created_at
  Micropost Load (0.8ms)  SELECT  `microposts`.* FROM `microposts` ORDER BY `microposts`.`created_at` ASC LIMIT 1
=> Wed, 04 Jul 2018 04:11:06 UTC +00:00

演習3

ユーザーに紐づけられたマイクロポストの配列を取得する際も, 降順で検索されていることがわかります.

>> user = User.first
>> user.microposts
  Micropost Load (0.5ms)  SELECT  `microposts`.* FROM `microposts` WHERE `microposts`.`user_id` = 1 ORDER BY `microposts`.`created_at` DESC LIMIT 11

ユーザーを削除すると, マイクロポストも削除されます.

>> user.destroy

>> Micropost.find(1)ActiveRecord::RecordNotFound (Couldn't find Micropost with 'id'=1)

13.2 マイクロポストを表示する

13.2.1

1つのマイクロポストを表示するパーシャルには, micropostというローカル変数が使われています.

# app/views/microposts/_micropost.html.erb

<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  </span>
</li>

ではこのローカル変数はどこで定義されているのでしょうか. RailsガイドAPIを見ると, 以下はすべて等価であることがわかります. ただしインスタンス変数@customerは, Customerモデルのインスタンスであることとします.

<%= render @customer %>
<%= render partial: "customer", object: @customer %>
<%= render partial: "customer", locals: { customer: @customer } %>

これらはapp/views/customers/_customer.html.erbレンダリングします. またこのパーシャル内ではパーシャル名の先頭からアンダースコアを除いたローカル変数が利用できます. つまりcustomerというローカル変数が定義されるということです. この場合は

customer = @customer

のような定義がなされます.

Micropostの場合がまさにこの例の通りでしたね. 命名規則によって, シンプルに書くことができるようになっています.

演習1

いろいろ試してみました.

>> helper.time_ago_in_words(3.seconds.ago)
=> "less than a minute"
>> helper.time_ago_in_words(3.minutes.ago)
=> "3 minutes"
>> helper.time_ago_in_words(3.hours.ago)
=> "about 3 hours"
>> helper.time_ago_in_words(3.days.ago) 
=> "3 days"
>> helper.time_ago_in_words(3.weeks.ago)  
=> "21 days"
>> helper.time_ago_in_words(3.months.ago)
=> "3 months"
>> helper.time_ago_in_words(3.years.ago)
=> "about 3 years"

演習2

単数が複数かによって多少挙動が異なりますね.

>> helper.time_ago_in_words(1.second.ago)
=> "less than a minute"
>> helper.time_ago_in_words(1.minute.ago)  
=> "1 minute"
>> helper.time_ago_in_words(1.hour.ago)
=> "about 1 hour"
>> helper.time_ago_in_words(1.day.ago) 
=> "1 day"
>> helper.time_ago_in_words(1.week.ago)  
=> "7 days"
>> helper.time_ago_in_words(1.month.ago)
=> "about 1 month"
>> helper.time_ago_in_words(1.year.ago)
=> "about 1 year"

演習3

以下の通りです.

>> microposts = User.first.microposts.paginate(page: nil)
>> microposts.class
=> Micropost::ActiveRecord_AssociationRelation

直接取得するとこうなります.

>> m = Micropost.all
>> m.class
=> Micropost::ActiveRecord_Relation

13.2.2

演習1, 2

>> (1..10).to_a.take(6)
=> [1, 2, 3, 4, 5, 6]
>> (1..10).take(6)
=> [1, 2, 3, 4, 5, 6]

演習3

>> Faker::University.name
=> "The Harris College"
>> Faker::University.name
=> "West Bergstrom Institute"

>> Faker::PhoneNumber.phone_number
=> "1-252-219-7186"
>> Faker::PhoneNumber.cell_phone
=> "1-248-219-0040"

>> Faker::Hipster.words(4)
=> ["DIY", "knausgaard", "kitsch", "pitchfork"]
>> Faker::Hipster.sentence 
=> "Poutine butcher salvia green juice vhs biodiesel."

>> Faker::ChuckNorris.fact
=> "The only pattern Chuck Norris knows is God Object."
>> Faker::ChuckNorris.fact
=> "Chuck Norris doesn't need a java compiler, he goes straight to .war"

他にも多種多様なFakerが用意されています. ドキュメントを読むだけでも楽しいですね.

13.2.3

演習1, 2

簡単なので省略します.

13.3 マイクロポストを操作する

13.3.1

演習1

ブログ等を見てみると, 単にDRYに反するから, という解答でいいんですね. メソッドのオーバーライドという仕組みがあるので, コード自体に不備があるというわけではないですね. DRYに反していることが不備なのかもしれませんが...

13.3.2

エラーメッセージパーシャルにオブジェクトを渡すために,

object: f.object

という書き方が見られます. でもおかしく感じる点がありますね. objectキーワードは13.2.1でも登場しましたが, その時とは異なる点があります. それはパーシャル内におけるローカル変数の名前です.

たとえば

render 'micropost', object: @new_micropost

のような書き方をすれば, パーシャル内に定義されるローカル変数はmicropostです.

しかしエラーメッセージパーシャルの中では, objectという名前の変数を使っていますね.

<% if object.errors.any? %>
    <div id="error_explanation">
        <div class="alert alert-danger">
            The form contains <%= pluralize(object.errors.count, "error") %>.
        </div>
        <ul>
            <% object.errors.full_messages.each do |msg| %>
                <li><%= msg %></li> 
            <% end %>
        </ul>
    </div>
<% end %>

パーシャル名と同じ名前のローカル変数は定義されていないのでしょうか. では実験してみましょう. printデバッグのような方法を取りましょう. エラーメッセージパーシャルの中に, 以下の一行を加えます.

<%= p error_messages %>

パーシャル名と同名の変数の中身を見てやろうということです. これで不正な値をフォームから送ろうと思ってページ遷移したところ, エラーが出ました.

undefined local variable or method `error_messages'

やっぱりダメでしたね.

ということはrenderメソッドの挙動をまだよく理解できていないということです. 詳しくはまた今度調べます.

演習1

以下のようにパーシャルを作成しました.

# app/views/static_pages/home.html.erb 

<% if logged_in? %>
    <%= render 'home_logged_in' %>
<% else %>
    <%= render 'welcome' %>
<% end %>
# app/views/static_pages/_welcome.html.erb

<div class="center jumbotron">
  <h1>Welcome to the Sample App</h1>

  <h2>
    This is the home page for the 
    <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a> 
    sample application.
  </h2>

  <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
</div>

<%= link_to image_tag("rails.png", alt: "Rails logo"), 'http://rubyonrails.org/' %>
# app/views/static_pages/_home_logged_in.html.erb

<div class="row">
    <aside class="col-md-4">
        <section class="user_info">
            <%= render 'shared/user_info' %>
        </section>
        <section class="micropost_form">
            <%= render 'shared/micropost_form' %>
        </section>
    </aside>
</div>

13.3.3

演習1

以下の通りです.

INSERT INTO `microposts` (`content`, `user_id`, `created_at`, `updated_at`) VALUES ('Good morning.', 1, '2018-07-05 01:07:23', '2018-07-05 01:07:23')

演習2

すべて等しいです.

>> Micropost.where("user_id=?", user.id) == user.microposts
=> true
>> Micropost.where("user_id=?", user.id) == user.feed
=> true

13.3.4

演習1, 2

省略.

13.3.5

演習1

無効な送信に対しては, 以下をコメントアウトしました.

# app/models/micropost.rb

class Micropost < ApplicationRecord
    belongs_to :user

    default_scope -> { order(created_at: :desc) }
    validates :user_id, presence: true
#    validates :content, presence: true, length: { maximum: 140 }
end

たしかにテストは失敗します.

有効な送信に対しては, 以下をコメントアウトしました.

# app/controllers/microposts_controller.rb

    def create
        @micropost = current_user.microposts.build(micropost_params)
#        if @micropost.save
#            flash[:success] = "Micropost created!"
#            redirect_to root_url
#        else
#            @feed_items = []
#            render 'static_pages/home'
#        end
    end

たしかにテストは失敗します.

投稿の削除に対しては, 以下をコメントアウトしました.

# app/controllers/microposts_controller.rb

def destroy
    #@micropost.destroy
    flash[:success] = "Micropost deleted"
    redirect_back(fallback_location: root_url)
end

確かにテストは失敗します.

異なるユーザのプロフィールに対しては, コメントアウトというか, 以下のように編集しました.

# app/views/microposts/_micropost.html.erb

<li id="micropost-<%= micropost.id %>">
    <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
    <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
    <span class="content"><%= micropost.content %></span>
    <span class="timestanp">
        Posted <%= time_ago_in_words(micropost.created_at) %> ago.
        <% if true #current_user?(micropost.user) %>
            <%= link_to "delete", micropost, method: :delete, data: { confirm: "You Sure?" } %>
        <% end %>
    </span>
</li>

確かにテストは失敗します.

演習2

省略.

13.4 マイクロポストの画像投稿

13.4.1

Gemfileを編集したら, Dockerイメージに反映させます.

$ docker-compose build

これでイメージが更新され, 同時にGemもインストールされます. あとはいつも通りコンテナを起動すればOKです.

$ docker-compose up -d

演習1

省略.

演習2

テストコードは以下のように編集しました.

# test/integration/microposts_interface_test.rb

test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select 'div.pagination'
    assert_select 'input[type=file]' # 変更点

    # invalid post
    assert_no_difference 'Micropost.count' do
        post microposts_path, params: { micropost: { content: "" } }
    end
    assert_select 'div#error_explanation'

    # valid post
    content = "This micropost really ties the room together."
    picture = fixture_file_upload('test/fixtures/rails.png', 'image/png') # 変更点
    assert_difference 'Micropost.count', 1 do
        post microposts_path, params: { micropost: { content: content, picture: picture } } # 変更点
    end
    assert @user.microposts.first.picture? # 変更点
    assert_not flash.empty?
    assert_redirected_to root_url
    follow_redirect!
    assert_match content, response.body
    
    # 省略
    
end

13.4.2

チュートリアルの中ではextension_white_listというメソッドが使われていますが, 自分のファイルを見ると, extension_whitelistというメソッドでした. アンダースコアの分だけ異なります.

carrierwaveのドキュメントには後者が載っていました. バージョンが上がったことにより名前が変わったんでしょう.

演習1

そもそもそんな大きな画像ねーよと思いましたが, 10mb 画像と検索したら出てきました.

jQueryによるアラートは出ましたが, 投稿自体はできてしまいました. Railsサーバーを再起動したところ, サーバー側のエラーが表示されるようになりました.

演習2

そもそもブラウザからファイルを指定する場合, 許可された拡張子を持つファイルしか選択できませんね.

13.4.3

Debian環境を使っているので, パッケージのインストールは以下のコマンドで行いました. パッケージ名は全て小文字でOKです.

apt-get install -y imagemagick

Dockerイメージに反映させたい場合は, Railsコンテナ用のDockerfileを編集します.

FROM ruby:2.5.1

RUN apt-get update -qq && apt-get -y install \
        vim \
        build-essential \
        libpq-dev \
        nodejs \
        mysql-client \
        imagemagick    # ここを追加!

RUN mkdir /myapp

COPY Gemfile /myapp
COPY Gemfile.lock /myapp

WORKDIR /myapp
RUN gem install bundler && bundle install

ENV COLUMNS=200 LINES=50

DockerfileやGemfileを変更した場合は以下のコマンドを実行します.

$ docker-compose build

あとはいつも通りコンテナを起動すればOKですね.

$ docker-compose up -d

演習1

省略.

演習2

エラー出ない...

rails.pngは小さい画像ですから, 問題なくアップロードされてしまいますよね. だからエラーは出なくて正解な気がしてしまうのですが. とはいえテスト環境ではリサイズが行われないようにしておきます.

13.4.4

この節はやっていません.