ソート機能(ransack)

内容

投稿の並び順を変えたいので、ransackを用いてソート機能を実装する

ソート機能

1. counter_cultureの導入

投稿をいいねの多い順に並べ替えたいため、postsテーブルにlikes_countというカラムを作りたい
Railsにはcounter_cacheという機能がデフォルトである
→すでにpostsテーブルにデータがある場合、likes_countの値は自動更新されない!
→counter_cultureはcounter_cacheの弱点も補強してくれるような高機能のgem、ということで導入
gemfile

gem 'counter_culture'

ターミナル

% bundle install
% rails s

マイグレーションファイルを生成
ターミナル

% rails generate counter_culture Post likes_count

以下のようなマイグレーションファイルが生成される
db/migrate/2021xxxxxxxx_add_likes_count_to_posts.rb

class AddLikesCountToPosts < ActiveRecord::Migration[6.0]
  def self.up
    add_column :posts, :likes_count, :integer, null: false, default: 0
  end

  def self.down
    remove_column :posts, :likes_count
  end
end

ターミナル

% rails db:migrate

モデルには以下のように記載
model/like.rb

class Like < ApplicationRecord
  belongs_to :post
  counter_culture :post
end

model/post.rb

class Post < ApplicationRecord
  has_many :likes, dependent: :destroy
end

その後、すでにpostsテーブルにたくさんデータがある等の場合、コンソールで以下の様に実行
ターミナル

% rails c
pry(main)> Like.counter_culture_fix_counts

こうすることで、postsテーブルのlikes_countのカラムの中身がアップデートされる
これは便利!

2. ビューの編集(ransack)

1.でいいねの数がpostsテーブルに取得できたので、ビューで人気順に並べ替えれるように編集
その他、投稿の新しい順や距離の長い順等も合わせて並べ替えができるように記述
_search.html.erb

<%= search_form_for @q, url: root_path, class: "search-form" do |f| %>
  <%= f.select(:sorts, {'投稿が新しい順': 'created_at desc', '投稿が古い順': 'created_at asc', '距離が長い順': 'distance desc', '距離が短い順': 'distance asc', '人気順': 'likes_count desc'}, { selected: params[:q][:sorts] }, class: "sort-input") %>
  <%= f.submit "検索", class: "search-btn" %>
<% end %>

※前回の投稿では、検索後にsearch.html.erbに遷移するようにurl等も指定していたが、すべて投稿一覧ページで完結すると思いindex.html.erbに遷移するよう記述を変更した

3. 投稿一覧ページのビューを編集

サイドバー(検索機能)とメインコンテンツ(投稿一覧)に分けて表示

次回

他人の投稿にコメントできるようにコメント機能を実装

投稿検索機能(ransackの導入)

内容

コメント機能の前に、投稿検索機能を実装する

投稿検索機能

1. ransackを導入

ransackというgemを用いて検索機能を簡単に実装する
gemfile

gem 'ransack'

ターミナル

% bundle install
% rails s

2. 検索機能のMVC設定

collectionとmemberを用いて、ルーティングを設定する
これを使用すると、生成されるルーティングのURLと実行されるコントローラーを任意にカスタムできる
collectionはルーティングに:idがつかない、memberは:idがつくという違いがある
routes.rb

resources :posts, except: :index do
  collection do
    get 'search'
  end
end

コントローラーの編集
posts_controller.rb

before_action :set_search, only: %i[index search]

private
def set_search
  @q = Post.ransack(params[:q])
end

投稿一覧ページで検索ができるようにビューを編集(部分テンプレート)
_search.html.erb

<%= search_form_for @q, url: search_posts_path, class: "search-form" do |f| %>
# gteqとlteqオプションを用いると、範囲を指定した検索が可能
  <%= f.number_field :distance_gteq, placeholder: "例)5", class: "distance-search" %>km以上〜
  <%= f.number_field :distance_lteq, placeholder: "例)10", class: "distance-search" %>km以下
# eq_anyオプションを用いると、check_boxのどれかに当てはまるものを検索可能
  <% choices = ["多い", "やや多い", "どちらともいえない", "やや少ない", "少ない"] %>
  <% choices.each do |c| %>
    <%= f.check_box :traffic_eq_any, {multiple: true}, c, nil %>
    <%= f.label :traffic, c %><br>
  <% end %>
# contを用いると、指定したカラムの単語の一部分を含むものを検索可能
  <%= f.search_field :comment_cont, placeholder: "投稿を検索する", class: "search-input" %>
  <%= f.submit "検索", class: "search-btn" %>
<% end %>

3. ソート機能のMVC設定

次回

