記事にユーザを追加: 小林研 Rails Tips (76)

このページの内容は以下のリポジトリに1日遅れで反映されます(記事執筆前に前日分をコミットしています)。

https://github.com/hkob/hkob_blog

はじめに

Rails Tips の 76 回目です。昨日はビューを作成したところまでで終わってしまったので、その続きを実装すると同時に記事に user を追加してみます。今日はとりあえず関連を作成するところまでを作成していきます。

Rails をはじめよう - Railsガイド

view の修正

昨日の作業で画面表示用にユーザに name を追加していました。devise/registrations/new.html.haml に name のフィールドを追加しておきます。

%h2 Sign up
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
  = render "devise/shared/error_messages", resource: resource
  .field
    = f.label :email
    %br/
    = f.email_field :email, autofocus: true, autocomplete: "email"
  .field
    = f.label :name
    %br/
    = f.text_field :name, autocomplete: "name"
  .field
    = f.label :password
    - if @minimum_password_length
      %em
        (#{@minimum_password_length} characters minimum)
    %br/
    = f.password_field :password, autocomplete: "new-password"
  .field
    = f.label :password_confirmation
    %br/
    = f.password_field :password_confirmation, autocomplete: "new-password"
  .actions
    = f.submit "Sign up"
= render "devise/shared/links"

同様に同じ場所の edit.html.haml にも name を追加しておきます。

%h2
  Edit #{resource_name.to_s.humanize}
= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f|
  = render "devise/shared/error_messages", resource: resource
  .field
    = f.label :email
    %br/
    = f.email_field :email, autofocus: true, autocomplete: "email"
  - if devise_mapping.confirmable? && resource.pending_reconfirmation?
    %div
      Currently waiting confirmation for: #{resource.unconfirmed_email}
  .field
    = f.label :name
    %br/
    = f.text_field :name, autocomplete: "name"
  .field
    = f.label :password
    %i (leave blank if you don't want to change it)
    %br/
    = f.password_field :password, autocomplete: "new-password"
    - if @minimum_password_length
      %br/
      %em
        = @minimum_password_length
        characters minimum
  .field
    = f.label :password_confirmation
    %br/
    = f.password_field :password_confirmation, autocomplete: "new-password"
  .field
    = f.label :current_password
    %i (we need your current password to confirm your changes)
    %br/
    = f.password_field :current_password, autocomplete: "current-password"
  .actions
    = f.submit "Update"
%h3 Cancel my account
%div
  Unhappy? #{button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete}
= link_to "Back", :back

ここまでできれば、 https://localhost:3000/users/sign_up にアクセスするとアカウント登録が可能になりました。ただし、今はログインしてもしなくても何も違いはありません。

記事へのユーザの追加

全てのクラスでログインを必要としたい場合には、以下のように ApplicationController の下に :authenticate_user! を追加してしまうのが簡単です。ただし、今回のブログの場合にはこれは適さないと思います。

class ApplicationController < ActionController::Base
  before_action :authenticate_user! # ブログには適さない気がするので今回はこれは記述しない

記事の一覧はログインしなくても閲覧は可能だが、自分の記事だけは編集が可能にしたいと思います。そのためには、記事に user_id 属性が必要になります。

$ bin/rails g migration add_user_id_to_article user_id:integer
      invoke  active_record
      create    db/migrate/20240213113859_add_user_id_to_article.rb

devise.en.yml は作られていますが、ja 版がありません。以下のコマンドで作っておきます。ちょっとファイル名と中身が違うようですね。日本語しか使わないので、en はそのままでいいでしょう。

$ bin/rails g devise:i18n:locale ja
      create  config/locales/devise.views.ja.yml

user_id は必須属性にしたいので、null: false とし、index も作成します。

class AddUserIdToArticle < ActiveRecord::Migration[7.1]
  def change
    add_column :articles, :user_id, :integer, null: false
    add_index :articles, :user_id
  end
end

これで db:mirate しておきます。次のテストのために spec/fixtures/users.yml を用意しておきます。

hkob:
  email: hkob@example.com
  name: hkob

other:
  email: other@example.com
  name: other

can_delete:
  email: can_delete@example.com
  name: can_delete

同様に spec/fixtures/articles.yml にも user を登録しました。

article1:
  title: "This is the first article"
  body: "This is the body of the first article"
  status: "public"
  user: hkob

can_delete:
  title: "This is the second article that can be deleted"
  body: "This is the body of the second article"
  status: "private"
  user: can_delete

この関連のテストのために、「関連確認」と「親は削除不可」を追加しました。「親は削除不可」は以下のページで紹介したものです。

context "属性に関する共通テスト" do
    subject { can_delete }

    it_behaves_like "存在制約", %i[title body]
    it_behaves_like "値限定制約", :status, %w[public private archived], %w[NG]
    it_behaves_like "削除可能制約"
    it_behaves_like "関連確認", :article, has_many: %i[user]
    it_behaves_like "親は削除不可", :article, %i[user]

このテストを書いた時点でほとんどのテストが失敗しています。articles には user という属性はないからです。

ActiveRecord::Fixture::FixtureError:
        table "articles" has no columns named "user".

この問題は article に belongs_to を追加することで解決します。

class Article < ApplicationRecord
  include Visible

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
  belongs_to :user
  has_many :comments, dependent: :destroy
end

残るエラーは user に articles というメソッドがないというものです。こちらは user.rb に has_many を記述することで解決します。今回は、親は削除不可にするために、dependent:restrict_with_error を設定することにします。これは子供を持つ親が削除できなくするための制約になります。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :trackable

  has_many :articles, dependent: :restrict_with_error
end

これでテストは全て通過しました。

おわりに

明日は、rspec でログインでのテストを実行します。その後はログインの可否、自己所有の記事の編集確認、他人所有の記事の拒否確認など順番にテストしていきます。