🦓

[Rails]女性限定イベントを作成する2/2

2023/06/25に公開

はじめに

前回の記事ではユーザテーブルに性別カラムを追加してきました。
その理由としては女性限定のイベントを作りたいからです。
イベントテーブルにonly_womanカラムを追加し、実装していきます。

満たしたい要件はこちらになります:
1. イベント作成画面にOnly womanのチェックボックスにチェックを入れるだけで女性限定イベントを作できる:
→ ユーザーが女性以外の場合、イベント作成時にOnly womanは表示されず、女性限定イベントが作成できない。
2. 女性限定イベントには女性のみが参加できる。
→ 女性以外の場合、女性限定イベントの詳細ページを開いた際に「参加する」ボタンが表示されない。
3. 女性限定イベントは一覧・詳細画面にて「女性限定」が表記される。

環境

Rails 6.1.4.1
ruby 3.0.2

イベントテーブルにonly_womanカラムを追加する

女性限定かそうでないかのいずれかになるのでデータタイプをbooleanにします。

bin/rails generate migration AddOnlyWomanToEvents OnlyWoman:boolean
Running via Spring preloader in process 53267
      invoke  active_record
      create    db/migrate/20230624114926_add_only_woman_to_events.rb
db/migrate/xxx_add_only_woman_to_events.rb
class AddOnlyWomanToEvents < ActiveRecord::Migration[6.1]
  def change
    add_column :events, :only_woman, :boolean, default: false
  end
end
bin/rails db:migrate
== 20230624114926 AddOnlyWomanToEvents: migrating =============================
-- add_column(:events, :only_woman, :boolean, {:default=>false})
   -> 0.0030s
== 20230624114926 AddOnlyWomanToEvents: migrated (0.0031s) ====================

event.rbに女性イベントスコープを追加する

Railsにおけるスコープ(Scope)は、Active Recordモデルに対して特定の条件を指定してデータベースクエリを実行するための方法です。スコープを使用することで、一連の条件をまとめて名前付きのメソッドとして定義し、複数の場所で再利用することができます。

スコープは、モデルクラス内で scope メソッドを使用して定義されます。以下はスコープの例です。

class Article < ApplicationRecord
  scope :published, -> { where(published: true) }
  scope :recent, -> { order(created_at: :desc).limit(5) }
  scope :by_author, -> (author) { where(author: author) }
end

上記の例では、Article モデルに対していくつかのスコープが定義されています。

  • published: published カラムが true の記事を取得するスコープ。
  • recent: created_at カラムで降順にソートされた最新の5つの記事を取得するスコープ。
  • by_author: 引数として渡された著者に関連する記事を取得するスコープ。

これらのスコープは、次のように使用することができます。

Article.published # 公開された記事を取得
Article.recent # 最新の5つの記事を取得
Article.by_author("John") # 著者が "John" の記事を取得

スコープはチェーン可能であり、複数の条件を組み合わせることもできます。

Article.published.recent # 公開された最新の5つの記事を取得
Article.published.by_author("John") # 著者が "John" かつ公開された記事を取得

これにより、再利用可能なクエリのセットを定義し、コードの可読性と保守性を向上させることができます。

ユーザが女性であれば女性限定イベントを作れる

イベントscopeonly_womantrueになる条件を追加します。

app/models/event.rb
class Event < ApplicationRecord
...

  scope :only_woman, -> { where(only_woman: true) }

  def female_only?
    only_woman
  end

  def can_attend?(user)
    return true unless female_only?
    user&.female?
  end
end

上記のコードでは、Eventモデルに only_woman スコープと female_only? メソッド、can_participate? メソッドを追加しています。

only_woman スコープは、only_woman 列が true のイベントのみを取得するためのスコープです。

female_only? メソッドは、イベントが女性限定かどうかを判定するためのメソッドです。only_woman 列の値が true の場合に true を返します。

can_attend? メソッドは、指定されたユーザーがイベントに参加できるかどうかを判定するためのメソッドです。女性限定イベントの場合は、指定されたユーザーが女性である場合にのみ参加できるように制約を設けています。女性限定でないイベントの場合は、制約を無視して参加できるようにしています。

これらのメソッドを使うことで、モデル内で女性限定イベントの条件を扱い、必要な制約や表示を設定することができます。

イベントコントローラーを編集する

コントローラー内で、女性ユーザが女性限定のイベントを作成できるようにします。
イベントパラメーターに:only_womanパラメーターを追加します。