ソート機能実装しようとしたが、いろんなとこで詰まってしまったため持ち越し
ransackを使った方がやりやすいのか使わない方がいいのか、そこも考えて取り組みたい
あと今回実装した検索機能で、検索したらsearch.html.erbをビューとして返すようにしたが、indexアクションいじってindex.html.erbを使い回した方がいいかと思った(その場合、上記の記述は変わる)

参考

ransackを用いた複数カラムのキーワード検索

複数のカラムでキーワード検索したい場合は、以下のように記述する

# orを用いてカラムを複数指定
<%= f.search_field :address_or_comment_cont, placeholder: "投稿を検索する", class: "search-input" %>

いいね機能

内容

いいね機能を投稿一覧表示と詳細表示ページにつける

いいね機能

Ajaxを用いて非同期通信でいいね機能を実装する

1. モデルの作成

Likeモデルを作成し、マイグレーションファイルを編集
ターミナル

% rails g model like

likes.rb

t.references :user, null: false, foreign_key: true
t.references :post, null: false, foreign_key: true

ターミナル

% rails db:migrate

2. アソシエーションの記述

3. バリデーションの設定

1つの投稿にユーザーが1回しかいいねできないように、バリデーションを設定する
like.rb

validates :post_id, uniqueness: {scope: :user_id}

4. ルーティングの設定

投稿に紐づくようにルーティングをネストさせる
routes.rb

resources :posts, except: :index do
  resource :likes, only: %i[create destroy]
end

5. コントローラーの設定

コントローラーを作成、createアクションとdestroyアクションを記述
ターミナル

% rails g controller likes

likes_controller.rb

before_action :post_find

def create
  Like.create(user_id: current_user.id, post_id: @post.id)
end

def destroy
  @like = Like.find_by(user_id: current_user.id, post_id: @post.id)
  @like.destroy
end

private
def post_find
  @post = Post.find(params[:post_id])
end

6. ビューの設定

Userモデルにメソッドを追加しておく
user.rb

def liked_by?(post_id)
  likes.where(post_id: post_id).exists?
end

部分テンプレートを作成し、いいねボタンを押せるようにする
ユーザーがいいねボタンをすでに押しているかどうかで条件分岐
_like.html.erb

<% if user_signed_in? %>
  <% if current_user.liked_by?(post.id) %>
    <%= link_to post_likes_path(post.id), method: :delete, remote: true do %>
      <i class="fas fa-heart unlike-btn"></i>
    <% end %>
    <span><%= post.likes.count %></span>
  <% else %>
    <%= link_to post_likes_path(post.id), method: :post, remote: true do %>
      <i class="far fa-heart like-btn"></i>
    <% end %>
    <span><%= post.likes.count %></span>
  <% end %>
<% else %>
  <i class="far fa-heart like-btn"></i><span><%= post.likes.count %></span>
<% end %>

投稿一覧(および詳細)ページでは以下のように記述
このidはjsで操作する際に使用する
index.html.erb

<div id="likes-<%= post.id %>">
  <%= render "shared/like", post: post %>
</div>

7. Ajax(非同期通信)

まずは前提知識からまとめる
- romote: trueとは
一言で言うと、リクエストがhtml形式ではなくjs形式になる
- .js.erbファイルとは
一言で言えば、Rubyを埋め込むことのできるjsファイル
このファイル形式ではコントローラーで定義したインスタンス変数を記述することが可能となる
コントローラーは「app/views/コントローラ名/アクション名.リクエストの形式.〇〇」というファイルを探しに行くらしい
よって、remote: trueでリクエストの種類からレスポンスの種類までが異なることとなる
ファイルは自分でcreate.js.erbとdestroy.js.erbを作成(記述内容は両方とも同じ)
create.js.erb、destroy.js.erb

$('#likes-<%= @post.id %>').html("<%= j(render "shared/like", post: @post ) %>");

上記のjsではid名で操作するビューを取得しているが、このid名が、先ほどビューに入れたid名を取得している
※ちなみにrenderの前に記載の「j」は特殊文字エスケープするためのメソッド

8. rubocop

次回

投稿詳細ページでコメント機能を実装

参考(今回の記事とは無関係)

1. 文字列を途中で区切って、「…」等と表記したい時

truncateを使用して文字列を切り捨てる

# 例)文字数30文字で切り捨てを行う場合
<%= truncate(post.comment, length: 30) %>

2. link_toでアラートを表示させたい時

data-confirmを使用する

# 例)投稿削除ボタンを押した時にアラートを表示したい時
<%= link_to "削除", post_path(post.id), method: :delete, data: {confirm: "投稿を削除してもよろしいですか?"} %>

投稿削除機能および投稿詳細機能

内容

投稿削除機能および投稿詳細機能の実装を行う

投稿削除機能

1. MVCの設定

ルーティングとビューを編集
コントローラーは以下の通り、削除できたらユーザートップページに遷移するように設定
posts_controller.rb

