ソート機能(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
次回
マイページを実装し、個人の投稿一覧表示ができるようにする