app/controllers/events_controller.rb
class EventsController < ApplicationController
  def create
    @event = Event.new(event_params)

    # ユーザーが女性かつOnly womanがチェックされた場合のみ、女性限定のイベントとして保存する
    if current_user.female? && params[:event][:only_woman] == "1"
      @event.only_woman = true
    end
    ## 一行で書く
    @event.only_woman = true if current_user.female? && params[:event][:only_woman] == "1"

    if @event.save
      User.all.find_each do |user|
        NotificationFacade.created_event(@event, user)
      end
      redirect_to event_path(@event)
    else
      # イベントの作成が失敗した場合の処理
      render :new
    end
  end

  private

  def event_params
  params.require(:event).permit(:title, :content, :only_woman, :held_at, :prefecture_id, :thumbnail)
  end
end

create アクション内で、current_user.female? で現在のユーザーが女性かどうかを判定し、かつ params[:event][:only_woman] が "1" である場合にのみ、@event.only_womantrue に設定しています。

イベント作成フォームにチェックボックスを追加する

app/views/events/_form.html.erb
<% if current_user&.female? %>
  <div class="mb-3">
    <%= f.check_box :only_woman, { value: "true"}, class: 'form-check-label', for:'flexCheckDefault' %>
    <%= f.label :only_woman %>
  </div>
<% end %>


女性限定のイベントを作ってみて確認します。

irb(main):002:0> event = Event.last
  Event Load (0.1ms)  SELECT "events".* FROM "events" ORDER BY "events"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> 
#<Event:0x00007f965c2cfdb8
...
irb(main):003:0> event.only_woman?
=> true

イベント詳細と一覧ページに’女性限定’を表示させる

詳細ページ

app/views/events/show.html.erb
<% if @event&.female_only? %>
  <span class="badge bg-secondary">女性限定</span>
<% end %>

一覧ページ

app/views/events/_event.html.erb
<% if event&.female_only? %>
  <span class="badge bg-secondary">女性限定</span>
<% end %>

女性イベントの作成が可能になりました。
次は女性限定のイベントに女性ユーザーだけが参加できるようにします。

参加者コントローラーを編集する

参加者コントローラーに女性限定イベントであれば参加するユーザーは女性かどうかを検証する条件を追加します。
女性限定イベントに女性ユーザでない場合、参加処理を中止するようにします。

app/controllers/events/attendances_controller.rb
class Events::AttendancesController < ApplicationController
  def create
    @event = Event.find(params[:event_id])

    if @event.only_woman? && !current_user.female?
      redirect_back(fallback_location: root_path, alert: '女性限定のイベントです')
      return
    end

    event_attendance = current_user.attend(@event)
    (@event.attendees - [current_user] + [@event.user]).uniq.each do |user|
      NotificationFacade.attended_to_event(event_attendance, user)
    end
    redirect_back(fallback_location: root_path, success: '参加の申込をしました')
  end

...
end

イベント詳細ページに女性限定イベントの場合、女性ユーザー以外に「参加する」ボタンが表示されないようにする

app/views/events/show.html.erb
<% unless @event.only_woman && !current_user.female? %>
<%= link_to '参加する',
    event_attendance_url(@event),
    class: 'btn btn-primary',
    method: :post,
    data: { confirm: '申し込みます' }
%>
<% end %>

女性限定イベント一覧を取得する

要件外ですが、女性限定イベント一覧を取得し表示させたいと思います。

routes.rbにコレクションを追加する

config/routes.rb
Rails.application.routes.draw do
...
resources :events do
  collection do
    get :future
    get :past
    get :only_woman
  end
end

イベントコントローラーにonly_womanメソッドを追加する

Ransackgemを使ってonly_womanfutureの条件を満たす検索結果を返すようにします。

app/controllers/events_controller.rb
def only_woman
    @q = Event.only_woman.ransack(params[:q])
    @events = @q.result(distinct: true).includes(:bookmarks, :prefecture, user: { avatar_attachment: :blob })
                .order(held_at: :desc).page(params[:page])
    @search_path = future_events_path
    render :index
end

ビューにタブを追加する

app/views/events/index.html.erb
<li class="nav-item" role="presentation">
   <a class="nav-link <%= active_if('events#only_woman') %>" href="/events/only_woman" role="tab">女性限定イベント</a>
</li>

終わりに

モデル・コントローラー・ビューそれぞれの役割の理解を深めることができて楽しい実装でした。
https://guides.rubyonrails.org/form_helpers.html#making-select-boxes-with-ease

Discussion