def destroy
  @post = Post.find(params[:id])
  @post.destroy
  redirect_to user_path(current_user.id)
end

2. rubocop

投稿編集機能

1. MVCの設定

ルーティング、コントローラー、ビューを編集
ビューではtable、tr、th、td要素を用いて表を作成した

2. rubocop

次回

いいね機能を投稿一覧表示と詳細表示ページにつける

投稿編集機能

内容

投稿編集機能の実装

投稿編集機能

1. MVCの設定

ルーティングを設定し、ビューは新規投稿のものを使い回し
投稿が編集できたらユーザーのマイページに遷移するようコントローラーを編集する
posts_controller.rb

def edit
  @post = Post.find(params[:id])
end

def update
  @post = Post.find(params[:id])
  if @post.update(post_params)
    redirect_to user_path(current_user.id)
  else
    render :edit
  end
end

2. 編集ページへ遷移するボタンをjQueryで導入

ユーザーのマイページから投稿編集ページへ遷移できるよう、マウスオーバーするとドロップボタンが出てくるように実装
前回はJavaScriptを使用したが、jQueryの方が簡単に実装できるためこちらを用いる
まずはjQueryのインストール
ターミナル

% yarn add jquery

Rails5以前はjquery-railsというGemをインストールするのが普通だったらしい、Rails6から標準装備されているWebpackerで管理する際はyarnコマンドを使用してインストールする→簡単に導入できる

次にWebpackの設定ファイルでjQueryを管理下として認定
config/webpack/environment.js

const { environment } = require('@rails/webpacker')
// 以下追記
const webpack = require('webpack')
environment.plugins.prepend('Provide',
    new webpack.ProvidePlugin({
        $: 'jquery/src/jquery',
        jQuery: 'jquery/src/jquery'
    })
)
// ここまで
module.exports = environment

application.jsでjQueryを呼び出せるようにする
application.js

// 追記
require('jquery')

以上で導入完了

jsファイルにjQueryのコードを記述
最初以下のように記述していたが、これだとクリックしたclass要素すべてに対してtoggleの表示/非表示が実行されてしまった

$(document).on("click", '.leader', function() {
  $('.leader-lists').toggle();
});

よってthisを入れてクリックした1つだけに対して動作するように修正

$(document).on("click", '.leader', function() {
  $('.leader-lists', this).toggle();
});

以上のようにjQueryを用いると簡潔にコードが書けるようになる

3. rubocop

次回

投稿削除機能から

参考

1. FontAwesomeの使用

FontAwesomeでアイコンを使用したかったが、なぜかサイトに表示できず
FontAwesomeのver.が6になっていてHTMLの表記方法が変わっていた
→自分のkitコードの最新バージョンがver.5であったため、ver.5のHTML表記で書かないといけない = まだ6に対応していない? or kitコードを何らかの手段で最新にアップデートする必要がある?

2. 部分テンプレートに変数を渡す時の注意点

部分テンプレートを呼び出す際、partialを省略できることは知っていたので、以下のように記述していた

render 'shared/post', locals: { post: @post }

しかしこれではうまく変数を渡せなかった
→partialを省略したらlocalsも省略する必要があるらしい

render 'shared/post', post: @post

3. jQueryでonしたクリックが効かない

onメソッドの基本的な記述方法は以下の通り

$(".btn").on("click", function(){
  console.log("効かない");
});

上記の記述は正しいが、ページ読み込み後に生成された要素には効かないらしい
jQueryで後から追加した要素等がそれに該当
そういう時は引数にセレクタを指定する

$(document).on("click", ".btn", function(){
  console.log("効いた");
});

マイページ実装

内容

マイページを実装し、個人の投稿一覧表示およびユーザー情報編集ができるようにする

個人の投稿一覧表示機能

1. MVCの設定

Userモデルはすでに新規登録機能で実装済みのため、今回はusersコントローラーのみ作成
ターミナル

% rails g controller users

ルーティング、usersコントローラー、ビューを編集
今回はマイページで一覧表示と個人情報編集を実装するため、indexとedit、updateを設定(edit、updateは別の機能で実装)
と思ったが上記は間違い
→indexだとユーザー一覧表示になる
→ユーザーの投稿一覧を表示したいため、showを設定(ユーザー詳細表示)
users_controller.rb

def show
  user = User.find(params[:id])
  @posts = user.posts
end

投稿一覧表示のビューはすでに実装したposts/index.html.erbを参考に作成

2. rubocop

ユーザー情報編集機能

1. MVCの設定

編集ページのビューは新規登録ページを使い回し
ルーティングは先に設定しているので、コントローラーを編集
※メールアドレスとパスワードを編集すると、デフォルトでログアウトするように設定されている
→これらを編集したいときはbypass_sign_inを使用するみたい users_controller.rb

