55
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DMM WEBCAMPAdvent Calendar 2023

Day 18

【Rails】初学者向け!グループ機能を実装したアプリケーションを作ってみよう!

Last updated at Posted at 2023-12-17

1. はじめに

こんにちは!
DMM WEBCAMP Advent Calendar 2023 :christmas_tree: 18日目 を担当します @GotouAsahiです!
今回は初学者向けにRailsのグループ機能の実装したアプリケーションを作成していこうと思います!
グループ機能はベースができたらグループ内チャットやサブスクライブなど様々な方向に派生することができるのでぜひ今回の実装にチャレンジして、できたらぜひコメントで共有してみてください!
また、初めての記事執筆のため少し長めの記事にになりましたが間に知っとくと役立つ知識も挟んでいるので最後までお付き合いいただけると幸いです!:bow_tone1:

今回作成するアプリケーションの概要

ER図

mermaid-diagram-2023-11-24-201447.png

環境

Ruby 3.1.2
Rails 6.1.7

2. deviseを用いたユーザーの導入

今回はdeviseの導入方法や使い方がメインではないのでサラッと導入していきます!

Gemfileの編集

まずは、Gemfileにdeviseを追加します。

Gemfile
# Gemfile

# ... 他のgem ...

gem 'devise'

以下のコマンドを実行してdeviseをインストールします。

bundle install
rails g devise:install

モデルの作成

rails g devise User name:string
rails db:migrate

ビューのカスタマイズ

新規登録時にユーザー名も登録できるように、コマンドを実行後に新規登録フォームに名前のフィールドを追加します。

rails g devise:views
app/views/devise/registrations/new.html.erb
<div class="field">
  <%= f.label :email %><br>
  <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
</div>
  
+ <div class="field">
+  <%= f.label :name %><br>
+  <%= f.text_field :name %>
+ </div>

パラメーターの許可

新規登録時にコントローラーでnameを許可します。

app/controllers/application_controller.rb
before_action :configure_permitted_parameters, if: :devise_controller?

def configure_permitted_parameters
  devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
end

ヘッダーの作成

最後に、簡単なログイン・ログアウトのリンクがあるヘッダーを作成します。

app/views/layouts/application.html.erb
<body>
+  <header style="text-align: center;">
+    <% if user_signed_in? %>
+      <%= link_to "Logout", destroy_user_session_path, method: :delete %>
+    <% else %>
+      <%= link_to "Sigup", new_user_registration_path %>
+      <%= link_to "login", new_user_session_path %>
+    <% end %>
+  </header>
  <%= yield %>
</body>

/users/sign_upにアクセスして以下のようになっていればOKです!!
image.png

3. グループ機能の導入

モデルの作成とマイグレーション

最初に、グループのモデルを作成しましょう。以下のコマンドでモデルの作成とマイグレーションを行います。

rails g model Group name:string description:text
rails db:migrate

コントローラの生成

次に、グループを扱うためのコントローラを生成します。

rails g controller Groups new index show edit

routes.rbに追記

config/routes.rb
Rails.application.routes.draw do
-  get 'groups/new'
-  get 'groups/index'
-  get 'groups/show'
-  get 'groups/edit'
   devise_for :users
+  root to: 'groups#index'
+  resources :groups,except: [:index]
end

これにより、新規作成、一覧表示、詳細表示、編集、削除のための基本的なアクションが用意されます。
ルートパス(/の後になにもないurl)に飛んでGroups#indexが表示されればOKです!!

4.グループ機能の骨組みの作成

グループ作成機能の追加

まず、新しいグループを作成する機能を追加します。GroupsControllerに以下のコードを追加します。

app/controllers/groups_controller.rb
  def new
    @group = Group.new
  end
  def create
    @group = Group.new(group_params)
    if @group.save
      redirect_to group_path(@group.id)
    else
      render :new
    end
  end
  
  private

  def group_params
    params.require(:group).permit(:name, :description)
  end

フォームの作成

次に、新しいグループを作成するためのフォームを作成します。

app/views/groups/new.html.erb
<h1>グループ作成画面</h1>

