Rails チュートリアル(機能拡張編)その2
問題文から予想した必要な作業
- Messageモデルを作成
- show_messageページ(ログインしているUserが参加しているMessageのリンク集)を作成
- Message.dialog_idsにマッチした正規表現で文字列を入力する機能を作成
- Messageのやり取りを表示するページを作成
- UserプロフィールにMessageへのリンクを作成
作業履歴
- Messageモデルを作成
以下のコマンドでMessageモデルを作成。
% rails generate model Message content:text receiver_id:integer dialog_ids:string picture:string user:references
% rails generate migration add_index_to_messages_dialog_ids
でdialog_idsにindexを加える。
class AddIndexToMessagesDialogIds < ActiveRecord::Migration[5.2] def change add_index :messages, :dialog_ids end end
MessageモデルにバリデーションとUserモデルとのリレーション関係を書く。
class Message < ApplicationRecord belongs_to :user belongs_to :receiver, class_name: "User" scope :recent_messages, ->(user_id) { where("dialog_ids like ? or dialog_ids like ?", "%-#{user_id}", "#{user_id}-%").group(:dialog_ids).order(created_at: :desc) } mount_uploader :picture, PictureUploader validates :user_id, presence: true validates :dialog_ids, presence: true validates :receiver_id, presence: true validates :content, presence: true, length: { maximum: 140 } validate :picture_size private # アップロードされた画像のサイズをバリデーションする def picture_size if picture.size > 5.megabytes errors.add(:picture, "should be less than 5MB") end end end
scope :recent_messages
は、where("dialog_ids like ? or dialog_ids like ?", "%-#{user_id}", "#{user_id}-%")でuserが参加している全てのMessageを持って来る→group(:dialog_ids)で同じdialog_ids(特定のユーザ間)のMessageの内、最後の物を取ってくる→order(created_at: :desc)で特定のユーザ間の最後のMessageをcreated_atで並び替える→特定ユーザ間のMessageの最後のやり取りを最新順に並び替えている。
Userモデルにもリレーションを書く。
class User < ApplicationRecord has_many :messages, dependent: :destroy has_many :receivers, -> { group(:receiver_id).order("messages.created_at DESC") }, through: :messages ︙ # 現在のユーザーがフォローしてたらtrueを返す def following?(other_user) following.include?(other_user) end # 現在のユーザーがフォローされていたらtrueを返す def followers?(other_user) followers.include?(other_user) end # 現在のユーザーとfollower,following関係のユーザか調べる def inter_relationship?(other_user) followers?(other_user) && following?(other_user) end private ︙ end
has_manyにscope書いて、取って来たレコードの重複を無くした(やってる事はMessageのscopeとほぼ同じ)。また inter_relationship?()
は、ログインしているユーザと渡されたユーザが相互フォロー関係であるかを確認する為の関数である。
Messageモデルのバリデーションのテストを書く。micropost_testと変わらなかったので、ソースは省略。
- show_messageページ(ログインしているUserが参加しているMessageのリンク集)を作成
ルートを作成。
Rails.application.routes.draw do ︙ resources :users do member do get :show_message end end ︙ resources :messages, only: [:create, :destroy] resources :messages, only: :show, param: :dialog_ids end
show_messageページの他にmessagesコントローラのリソース(show[特定ユーザとのMessageのやり取りを表示するページ]、create[Messageの新規作成]、destroy[Messageの削除])も書いた。
Usersコントローラにshow_messageアクションを作成。
class UsersController < ApplicationController before_action :logged_in_user, only: [..., :show_message] before_action :correct_user, only: [..., :show_message] ︙ def show_message @micropost = current_user.microposts.build @feed_items = Message.recent_messages(current_user.id).paginate(page: params[:page]) render 'static_pages/home' end private # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless current_user?(@user) end ︙ end
show_messageページはログインしているユーザ以外のアクセスをbefore_actionで弾く。動きとしては、トップページに表示されているfeed欄 : @feed_itemsをそれぞれのユーザとのMessageのやり取りの内、最新のMessage群 : Message.recent_messages(current_user.id)に変更して、トップページを再描画している。
トップページのfeedパーシャル(app/views/shared/_feed.html.erb)にshow_messageページへのリンクを作成。
<section> ︙ <a href="<%= show_message_user_path(current_user) %>" class="variable"> <strong class="recent_message"> <h3>Messages</h3> </strong> </a> </section> <% if @feed_items.any? %> <ol class="microposts"> <%= render @feed_items %> </ol> <%= will_paginate @feed_items %> <% end %>
show_messageへのリンクを踏むと、@feed_itemsがMessageオブジェクトになるので、render先がmessageパーシャル(app/views/messages/_message.html.erb)に変更される。
messageパーシャルを作成。micropostパーシャルとほぼほぼ変わらないので、ソースは省略。将来的に、micropostのデザインとは変える予定。
- Message.dialog_idsに、正規表現にマッチした文字列を入力する機能を作成
Messageモデルに機能を追加。
class Message < ApplicationRecord ︙ def Message.dialog_ids(user_id, receiver_id) if user_id < receiver_id "#{user_id}-#{receiver_id}" else "#{receiver_id}-#{user_id}" end end def Message.include_user?(user_id, dialog_ids) dialog_ids.split("-").include?(user_id.to_s) end def Message.another_user(user_id, dialog_ids) dialog_ids.split("-").reject{|n| n == user_id.to_s}.first.to_i end private ︙ end
機能説明は以下。
Message.dialog_ids()
: 新規Messageのdialog_idsに入力する値を作成する。新規Messageのuser_idとreceiver_idを使って小さい方のid-大きい方のid
と言う文字列を返す。Message.include_user?()
: dialog_idsにuser_idが含まれているかを調べる。Messageのやり取りを見るページにアクセスする時、dialog_idsに含まれていないユーザを弾くための関数。Message.another_user()
: dialog_idsに参加している相手のuser_idを返す。
- Messageのやり取りを表示するページを作成
Messagesコントローラを作成。
class MessagesController < ApplicationController before_action :logged_in_user, only: [:create, :destroy, :show] before_action :correct_user, only: :destroy before_action :correct_dialog_ids, only: :show def show receiver_id = Message.another_user(current_user.id, params[:dialog_ids]) if current_user.inter_relationship?(User.find(receiver_id)) @feed_items = Message.where("dialog_ids = ?", params[:dialog_ids]).order(created_at: :desc).paginate(page: params[:page]).per_page(10) @message = current_user.messages.build(receiver_id: receiver_id, dialog_ids: params[:dialog_ids]) else redirect_to root_url end end def create @message = current_user.messages.build(message_params) if current_user.inter_relationship?(User.find(@message.receiver_id)) if @message.save flash[:success] = "Message created!" redirect_to message_url(@message.dialog_ids) else @feed_items = [] render 'show' end else redirect_to root_url end end def destroy @message.destroy flash[:success] = "Message deleted" redirect_back(fallback_location: root_url) #redirect_to request.referrer || root_url end private def message_params params.require(:message).permit(:content, :picture, :receiver_id, :dialog_ids) end def correct_user @message = current_user.messages.find_by(id: params[:id]) redirect_to root_url if @message.nil? end # ログインしているユーザーはdialogに参加しているユーザーであるか確認 def correct_dialog_ids redirect_to root_url unless Message.include_user?(current_user.id, params[:dialog_ids]) end end
showアクションでは、特定ユーザ間のMessageのやり取りを全て取得 : @feed_items
し、新規Messageのフォーム作成の為に、Messageオブジェクト : @message
をviewに渡している。また before_action :correct_dialog_ids
は、showページにアクセスしようとしているユーザが、そのページのMessageのやり取りに参加しているかを確認している。 current_user.inter_relationship?
はログインしているユーザとreceiverが相互フォロー関係であるかを確認し、違ったらrootページにリダイレクトしている。
テスト用にMessageのfixturesを書く。
orange: content: "I just ate an orange!" receiver_id: <%= ActiveRecord::FixtureSet.identify(:lana) %> created_at: <%= 10.minutes.ago %> dialog_ids: <%= Message.dialog_ids(ActiveRecord::FixtureSet.identify(:michael), ActiveRecord::FixtureSet.identify(:lana)) %> user: michael ︙ <% 30.times do |k| %> message_<%= k %>: content: <%= Faker::Lorem.sentence(5) %> receiver_id: <%= ActiveRecord::FixtureSet.identify(":user_#{k}") %> created_at: <%= Time.zone.now %> dialog_ids: <%= Message.dialog_ids(ActiveRecord::FixtureSet.identify(:michael), ActiveRecord::FixtureSet.identify(":user_#{k}")) %> user: michael <% end %> <% 30.times do |n| %> message_lana<%= n %>: content: <%= Faker::Lorem.sentence(5) %> receiver_id: <%= ActiveRecord::FixtureSet.identify(:lana) %> created_at: <%= n.days.ago %> dialog_ids: <%= Message.dialog_ids(ActiveRecord::FixtureSet.identify(:michael), ActiveRecord::FixtureSet.identify(:lana)) %> user: michael <% end %> ︙
michaelから、同じユーザ(:lana)へのmessageを30件以上と、30人以上のユーザへ向けたmessageを作成。
Messagesコントローラのテストを書く。micropostsコントローラのテストと変わらない所は省略。
class MessagesControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @receiver = users(:malory) @message = messages(:orange) @no_following = users(:archer) end ︙ test "should redirect show when wrong user logged in" do log_in_as(@user) get message_path(Message.dialog_ids(users(:lana).id, @receiver.id)) assert_redirected_to root_url end test "should redirect show when be not following and followed from receiver" do log_in_as(@user) get message_path(Message.dialog_ids(@user.id, @no_following.id)) assert_redirected_to root_url end test "should redirect create when be not following and followed from receiver" do log_in_as(@user) assert_no_difference 'Message.count' do post messages_path, params: { message: { content: "Lorem ipsum", receiver_id: @no_following.id, dialog_ids: Message.dialog_ids(@user.id, @no_following.id)} } end assert_redirected_to root_url end end
lanaとmalory : @receiver
のMessageのやり取りを表示するページにmichael : @user
でログインした状態で入れない事をテストしている。また、新規Message作成とMessageのやり取りを表示するページへのアクセスは相互フォロー関係に無いユーザ間(michaelとarcher : @no_following
)では行えない事もテストしている。
show_massageページのテストを書く(test/integration/messages_interface_test.rb)。
class MessagesInterfaceTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @receiver = users(:malory) @another = users(:lana) end test "should put recent_message at the top in show_message" do log_in_as(@user) # 有効なメッセージ content = "This message really ties the room together" picture = fixture_file_upload('test/fixtures/rails.png', 'image/png') assert_difference 'Message.count', 1 do post messages_path, params: { message: { content: content, picture: picture, receiver_id: @receiver.id, dialog_ids: Message.dialog_ids(@user.id, @receiver.id) } } end get show_message_user_path(@user) assert_select 'div.pagination' assert_select 'input[type="file"]' # 自分のメッセージに削除リンクが付いているか確認 assert_select "a[href=?]", message_path(Message.last) # 先頭が最新のメッセージになっているか確認 assert assigns(:feed_items).first.picture? # 同じ人への有効なメッセージ content = "This message don't indlude pucture." assert_difference 'Message.count', 1 do post messages_path, params: { message: { content: content, receiver_id: @receiver.id, dialog_ids: Message.dialog_ids(@user.id, @receiver.id) } } end get show_message_user_path(@user) # 自分のメッセージに削除リンクが付いているか確認 assert_select "a[href=?]", message_path(Message.last) # 先頭が最新のメッセージになっているか確認 assert_not assigns(:feed_items).first.picture? # 同じ人へのメッセージは最新の物が一つだけ表示されている事を確認 assert_not assigns(:feed_items).pluck(:id).include?(Message.last(2).first.id) # 違う人への有効なメッセージ content = "This message is hello to lana." assert_difference 'Message.count', 1 do post messages_path, params: { message: { content: content, receiver_id: @another.id, dialog_ids: Message.dialog_ids(@user.id, @another.id) } } end get show_message_user_path(@user) # 自分のメッセージに削除リンクが付いているか確認 assert_select "a[href=?]", message_path(Message.last) # 先頭が最新のメッセージになっているか確認 assert_equal assigns(:feed_items).first.content, content # 相手からのメッセージが最初に表示されている log_in_as(@another) get show_message_user_path(@another) # 相手のメッセージに削除リンクが付いていない事を確認 assert_select "a[href=?]", message_path(Message.last), count: 0 assert_equal assigns(:feed_items).first.content, content end end
michaelでログイン→maloryへの新規Messageを作成→show_message(ログインしているUserが参加しているMessageのリンク集)へアクセス→自分のmessageに削除リンクがある事&feed欄 : :feed_items
の先頭が新規Messageになっている事を確認→先ほどと同じ人(malory)へ新規Messageを作成→show_message(ログインしているUserが参加しているMessageのリンク集)へアクセス→同じ人へのmessageは、最新の物だけが表示される事を確認→違う人(lana)へのMessageも同じように確認→相手(lana)でログイン→michaelが作成したmessageが見えているかを確認。
Messageのやり取りを表示するページ : show
を作成(app/views/messages/show.html.erb)。
<section class="messages"> <div class="user_name"> <strong> <%= link_to @message.receiver.name, @message.receiver %> </strong> <%= link_to gravatar_for(@message.receiver, size: 50), @message.receiver %> </div> </section> <% if @feed_items.any? %> <ol class="messages"> <%= render @feed_items.reverse %> </ol> <%= will_paginate @feed_items %> <% end %> <%= render 'shared/message_form' %>
showアクションから渡された特定のユーザとのMessageのやり取り : @feed_items
をmessageパーシャルに渡して表示している。reverseしているのは、画面の上から下に行くほど最近のMessageを置いて、会話っぽく見せたかったからである。messageフォームのパーシャル : app/views/shared/_message_form.erb
は、micropost_formと変わらなかったので省略。
Messageの一連の動作テストを書く(test/integration/messages_interface_test.rb)。
class MessagesInterfaceTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @receiver = users(:malory) @another = users(:lana) end ︙ test "message interface" do log_in_as(@user) # 無効なメッセージ assert_no_difference 'Message.count' do post messages_path, params: { message: { content: '', receiver_id: @another.id, dialog_ids: Message.dialog_ids(@user.id, @another.id) } } end assert_template 'messages/show' assert_select 'div#error_explanation' # 有効なメッセージ content = "To #{@another.name} from #{@user.name}" assert_difference 'Message.count', 1 do post messages_path, params: { message: { content: content, receiver_id: @another.id, dialog_ids: Message.dialog_ids(@user.id, @another.id) } } end assert_redirected_to message_path(Message.dialog_ids(@user.id, @another.id)) follow_redirect! assert_select 'div.pagination' assert_select 'input[type="file"]' # 最新のメッセージが会話の最後(一番下)に表示されている assert_equal assigns(:feed_items).reverse.last.content, content # 自分のメッセージには削除リンクが付いている assigns(:feed_items).each do |message| if message.user_id == @user.id assert_select "a[href=?]", message_path(message) else assert_select "a[href=?]", message_path(message), count: 0 end end end end
michaelでログイン→無効な新規Messageを作成→render先がshowページ&errorメッセージが表示されているかを確認→有効な新規Messageを作成→showページにリダイレクトされている&最新のMessageが一番下に表示されている&自分のMessageには削除リンクが表示されている事を確認。
- UserプロフィールにMessageへのリンクを作成
Userプロフィール app/views/users/show.html.erb
にMessageページへのリンクを作成。
<% provide(:title, @user.name) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> ︙ <% if logged_in? %> <span><%= link_to "View Messages", message_path(Message.dialog_ids(current_user.id, @user.id)) if current_user.inter_relationship?(@user) %></span> <% end %> </section> </aside> ︙ </div>
見ているUserプロフィールとログインしているユーザが相互フォロー関係だったら、Messageページへのリンクを作成している。
上手く作れているかを確認するテストを書く(test/integration/users_profile_test.rb)。
class UsersProfileTest < ActionDispatch::IntegrationTest include ApplicationHelper def setup @user = users(:michael) @inter_relationship = users(:malory) @no_following = users(:archer) end test "profile display" do log_in_as(@user) get user_path(@user) #自分のプロフィールには、メッセージページへのリンクが無い事を確認。 assert_select 'a[href=?]', message_path(Message.dialog_ids(@user.id, @user.id)), count: 0 log_in_as(@inter_relationship) get user_path(@user) #相互フォローしているユーザのプロフィールには、メッセージページへのリンクがある事を確認。 assert_select 'a[href=?]', message_path(Message.dialog_ids(@inter_relationship.id, @user.id)), count: 1 log_in_as(@no_following) get user_path(@user) #相互フォロー関係に無いユーザのプロフィールには、には、メッセージページへのリンクが無い事を確認。 assert_select 'a[href=?]', message_path(Message.dialog_ids(@no_following.id, @user.id)), count: 0 ︙ end
ログインしているユーザ自身のプロフィール、ログインしているユーザと相互フォロー関係にあるユーザのプロフィール、ログインしているユーザと相互フォロー関係に無いユーザのプロフィールページで、Messageへのリンクがそれぞれ、一つも無い、一つ有る、一つも無い事を確認している。
分かった知識一覧
has_many :シンボル名, -> { SQL文 }, through: :テーブル名
: has_manyにscopeを書ける。resources :コントローラ名, param: :シンボル名
: resourceのparamのシンボル名:id
を別のシンボル名に変更する事が出来る。Array.delete(要素)
: 返り値は削除した要素。Array.reject{|n| 条件文}
: 返り値は削除した後のArray。- sqlでのエスケープはバックスラッシュ
\
レコードオブジェクト.pluck(:カラム名)
: レコードのカラムの値を配列として取得。レコードオブジェクトのままで検索:exists?(検索レコード)
を行うと、最終的に取ってくるレコードだけでなく、途中式で取得したレコードも検索範囲に含まれてしまうので、pluckで配列化してから検索:include?(検索する値)
しよう。
改善したい所
feedとreply_feedとshow_messageのどちらのページを見ているかを分かりやすくしたい
前と同じ事を言っている。messageページはページへの遷移じゃなく、ポップアップを出して表示したい
ページを遷移させるのではなく、ウィンドウの手前に小さめのポップアップを表示して、その中でmessageのやり取りページを表示したい。messageページの機能としては、messageの相手のアイコン&名前、messageのやり取り数件表示、新規messageフォームだけで良いので、ウィンドウでやらせるには広すぎると思うので、ポップアップを出してコンパクトにまとめたい。messageページの一番下を新規messageフォームにし、真ん中のmessageの表示にスクロールを付けて、常に最新メッセージがフォームのすぐ上に来るように設定したい
ウィンドウの真ん中にスクロールページを設けて、そこにmessageのやり取りを表示したい。また、messageはpagingで取得せず、読み込んだ上限までスクロールした時に、その都度読み込むようにしたい。messageの横に既読機能を付けたい
Messageモデルにread_tagカラムを作って、初期値をfalseにする。それで、そのmessageを作ったユーザ以外(相手)がページ上で、レコードを読み込んだ時に値をtrueに変える。レコードのread_tagがtrueになった時に、ページ上でmessageの横に ‘既読’ を表示。もしかしたら、javascriptでページ内処理が必要かも?相手と自分のMessageは分かりやすく振り分けたい
自分が作成したmessageは左側、相手が作成したmessageは右側に表示したい。吹き出しっぽいアイコンを用意して、その中に入れたい(向きも調節したい)。show_messageページに相互フォローの関係にあるユーザ全員へのMessageページへのリンクを表示したい
今の仕様だと、新しいユーザへのmessageページは、そのユーザのプロフィールからしかリンクが無い。これを、相互フォロー関係が出来た時に、show_messageページにリンクを貼れるようにしたい。相互フォロー状態になったユーザ情報を何処かに保存しないと行けないので、Dialogモデルを作って、Dialog.idsカラムにMessage.dialog_idsを入れる。そして、誰かがフォローを行った時に、その相手と相互フォロー関係になっていたら、レコードを作る。Messageモデルは、dialog_idsを削除し、Dialogモデルとhas_many関係を構築し、Dialogモデルのレコードidから紐付けたMessageを取得するようにするとか?どうせなら、最初からサンプルデータを消すより、既存のサンプルデータを自動に書き換えて、上手く新しいバージョンに適用させる演習もやってみたい。show_messageページのUserへのMessageを表示するリンクには常に相手のプロフィール画像を使いたい
dialog_idsから、相手のidを取ってきて、そのままアイコンと名前持ってくれば良いだけ。すぐ出来そう^_^;show_messageのページのUserへのMessageを表示するリンクを相互フォローの関係が切れた場合、消したい(レコードは残す)
show_messageのページ上で、取得したレコード(recent_messages)のdialog_idsから、ユーザ間の現在のフォロー関係を調べても良いけど、どうせなら、Dialogモデル作った後に、inter_relationshipカラムを作って、そこにtrueかfalseを付けた方が速いのでは?とか思ってる。