def edit
  @user = User.find(params[:id])
end

def update
  @user = User.find(params[:id])
  if @user.update(user_params)
    bypass_sign_in(@user)
    redirect_to user_path(@user.id)
  else
    render :edit
  end
end

private

def user_params
  params.require(:user).permit(:nickname, :email, :password, :password_confirmation, :last_name, :first_name, :last_name_reading, :first_name_reading, :birthday)
end

同一の記述は最後にまとめてbefore_actionにまとめることとする
また、ログアウト状態で編集ページへ遷移できないようにbefore_actionに記述する
他人の編集ページへ遷移することもできないようにbefore_acitionで設定する
users_controller.rb

before_action :move_to_index, except: :show

private
def move_to_index
  redirect_to root_path if current_user.id != @user.id
end

2. ヘッダーにマウスオーバーすると表示が変わるよう設定

header.jsファイルを作成し、そこにコードを記述して実装
header.jsを読み込む
app/assets/javascript/packs/application.js

require("../header")

header.jsを編集

次回 投稿編集機能を実装

参考

新規投稿している時、小数点がある数値が保存できなかった
number_fieldは整数しか扱えないらしいので、stepをつける
new.html.erb

form.number_field :~, step: "0.1"

また、integer型でDBを設計してしまっていたため、マイグレーションファイルをfloat型に修正
posts.rbマイグレーションファイル

t.float :~, null: false

画像投稿機能(ActiveStorageの導入)

内容

画像投稿も実装するため、再度新規投稿機能の見直しから

新規投稿機能の見直し

1. Googleマップを埋め込む

新規投稿の上部にGoogleマップの埋め込み機能を使用して、マップを表示
GoogleMapsAPIの導入も考えているが、余裕があれば追加実装で行う

新規投稿機能で画像アップロード機能を追加(まずは1枚のみ)

1. Active Storageの導入

まずはImageMagickをHomebrewからインストール
ターミナル

% brew install imagemagick

続いてGemをインストールし、ローカルサーバーを再起動 Gemfile

# Gemfileの一番下に記述する

gem 'mini_magick'
gem 'image_processing', '~> 1.2'

ターミナル

% bundle install
% rails s

続いてActive Storageをインストール
ターミナル

% rails active_storage:install
% rails db:migrate

Sequel Proを確認し、DBが存在すればOK

2. postsテーブルに画像ファイルを紐付ける

postsテーブルとActive Storageのテーブルで管理された画像ファイルのアソシエーションを記述
app/models/post.rb

class Post < ApplicationRecord
  has_one_attached :image
end

この時、postsテーブルにカラムを追加する必要はない

3. 画像の保存を許可するストロングパラメーターにする

imageという名前でアクセスできるようになった画像ファイルの保存を許可する実装を行う
app/controllers/posts_controller.rb

private

def post_params
  params.require(:post).permit(~, :image).merge(user_id: current_user.id)
end

4. 画像アップロード機能を表示

ビュー、CSSを編集
app/views/posts/new.html.erb

<%= f.file_field :image %>

5. Postモデルの単体テストコード修正

ダミー画像を用意
「public」ディレクトリの中に「images」というディレクトリを作成し、ダウンロードした画像を、imagesディレクトリの中に配置
FactoryBotの編集
afterメソッドを用いて、生成するダミーデータに画像を添付
spec/factories/posts.rb

after(:build) do |post|
  post.image.attach(io: File.open('public/images/test_image.png'), filename: 'test_image.png')
end

テストコードを実装し、テストを実行
ターミナル

bundle exec rspec spec/models/post_spec.rb

stashしたブランチを元に戻し、投稿一覧表示機能の修正

1. ビュー、ファイルの修正

保存した画像を表示
image_tagメソッドを記述して、画像を表示させる
なお、投稿画像が存在しない場合は、no imageファイルを表示する
app/views/posts/index.html.erb

<%= image_tag post.image, class: 'post-image' if post.image.attached? %>
<%= image_tag 'noimage.png', class: 'post-image' unless post.image.attached? %>

no imageファイルを表示させようとしたが、最初下記エラーが出た
Sprockets::Rails::Helper::AssetNotFound
The asset "noimage.png" is not present in the asset pipeline.
これはasset配下に画像ファイルが存在しないため起こるエラーである
→テストコードの実装の時にpublic配下に画像を保存していたので、それでいけると思ってしまった
その他も投稿内容を一覧表示させるよう編集
ついでにヘッダー、フッターも少し編集

※検索機能、人気順表示、いいね機能等も実装予定だが、別のブランチで作業する

2. rubocop

次回

マイページを実装し、個人の投稿一覧表示ができるようにする