このページの内容は以下のリポジトリに1日遅れで反映されます(記事執筆前に前日分をコミットしています)。
https://github.com/hkob/hkob_blog
はじめに
Rails Tips の 76 回目です。昨日はビューを作成したところまでで終わってしまったので、その続きを実装すると同時に記事に user を追加してみます。今日はとりあえず関連を作成するところまでを作成していきます。
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 でログインでのテストを実行します。その後はログインの可否、自己所有の記事の編集確認、他人所有の記事の拒否確認など順番にテストしていきます。