<%= form_with model: @group do |f| %>
  <% if @group.errors.any? %>
    <div id="error_explanation">
      <h2><%= @group.errors.count %> つのエラーが発生しました。</h2>
      <ul>
        <% @group.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %>
    <%= f.text_field :name, autofocus: true %>
  </div>

  <div class="field">
    <%= f.label :description %>
    <%= f.text_area :description %>
  </div>

  <div class="actions">
    <%= f.submit "作成する" %>
  </div>
<% end %>

<%= link_to '戻る', root_path %>

最後に、Groupモデルにバリデーションを追加しておきましょう。

app/models/group.rb
class Group < ApplicationRecord
+  validates :name, presence: true
+  validates :description, presence: true
end

これで、新しいグループの作成機能が完成しました!/groups/newに飛んで確認してみましょう!
以下の画面が出て投稿後/groups/1に行けばOKです!!(このタイミングではGroups#showと画面に出ていれば大丈夫です)
image.png

グループの一覧表示

グループ一覧を表示するためのアクションをGroupsControllerに追加します。

app/controllers/groups_controller.rb
  def index
    @groups = Group.all
  end

次に、一覧を表示するためのビューを作成します。

app/views/groups/index.html.erb
<h1>グループ一覧</h1>
<%= link_to '新しいグループを作成', new_group_path %>
<% @groups.each do |group| %>
  <li>
    <%= link_to group.name, group_path(group.id) %>
  </li>
<% end %>

ルートパスに戻り先程作ったグループが表示されているか確認しましょう!
image.png

グループの詳細表示

グループの詳細も同様にアクションとビューを作成します。

app/controllers/groups_controller.rb
  def show
    @group = Group.find(params[:id])
  end
app/views/groups/show.html.erb
<h1>グループ名:<%= @group.name %></h1>

<p><strong>説明:</strong> <%= @group.description %></p>

<%= link_to '一覧に戻る', root_path %>

一覧画面から名前をクリックして詳細画面に遷移するか見てみましょう!
image.png

グループの編集と削除

最後にグループの編集と削除ができるようにしていきます。

app/controllers/groups_controller.rb
  def edit
    @group = Group.find(params[:id])
  end
  
  def update
    @group = Group.find(params[:id])
    if @group.update(group_params)
      redirect_to group_path(@group.id)
    else
      render :edit
    end
  end
  
  def destroy
    group = Group.find(params[:id])
    group.destroy
    redirect_to root_path
  end

詳細ページに編集と削除用のリンクを追加します。

app/views/groups/show.html.erb
<h1>グループ名:<%= @group.name %></h1>

<p><strong>説明:</strong> <%= @group.description %></p>

+ <%= link_to "グループを編集する", edit_group_path(@group.id) %>
+ <%= link_to "グループを削除する", group_path(@group.id), method: :delete, data: {confirm: "本当にグループを消しますか?"} %>
<%= link_to '一覧に戻る', root_path %>

編集画面のビューファイルを作成します。

app/views/groups/edit.html.erb
<h1>グループ編集画面</h1>

<%= form_with model: @group do |f| %>
  <% if @group.errors.any? %>
    <div id="error_explanation">
      <h2><%= @group.errors.count %> つのエラーが発生しました。</h2>
      <ul>
        <% @group.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %>
    <%= f.text_field :name, autofocus: true %>
  </div>

  <div class="field">
    <%= f.label :description %>
    <%= f.text_area :description %>
  </div>

  <div class="actions">
    <%= f.submit "保存する" %>
  </div>
<% end %>

<%= link_to '戻る', group_path(@group.id) %>

これで、グループに関する機能が完成しました!
新しいグループを作成し、それぞれの機能を確認してみてください!
image.png
image.png

5. 多対多のアソシエーションについて

プログラミングの初学者の方にとっては、多対多のアソシエーションは難しく感じるかもしれませんが、とても重要な概念です。
今回は、ユーザーとグループの関連付けをする前に、多対多のアソシエーションについて簡単に説明します!

多対多のアソシエーションの特徴

  • 複雑な関係性: 通常、一対多のアソシエーションでは片方のモデルが他方を所有しますが、多対多のアソシエーションではお互いに所有し合う関係性です。
    今回のアプリでいうとユーザーが複数のグループに所属し、同じグループには複数のユーザーが所属するイメージです。
  • 中間テーブルの必要性: 多対多のアソシエーションを実現するためには、中間テーブルが不可欠です。
    このテーブルには、関連するモデル同士の ID が格納され、それによってテーブル同士の関係が作られます。

中間テーブルの重要性

多対多のアソシエーションでは、中間テーブルが重要な役割を果たします。中間テーブルを介して各モデルが関連付けられ、以下のメリットがあります。

  • 情報の格納: 中間テーブルにはユーザーがグループに参加した日時などの追加の情報を格納できます。
  • データの整合性: 中間テーブルを介することで、モデル同士の関連付けが一貫性を持ち、データの整合性が維持されます。

他にも関連付けられているモデル同士の間に新しい属性や条件を追加しやすくなったり、データベースのクエリや検索においても、効率化ができるなどのメリットがあります。

6. ユーザーとの関連付け

それでは簡単に多対多のアソシエーションについて説明したところでいよいよユーザーとグループを紐づけていきます!

中間テーブルの作成

まずはユーザーとグループを紐付ける中間テーブルとしてMembershipモデルを作成します。

rails g model Membership user_id:integer group_id:integer
rails db:migrate

モデルの設定

Membershipモデルの設定を行います。

app/models/membership.rb
class Membership < ApplicationRecord
+  belongs_to :user
+  belongs_to :group
end

この記述によって、Membershipモデルがuser_idとgroup_idを外部キーとして持ち、UserモデルとGroupモデルとの多対多の関連付けが行われています。
続いてUserモデルとGroupモデルの設定もしていきます。

app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
+  has_many :memberships
+  has_many :groups, through: :memberships
end
app/models/group.rb
class Group < ApplicationRecord
+  has_many :memberships, dependent: :destroy
+  has_many :users, through: :memberships
   validates :name, presence: true
   validates :description, presence: true
end

これで、Userモデル、Groupモデル、Membershipモデルがそれぞれ適切に関連付けられ、多対多の関係ができました。

throughについて

through オプションを使用することで、多対多の関連付けにおいてどの中間テーブルを経由して関連するかを指定します。これにより、モデル同士が直接的に関連していない場合でも、中間テーブルを介して関連付けることができます。
throughオプションは指定がなくてもRailsは中間テーブルを推測して関連付けを行いますが、throughオプションを使うことで、明示的に中間テーブルを指定することができ、コードの可読性の向上に繋がります。

7. グループへのユーザーの参加と脱退

コントローラーとアクションの追加

まず、ユーザーのグループ参加と脱退を処理するMembershipsControllerを作成します。

rails g controller memberships

次に、memberships_controller.rb を以下のように編集します。

app/controllers/memberships_controller.rb
class MembershipsController < ApplicationController
  before_action :authenticate_user!

  def create
    group = Group.find(params[:group_id])
    user = current_user
    group.users << user
    redirect_to group_path(group.id)
  end

  def destroy
    group = Group.find(params[:group_id])
    user = current_user
    group.users.delete(user)
    redirect_to root_path
  end
end

ちなみに:arrow_down:の部分は

user = current_user
group.users << user

:arrow_down:のコードをコンパクトにしたものです!

membership = Membership.new
membership.user_id = current_user.id
membership.group_id = group.id
membership.save

routes.rbに追記

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root to: 'groups#index'
- resources :groups, except: [:index]
+ resources :groups, except: [:index] do
+   resource :membership, only: [:create, :destroy]
+ end
end
ネストの解説

ネストを使用すると階層的なURL構造になります。
また、以下のようなメリットがあります。

  1. 関連性をわかりやすくする
    ユーザーがグループに参加・脱退するアクションは、グループと直接関連しています。URLにも同様の構造を持たせることでグループとの関連性を表しています。

  2. パラメータの明示性
    ネストを使用することで、paramsにグループのidが含まれ、アクション内で関連する情報を取得しやすくなります。
    今回でいうと params[:group_id] のことです。

URLの具体例
参加(create): /groups/1/membership
脱退(destroy): /groups/1/membership
これにより、URLから、グループとユーザーの関連性が明示的になります。
また、ユーザーがどのグループに参加しているのか、どのグループから脱退したのかがURLから理解しやすくなります。

これで、MembershipsControllerが作成され、グループへのユーザーの参加と脱退が処理されるようになりました。

ビューに参加と脱退ボタンの追加

ユーザーがグループに参加・脱退するためのボタンをビューに追加します。

app/views/groups/show.html.erb
<h1>グループ名:<%= @group.name %></h1>

<p><strong>説明:</strong> <%= @group.description %></p>

+  <% if user_signed_in? %>
+   <% if @group.memberships.exists?(user_id: current_user.id) %>
+     <%= link_to "脱退する",  group_membership_path(@group), method: :delete %>
+   <% else %>
+     <%= link_to "参加する", group_membership_path(@group), method: :post%>
+   <% end %>
+ <% end %>
<%= link_to "グループを編集する", edit_group_path(@group.id) %>
<%= link_to "グループを削除する", group_path(@group.id), method: :delete, data: {confirm: "本当にグループを消しますか?"} %>
<%= link_to '一覧に戻る', root_path %>
+ <h2>グループユーザー</h2>
+ <% @group.memberships.each do |member| %>
+   <p><%= member.user.name %></p>
+ <% end %>

まず <% if user_signed_in? %>でユーザーがログインしているかを確認し、ログインしている場合のみ参加と脱退のボタンを表示します。
次の <% if @group.memberships.exists?(user_id: current_user.id) %>でユーザーが既にグループに参加しているかを確認しています。

グループユーザーの一覧について

グループユーザーの一覧表示について短いコードですが大事な部分なので解説していきます。

  1. @group.membershipsで、現在のグループに関連付けられているメンバーシップの一覧を取得します。これは、has_many :membershipsといった関連付けのおかげで、Groupモデルがメンバーシップを複数持っているため使える記述になります。
  2. <% @group.memberships.each do |member| %>を使用して取得したメンバーシップの一覧に対して繰り返し処理を行います。簡単に言うと、グループに参加している各メンバーの情報に順番にアクセスしています。
  3. <%= member.user.name %>ではmember.userでメンバーシップに関連付けられているユーザーの情報にアクセスすることができます。アクセスしたユーザーの情報に対し.nameを使うことでユーザーの名前を取得して表示しています。

まとめると、グループに関連付けられた各メンバーシップにアクセスし、そのメンバーシップに関連づけられたユーザーの名前を表示しています。

最後に新規登録後に参加ボタンが表示されるか、参加後は脱退にボタンが変わりグループユーザーに名前が表示されるかを確認しましょう!
image.png
image.png

8. グループ管理者の実装

※管理者機能を実装する前に
管理者機能を実装した場合これまでに作成したグループのリンクを開くと管理者がいないため以下のようなエラーが出ます。
Monosnap Image 2023-11-24 20.54.43.png
開かなければ特に問題ありませんが気になる方は先に作成したグループを削除しておきましょう!

グループの編集と削除を行えるようにグループの管理者を追加していきます。

マイグレーションファイルの追加

rails g migration AddOwnerIdToGroups owner_id:integer
rails db:migrate

モデルに管理者に関する追加の記述

app/models/group.rb
class Group < ApplicationRecord
  has_many :memberships, dependent: :destroy
  has_many :users, through: :memberships
  validates :name, presence: true
  validates :description, presence: true
+ belongs_to :owner, class_name: 'User', foreign_key: 'owner_id'
end

belongs_to メソッドについて
belongs_to は、異なるモデル間の関連付けを定義するためのメソッドです。このメソッドはあるモデルが他のモデルに「所属している」ことを示します。
オプションの説明

:owner これは関連名で、関連付けの名前を指定します。
今回は、ownerという名前でUserモデルと関連付けました。

:class_name:これは関連付けられるモデルのクラス名を指定します。
'User' という文字列は、Userモデルを指しています。なぜこの記述が必要なのかというと、Railsは通常、関連するモデルのクラス名を単数形から自動的に推測しますが、関連名がモデル名と異なる場合には、class_nameオプションを使用してクラス名を明示的に指定する必要があります。
今回の場合はOwnerモデルなど存在せず、あくまでUserモデルを使用しているのでこの記述が必要になります。

:foreign_key:これは外部キーのカラム名を指定します。
モデル間の関連を構築する際、通常はモデル名_id(例: user_id、group_id)が外部キーのデフォルトの命名規則として使用されます。
しかし、もし異なる名前の外部キーを使用する場合には、foreign_key オプションを使ってその名前を指定する必要があります。
今回の場合はGroupモデルがUserモデルと関連付けられており、外部キーとしてuser_idではなくowner_idを使用しています。

まとめ
belongs_to :owner, class_name: 'User', foreign_key: 'owner_id' この記述ではGroupモデルはUserモデルにownerという名前の関連付けを持ちます。
そして、owner_idカラムを外部キーとして使用し、この外部キーを通じてUserモデルと関連付けられています。

コントローラーの編集

app/controllers/groups_controller.rb
class GroupsController < ApplicationController
+ before_action :authenticate_user!, except: [:index, :show]
+ before_action :owner?, only: [:edit, :update, :destroy]

    # ... 他のアクション ...

  def create
    @group = Group.new(group_params)
+   @group.owner = current_user
    if @group.save
+     @group.users << current_user
      redirect_to group_path(@group.id)
    else
      render :new
    end
  end

  # ... 他のアクション ...

  private

  def group_params
    params.require(:group).permit(:name, :description)
  end

+ def owner?
+   group = Group.find(params[:id])
+   if group.owner != current_user
+     redirect_to group_path(group.id)
+   end
+ end

グループ作成時に管理者としてログインユーザーを設定し、before_action :owner?, only: [:edit, :update, :destroy]def owner?でグループ編集画面やupdateアクションなどでオーナーかどうかを確認する処理を追加します。
また、グループの作成にログインしているユーザーのidが必要になるためログインしていないときはグループの作成などができないようにコントローラーのbefore_action :authenticate_user!, except: [:index, :show]の記述をします。

@group.owner = current_userについて

アソシエーションの設定のおさらい
先程のモデルの記述が役立つので軽く説明します!
まず、Groupモデルにbelongs_to :owner, class_name: 'User', foreign_key: 'owner_id'の記述があることで、Groupモデルのデータはowner_idカラムを通じてUserモデルのデータと関連付けられ、関連づけられた Userレコードを指すことができます。
オブジェクトへの代入
@group.owner = current_userは、Groupモデルのデータ(@group)のownerプロパティにUserモデルのデータ(current_user)を代入しています。この代入により、@groupはcurrent_userという管理者(owner)を持つようになります。
外部キー (owner_id) の設定
この代入が行われると、@group.ownerにはcurrent_userが設定され、同時に@group.owner_idにはcurrent_user のIDが格納されます。これにより、外部キー (owner_id) を介してGroupレコードとUserレコードが関連付けられます。
アソシエーションの利用
これらを行うことで、@group.ownerを通じてcurrent_userの情報にアクセスできます。例えば、@group.owner.nameといった形で、current_userの名前を取得することができます。

ビューの編集

app/views/groups/index.html.erb
  <h1>グループ一覧</h1>
+ <% if user_signed_in? %>
   <%= link_to '新しいグループを作成', new_group_path %>
+ <% end %>
  <% @groups.each do |group| %>
    <li>
      <%= link_to group.name, group_path(group.id) %>
    </li>
  <% end %>

ログインしていない場合はグループ作成を表示しないようにビューに<% if user_signed_in? %>を追記します。

app/views/groups/show.html.erb
<h1>グループ名:<%= @group.name %></h1>
+ <h2>グループオーナー:<%= @group.owner.name %></h2>

<p><strong>説明:</strong> <%= @group.description %></p>

<% if user_signed_in? %>
+ <% if @group.owner == current_user %>
+    <%= link_to "グループを編集する", edit_group_path(@group.id) %>
+    <%= link_to "グループを削除する", group_path(@group.id), method: :delete, data: {confirm: "本当にグループを消しますか?"} %>
- <% if @group.memberships.exists?(user_id: current_user.id) %>
+ <% elsif @group.memberships.exists?(user_id: current_user.id) %>
    <%= link_to "脱退する",  group_membership_path(@group), method: :delete %>
  <% else %>
    <%= link_to "参加する", group_membership_path(@group), method: :post%>
  <% end %>
<% end %>
- <%= link_to "グループを編集する", edit_group_path(@group.id) %>
- <%= link_to "グループを削除する", group_path(@group.id), method: :delete, data: {confirm: "本当にグループを消しますか?"} %>
<%= link_to '一覧に戻る', root_path %>

<h2>グループユーザー</h2>
<% @group.memberships.each do |member| %>
  <p><%= member.user.name %></p>
<% end %>

グループの詳細画面では<% if @group.owner == current_user %>で管理者のみ編集と削除のボタンが表示され、それ以外のユーザーには参加・脱退ボタンが表示されるようになります。

グループを作成し編集と削除ボタンが出るか、グループユーザーに名前があるかを確認しましょう!
image.png
また、他のユーザーで新規登録を行い、編集と削除ボタンがないかと参加と脱退の動作確認もしておきましょう!
image.png

9. 完成!!

以上でグループ機能がついたアプリケーションが完成しました!!
だいぶ長くなってしまいましたが最後までお付き合いいただきありがとうございました:bow:
ここからはグループチャット機能をつけたりメンバーのロール機能、追放機能など様々な方向に伸ばせると思うので色々触りながらご自身の思うように改造して行っていただければと思います!:muscle:
また、おまけで軽くCSSで装飾する場合のコードも書いておりますので良ければこちらも実装してみてください:crab::crab:

おまけ:CSS装飾版
app/views/layouts/application.html.erb
  <body>
    <header class="site-header">
      <% if user_signed_in? %>
        <%= link_to "Logout", destroy_user_session_path, method: :delete, class: "nav-link" %>
      <% else %>
        <%= link_to "Signup", new_user_registration_path, class: "nav-link" %>
        <%= link_to "Login", new_user_session_path, class: "nav-link" %>
      <% end %>
    </header>
    <main class="content">
      <%= yield %>
    </main>
  </body>
app/views/devise/registrations/new.html.erb
<h2 class="form-title">Sign up</h2>
<div class="devise-form">
  <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
    <%= render "devise/shared/error_messages", resource: resource %>

    <div class="form-field">
      <%= f.label :name, class: "field-label" %><br>
      <%= f.text_field :name, class: "text-input" %>
    </div>

    <div class="form-field">
      <%= f.label :email, class: "field-label" %><br />
      <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "text-input" %>
    </div>

    <div class="form-field">
      <%= f.label :password, class: "field-label" %>
      <% if @minimum_password_length %>
      <em class="password-note">(<%= @minimum_password_length %> characters minimum)</em>
      <% end %><br />
      <%= f.password_field :password, autocomplete: "new-password", class: "text-input" %>
    </div>

    <div class="form-field">
      <%= f.label :password_confirmation, class: "field-label" %><br />
      <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "text-input" %>
    </div>

    <div class="form-actions">
      <%= f.submit "Sign up", class: "submit-button" %>
    </div>
  <% end %>
  <div class="form-shared">
    <p class="form-message">
      アカウントがある場合は<%= render "devise/shared/links" %>してください。
    </p>
  </div>
</div>
app/views/devise/sessions/new.html.erb
<div class="devise-form">
  <h2 class="form-title">Log in</h2>

  <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
    <div class="form-field">
      <%= f.label :email, class: "field-label" %><br />
      <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "text-input" %>
    </div>

    <div class="form-field">
      <%= f.label :password, class: "field-label" %><br />
      <%= f.password_field :password, autocomplete: "current-password", class: "text-input" %>
    </div>

    <% if devise_mapping.rememberable? %>
      <div class="form-field">
        <%= f.check_box :remember_me, class: "checkbox-input" %>
        <%= f.label :remember_me, class: "checkbox-label" %>
      </div>
    <% end %>

    <div class="form-actions">
      <%= f.submit "Log in", class: "submit-button" %>
    </div>
  <% end %>

  <div class="form-shared">
    <p class="form-message">
      <%= render "devise/shared/links" %>
    </p>
  </div>
</div>
app/views/groups/index.html.erb
<div class="group">
  <div class ="group-list-tab">
    <h1 class="group-list-title">グループ一覧</h1>
    <% if user_signed_in? %>
      <%= link_to '新しいグループを作成', new_group_path, class: "new-group-link" %>
    <% end %>
  </div>
  <ul class="group-list">
    <% @groups.each do |group| %>
      <li class="group-item">
        <%= link_to group.name, group_path(group.id), class: "group-link" %>
      </li>
    <% end %>
  </ul>
</div>
app/views/groups/new.html.erb
<div class="group">
  <h1 class="form-title">グループ作成画面</h1>
  <div class="group-form">
    <%= form_with model: @group do |f| %>
      <% if @group.errors.any? %>
        <div id="error_explanation">
          <h2><%= @group.errors.count %> つのエラーが発生しました。</h2>
          <ul>
            <% @group.errors.full_messages.each do |message| %>
              <li><%= message %></li>
            <% end %>
          </ul>
        </div>
      <% end %>

        <div class="form-field">
          <%= f.label :name, class: "field-label" %>
          <%= f.text_field :name, autofocus: true, class: "text-input" %>
        </div>

        <div class="form-field">
          <%= f.label :description, class: "field-label" %>
          <%= f.text_area :description, class: "text-input" %>
        </div>

        <div class="form-actions">
          <%= f.submit "作成する", class: "submit-button" %>
        </div>
    <% end %>

    <%= link_to '戻る', root_path, class: "nav-link back" %>
  </div>
</div>
app/views/groups/show.html.erb
<div class="group">
  <div class="group-details">
    <h1>グループ名:<%= @group.name %></h1>
    <h2>グループオーナー:<%= @group.owner.name %></h2>
    <p><strong>説明:</strong> <%= @group.description %></p>
    <div class="group-actions">
      <% if user_signed_in? %>
        <% if @group.owner == current_user %>
          <%= link_to "グループを編集する", edit_group_path(@group.id), class: "group-action-link edit" %>
          <%= link_to "グループを削除する", group_path(@group.id), method: :delete, data: { confirm: "本当にグループを消しますか?" }, class: "group-action-link delete" %>
        <% elsif @group.memberships.exists?(user_id: current_user.id) %>
          <%= link_to "脱退する", group_membership_path(@group), method: :delete, class: "group-action-link exit" %>
        <% else %>
          <%= link_to "参加する", group_membership_path(@group), method: :post, class: "group-action-link join" %>
        <% end %>
      <% end %>
      <%= link_to '一覧に戻る', root_path, class: "group-action-link back" %>
    </div>
    <h2>グループユーザー</h2>
    <div class="group-members">
      <% @group.memberships.each do |member| %>
        <p><%= member.user.name %></p>
      <% end %>
    </div>
  </div>
</div>
app/views/groups/edit.html.erb
<div class="group">
  <h1 class="form-title">グループ編集画面</h1>
  <div class="group-form">
    <%= form_with model: @group do |f| %>
      <% if @group.errors.any? %>
        <div id="error_explanation">
          <h2><%= @group.errors.count %> つのエラーが発生しました。</h2>
          <ul>
            <% @group.errors.full_messages.each do |message| %>
              <li><%= message %></li>
            <% end %>
          </ul>
        </div>
      <% end %>

        <div class="form-field">
          <%= f.label :name, class: "field-label" %>
          <%= f.text_field :name, autofocus: true, class: "text-input" %>
        </div>

        <div class="form-field">
          <%= f.label :description, class: "field-label" %>
          <%= f.text_area :description, class: "text-input" %>
        </div>

        <div class="form-actions">
          <%= f.submit "保存する", class: "submit-button" %>
        </div>
    <% end %>

    <%= link_to '戻る', group_path(@group.id), class: "nav-link back" %>
  </div>
</div>
app/assets/stylesheets/application.css
/* 共通 */
body {
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 0;
}

.content {
  margin: 20px auto;
}

/* ヘッダー */
.site-header {
  background-color: #333;
  color: #fff;
  padding: 10px;
  text-align: right;
}

.nav-link {
  color: #fff;
  text-decoration: none;
  margin-left: 10px;
}

.nav-link.back {
  margin-top: 10px;
  color: #333;
}

.nav-link.back:hover {
  background-color: #f0f0f0;
}

/* ログイン・臨機登録画面 */
.form-title {
  text-align: center;
  font-size: 1.5em;
  color: #333;
}

.devise-form {
  max-width: 400px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 5px;
  background-color: #fff;
}


.form-field {
  margin-bottom: 15px;
}

.field-label {
  font-weight: bold;
}

.text-input {
  width: 100%;
  padding: 8px;
  box-sizing: border-box;
}

.password-note {
  font-style: italic;
  font-size: 0.9em;
  color: #888;
}

.form-actions {
  margin-top: 15px;
}

.submit-button {
  background-color: #4285f4;
  color: #fff;
  padding: 10px 15px;
  border: none;
  border-radius: 3px;
  cursor: pointer;
  font-size: 1em;
}

.submit-button:hover {
  background-color: #3172d6;
}

.form-shared {
  text-align: center;
  padding: 15px;
}

.form-message {
  font-size: 1.2em;
  color: #555;
  margin: 0;
}

.form-message a {
  color: #4285f4;
  text-decoration: underline;
  font-weight: bold;
}

.form-message a:hover {
  text-decoration: none;
}

/* グループ */
.group {
  width: 800px;
  margin: 0 auto;
}

.new-group-link {
  display: inline-block;
  padding: 15px;
  margin: 20px;
  background-color: #4285f4;
  color: #fff;
  text-decoration: none;
  border-radius: 5px;
}

.new-group-link:hover {
  background-color: #3172d6;
}

.group-list-tab {
  display: flex;
}

.group-list-title {
  font-size: 2em;
  color: #333;
  margin-top: 20px;
}

.group-list {
  list-style: none;
  padding: 0;
}

.group-item {
  margin-bottom: 10px;
}

.group-link {
  display: inline-block;
  padding: 10px;
  background-color: #f8f8f8;
  color: #333;
  text-decoration: none;
  border-radius: 5px;
}

.group-link:hover {
  background-color: #e0e0e0;
}

.group-form {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 5px;
  background-color: #fff;
}

.group-actions {
  margin-bottom: 20px;
}

.group-action-link {
  display: inline-block;
  padding: 8px 12px;
  margin-right: 10px;
  background-color: #4285f4;
  color: #fff;
  text-decoration: none;
  border-radius: 3px;
}

.group-action-link:hover {
  background-color: #3172d6;
}

.group-details {
  max-width: 600px;
  margin: 20px auto;
}

.group-details h1,
.group-details h2 {
  color: #333;
}

.group-details p {
  margin-bottom: 10px;
}

.group-members {
  max-width: 400px;
}

.group-members p {
  margin-bottom: 5px;
}

.group-action-link.back {
  background-color: #4285f4;
}

.group-action-link.back:hover {
  background-color: #3172d6;
}

.group-action-link.join,
.group-action-link.edit {
  background-color: #4caf50;
}

.group-action-link.join:hover,
.group-action-link.edit:hover {
  background-color: #388e3c;
}

.group-action-link.exit,
.group-action-link.delete {
  background-color: #f44336;
}

.group-action-link.exit:hover,
.group-action-link.delete:hover {
  background-color: #d32f2f;
}

10. おわりに

いかがでしたでしょうか。
今回の記事ではグループ機能をつけたアプリケーションを作っていきました。
Rails初学者の方が一緒に手を動かしながら勉強できる記事をイメージして作成していきました。
今後の学習の中で、この記事が少しでも役に立つと幸いです:muscle:
初めての執筆で至らない部分も多かったと思いますが最後までお読みいただきありがとうございます🙇
明日からもAdvent Calendarは続くので引き続き楽しんで貰えると嬉しいです!!!

55
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?