コロナで無職になった私がエンジニアになるまで

コロナの影響で前職をクビになってから、エンジニアを目指してます。

オリジナルアプリ作成記 8 ~ チャットルームの実装 2 ~

チャットルームの実装の続き。
昨日の記述のままだと指名した人物との1対1のチャットはできない。
できるけども、チャットルームのURLを直接入力さえしてしまえば
誰でも入室できるような状態である。
同じプロジェクトで動いているメンバー同士でチャットするなら問題はないが
今回実装したいチャットルームは1対1限定なので、アクセス制限をする必要がある。


アクセス制限するための準備

新しい部屋を作るときに送られる情報に注目する。

f:id:Kosei_Program:20210117233002p:plain


inputで送るデータは選択したuser_idとcurrent_user.idで配列でuser_idsとしてデータを送っている。
この結果、中間テーブルに二人分のユーザーが保存される。

f:id:Kosei_Program:20210117233310p:plain

そのためにもroom_controllerのcreateアクションを編集し直す。
ストロングパラメーターで保存するuser_idをuser_ids, としておくことで、
上で送られたデータを保存することができる。

この配列に保存されたuser_idsを利用して、アクセス制限をする。


アクセス制限

直接roomにアクセス制限をかけるというよりは、チャット画面に移動しなければいい
=>アクセス制限するのはチャット画面。
 =>チャット画面のcontrollerを編集する。

f:id:Kosei_Program:20210117233844p:plain

まず言わずもがなログインしていないとアクセスできないようにするために
before_actionで authenticate_user!を使用する。
deviseを導入している時に使用できるメソッド。
次にroom_idを取得して、それに紐づいているuser_idを調べてみる。
ここでテーブルを注目しすぎて勘違いしてしまったのが、
RoomUser.find(params[:room_id])で中間テーブルから該当する部屋のidを取得して、紐づいているuser_idを取得しようと思った。
部屋のidは取得することはできるが、送られているuser_idはそもそも配列なのでuser_idではうまく取得することはできない。

ではroomコントローラーのストロングパラメーターに注目してみると、
~~.permit(:name, user_ids: )
としているため、配列でデータが送られている。
この配列ごとを取得して、ログインしているuser_idが配列内になければ〜という条件分岐をすればいいのではないかと考えた。

よって取得するroom_idは、Roomモデルから取得する。
ここから紐づいているuser_idsを取得する。
@room.users.idsで取得できる。
(正直この記述も何回もエラーを出しながら記述した内容なので、結果的にこうなった感じがあるけども。)

unlessを使って記述すると 論理演算子を使えないような場合でも否定系の条件を記述をすることができる。
例えば、配列内に含まれているかどうかを調べたい時に使用するinclude?メソッドは
論理演算子を使用しないので、普通では含まれている場合のみの分岐しかできない。
ここでは「含まれていなければ〜」と記述したいので、

unless @room.users.ids.include?(current_user.id)

と記述する。
@room(選択した部屋)のusers.ids(紐づいているuser_id)にinclude?(current_user.id)(今ログインしているuser_id)は含まれていなければ?
というような条件分岐ができる。
ここでは、含まれていなければトップページへ飛ばすようにしている。
マイページからアクセス、記事一覧からアクセスと言ったようにいろんな場所からアクセスすることを考慮して、一番最初に戻るようにしたほうが何かと都合がいいかなと思ったため。
まぁなんとなくだけども。

長くなったけど、アクセス制限は条件分岐で自分のidと比較するパターンが多いので、
そこをどれと比較するのかというのと、比較対象をどうやって持ってくるのか
これが問題点になる。


オリジナルアプリ作成記 6 ~チャットルームの実装~

次にユーザー間でチャットできるような機能を実装する。

まず大前提として準備するものが二つある。
チャットルームとチャット画面の二つである。

レイアウト
完成した後に書くのもアレだけど、こんな感じで作成した。
参考にしたものはスクールのレイアウト。

f:id:Kosei_Program:20210116162540p:plain

画面左側にチャットルームが配置されて、真ん中にチャット画面が出る流れ。
試しにチャットルームを作成してみる。

f:id:Kosei_Program:20210116171217p:plain

レイアウトに関しては、完全にスクールの課題を参考にしている。
ここでルーム名を入力して、チャットメンバー(登録されているユーザー名)を選択すると一対一のチャットルームが作成される。

ちなみにルーム名にはバリデーションを組んでいるので、空で登録することはできない。
ここで問題になるのが多対多の関係。
ユーザーは多くの部屋に入る(持つ)ことができる。
ルームは多くのユーザーを持つことができる。
お互いにhas_manyの関係になると、いざ登録した時に無駄なカラムを作成してしまう。
ここで必要なのは中間テーブルと呼ばれるもの。

中間テーブル
room_userと名付けた中間テーブルを作成する。

f:id:Kosei_Program:20210116171750p:plain

マイグレーションには、room_id(作成する部屋)とuser_id(誰が入っているか)を記述する。
他のテーブルの要素を参考にするのでreferencesで。
外部キー制約?とか言ってたかな、foreign_key: true と記述するのも忘れずに。

これで多対多の問題は解消される。
流れとしては、部屋を作るとこの中間テーブルに
作成した部屋のid 部屋にいるuserのid
作成した部屋のid 指名したuserのid
と二つ記録されることになる。

部屋単体のテーブルには、部屋の名前だけ記録される。

実際に作成してみると…。

f:id:Kosei_Program:20210116172335p:plain

roomテーブルには…。

f:id:Kosei_Program:20210116172447p:plain

中間テーブルには…。

f:id:Kosei_Program:20210116172640p:plain

試しに作成した部屋がたくさんあるのでわかりづらいが、一番下の2項目が今回作成した部屋に該当する。


アソシエーションを組んでいることにより、互いのユーザー間であれば共通の部屋に入ることができる。

f:id:Kosei_Program:20210116173709p:plain

このcurrent_user.roomsの記述で操作しているユーザーと紐づいているroom(部屋)を表示する。

部屋の作成自体はこれで終了。
細かいアクセス制限は次回にする。

オリジナルアプリ作成記 6 ~コメント機能の追加 2~

久しぶりのアプリ作成日記になる。
実は毎日コツコツやっていたんだけども、アルバイトと制作の両立をしながらだとしっかりまとめる時間がなかった。
今日はじっくりまとめていく。

 

コメント投稿機能


前回までの作成記でコメント機能を実装した。

f:id:Kosei_Program:20210116153141p:plain


まぁレイアウトはともかく。
無事コメントを投稿することができている。
あとは、記事を削除してコメントも同時に削除されるようにしなければならない。

dependent: :destory

 

f:id:Kosei_Program:20210116154017p:plain

ここからは個人的な感覚の話になるので、玄人の人からしたら「いや違うし」みたいなことになるだろうけども。
記事とコメントはアソシエーションを組んでいる。
その上で記事が存在しなければコメントを残すことはできない。
なので、ここでは記事を親、コメントを子と考えてみる。
親側のモデルに記述されている子のアソシエーションに、

dependent: :destroy

と記述する。
これで記事が削除されたと同時にこれが記述されたコメントも一緒に削除される。

非常に簡単に記述できる上に、よくある機能を実装できる。

次の記事では、ユーザー同士でチャットできるような機能を実装してみる。
というか実装したので、記事をまとめる。

オリジナルアプリ作成記 5 ~コメント機能の追加~

明けました。今日からまたがんばっていきます。

投稿された依頼ページに一言くらいかけるコメント機能が欲しいなと思ったため
実装してみることにしました。
とりあえずの機能だけ。

モデルの作成

class CreateComments < ActiveRecord::Migration[6.0]
def change
create_table :comments do |t|
t.integer :user_id
t.integer :request_id
t.text :text
t.timestamps
end
end
end

モデルを作成したときに作られるマイグレーションファイルを編集。
ごちゃごちゃしたのはいらないので、とりあえずuser_idとrequest_idは必須。
t.textにはコメントの内容が入る。

 

アソシエーションを組む

request(記事)とuserは複数のcommentを持つことができると考えられる。
=> has_manyで記述。

対してcommentは一つのコメントは一人のuser、requestによるものと考えられる。
=> belongs_toで記述。

このアソシエーションが組めていないと、requestの中に入っているcommentを表示するみたいな記述をしたい時に不便。というかできない。

ルーティング

commentのcreateアクションのルーティングを作成しておく。
このルーティングも少し厄介。
1つの記事のページに表示されるので、ある記事(特定の表示させるrequestのページ)のコメントのようにルーティングさせなければならない。
よって、ネストさせる必要がある。

resources :requests do
resources :comments, only: :create
end

とまぁこんな感じ。
するとrequest_idが含まれたURIがルーティングされる。

  request_comments POST   /requests/:request_id/comments(.:format)                                                 comments#create

コントローラーの作成

commentコントローラーを作成して、createアクションを定義する。

class CommentsController < ApplicationController
def create
comment = Comment.create(comment_params)
redirect_to "/requests/#{comment.request.id}"
end

private
def comment_params
params.require(:comment).permit(:text).merge(user_id: current_user.id, request_id: params[:request_id])
end
end

 

コメントを投稿したら、コメントを投稿したrequestのshowアクションへジャンプするようにする。
でもこのままじゃ空でも投稿されて記録されちゃうので、条件分岐を使用する。

def create
if comment = Comment.create(comment_params)
redirect_to "/requests/#{comment.request.id}"
else
render "/requests/#{comment.request.id}"
end
end

まぁでもそんなに大袈裟なことはしないというか空のメッセージをコメント欄に表示させたくないだけなので、成功しても失敗してもジャンプ先は同じで。

アソシエーションを組んでいるおかげでcommentのコントローラーでもrequest.idと記述しても問題が発生しない。
記述の内容を噛み砕くと、createアクションで作成されたcommentと結びつくrequestを表示してね。ということになる。

ストロングパラメーターに関しては、注意する点はuser_idとrequest_idは外部のキーをから持ってくるので、merge(=統合)させる点。

コメントを投稿するためのフォーム

ビューファイルでform_withを使用して、コメントをモデルに送信する。
この際に送信するモデルはcommentとrequestである。

<% if user_signed_in? %>
<%= form_with(model: [@request, @comment], local: true) do |form| %>
<%= form.text_area :text, placeholder: "コメントする", rows: "2" %>
<%= form.submit "SEND" %>
<% end %>
<% else %>
<strong><p>※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p></strong>
<% end %>

if user_signed_in?でログインしているかどうかの条件分岐。
していなければそもそも投稿フォームが表示されない。

requestコントローラーのshowアクションに対しても、commentを表示させるための記述をしなければならない。

def show
@comment = Comment.new
@comments = @request.comments.includes(:user)
end

form_withで送信するために@commentを定義しておく。
また、showに投稿されているコメントを全て取得するためにアソシエーションを利用した記述を使って取得する。
@request(=> Request.find(params[:id]) )はbefore_actionでまとめて表記したので、ここでは省略している。


まとめて取得したコメントはeachを使って表示させる。

<h4><コメント一覧></h4>
<% @comments.each do |comment| %>
<p>
<strong><%= link_to comment.user.nickname, "/users/#{comment.user_id}" %></strong>
<%= comment.text %>
</p>
<% end %>

コメントを投稿したユーザー名をクリックするとそのユーザーのマイページへ飛べるように記述。

これで依頼に対してコメントしてくれたユーザーへ直接アクセスすることができる。
ここから派生してフォロー・フォロワーの機能があれば、ユーザー同士をつなげるなどできそう。

今日はここまで、細かいビューの調整とかはまた明日。


オリジナルアプリ作成記 4 ~依頼投稿一覧ページの作成、マイページ、エラーメッセージの表示~

エラーメッセージの表示 

前回の途中だったエラーメッセージの表示について。
layoutsの下に_error_messages.html.erbを作成して

<% if model.errors.any? %>
<div class="alert alert-warning">
<ul>
<% model.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>

のように記述。
ここら辺は調べたらすぐ出てきたので、テンプレ的な処理だと思われる。
あとはこれを表示できるようにする。

<%= render 'layouts/error_messages', model: form.object %>

これですね。
実際にはform_withの中にこれを書き込む。
これで、validationで引っ掛かった時とかにエラーメッセージが表示されるようになる。

依頼投稿一覧ページの作成

ここが一番今日時間かかった。
個人的にレイアウトを作成するのが非常に苦手。
自分の意図した部分に部品を配置しても「なんだかなー」ってなってしまう。
個人的にはデザインはともかく、アプリの機能性、学習した内容を使った開発を重視したいので適当にすることにした。今回からそうすると決めた。

f:id:Kosei_Program:20201226184431p:plain

だからデザインがダセェのはもう問題外。

マイページの作成

上の画像の名前をクリックするとマイページへアクセスできるようにしたい。

requests(記事のモデル)とusersでアソシエーションを組んでいるので、
users_controllerを作成して、そこでshowメソッドを定義する。

class UsersController < ApplicationController
def show
user = User.find(params[:id])
@nickname = user.nickname
@requests = user.requests
end
end

このuserの部分をcurrent_userで記述してもアソシエーションを組んでいるため、
マイページへアクセスすることができる…が!
そうすると、ログインしているユーザーのページ(=自分のページ)のみにしかアクセスすることができない。
それは問題なんで、user にUserモデルからidを取得してそれを扱うことにする。
こうすることで、他のユーザーのマイページへアクセスすることができる。
routes.rbも編集しておく。

マイページの中身はちなみにこちら。

<div class="show-wrapper">
<div class="show-main">
<div class="show-name">
<p><%= @nickname %>さんの投稿一覧</p>
</div>
<% @requests.each do |request| %>
<div class="show-title">
<p><%= request.title %></p>
</div>
<% end %>
</div>

 

f:id:Kosei_Program:20201226185217p:plain

あとはこの記事タイトルをクリックしたら、記事の詳細ページへ飛んで
そこでコメント打てるようにするなり、記事の編集をできるようにしたい。

詳細ページへのアクセスは記事一覧ページからもアクセスできるようにする。

オリジナルアプリ作成記 3 ~マイグレーションの追加、記事投稿機能の実装、エラーメッセージの表示~

マイグレーションファイルとテーブルにカラムを追加(references)

まずは、依頼投稿機能(記事投稿)のテーブルに投稿したユーザーのuser_idを追加できるようにマイグレーションファイルを追加した。

$ rails g migration AddUserIdToRequests

できたファイルに以下のように追記

 
  def change
    add_reference :requests, :user, foreign_key: true
  end

 

 

ここでforeign_key: trueのように外部キー制約を行うのを忘れずに。
あとはrails db:migrateでおしまい!

 

アソシエーション & ストロングパラメーター

次に投稿してみてuser_idがログインしているユーザーで記録されるように実装。
requestモデルとuserモデルの間でアソシエーションを組むことからスタート。

requestモデル => belongs_to :user
userモデル => has_many :requests

ここでrequestsコントローラーのストロングパラメーターに注意する。

private
def request_params
params.require(:request).permit(:name, :title, :text, :category_id)
.merge(user_id: current_user.id)
end

注意点は、外部キーを統合させる必要がある点。
merge(user_id: current_user.id)でログイン & 記事投稿したユーザーのidも統合されて記録されるようになる。

current_userはdeviseを導入していることで使えるようになる。

これで記事投稿してみると、requestテーブルに無事記録され、user_idもログインしているユーザーのidが記録されていた。

 

redirect_to & render
続いて、記事投稿に成功するとTopページへ失敗すると再度記事投稿ページへ戻される機能を実装。

def create
@request = Request.new(request_params)

if @request.save
redirect_to root_path
else
render :new
end
end

早い話こんな感じ。
特に考察することもなく、テンプレート的な記述。

問題は、renderでエラーメッセージをいかにして表示するか。
レイアウト崩れをどう直すかという点。
明日改めてまとめたいと思う。

今日はおしまい!
ここまで順調に進んでいる気がする!

 

オリジナルアプリ作成記 2 ~記事投稿ページの作成、ActiveHashでカテゴリーの設定~

眠い目こすりながら今日もアプリ作成です。
仕事のある日は、割と簡単な作業だけにして休みの日にガッツリ進めたい。
今日は、前回準備した記事投稿ページのレイアウトと、ActiveHashを設定。

f:id:Kosei_Program:20201221214551p:plain

レイアウトってカッコつけて言ったけども、やはりダセェ。
デザインできる人ってすげぇなって思う。
単に自分がデザインの勉強をしていないだけな気もするけども。


f:id:Kosei_Program:20201221214810p:plain

中身はこんな感じ。
埋め込みrubycssの設定ってどうするんだろ、って思って調べながら
適当にデザイン。
ここまでは特につまづくことなく。

続いてActiveHash。
まずはGemをインストール。

 
gem 'active_hash'

 

追加してbundle installで中身の作成です。

カテゴリーの中身を設定するためのmodelを作成。
マイグレーションファイルは必要ないので、スキップする。
% rails g model category --skip-migration
 
出来上がったmodelにActiveHash::Baseを継承させて設定する。
class Category < ActiveHash::Base
 self.data = [
   { id: 1, name: '--' },
   { id: 2, name: '作詞依頼' },
  { id: 3,........},

] end
こんな感じで設定したいカテゴリーを好きな数だけ追加する。

Requestの方のマイグレーションファイルにこのCategory_idを追加する。
今回は作成時にすでに作ったので、問題ない。

次にこのCategoryモデルとRequestモデルのアソシエーション。
class Request < ApplicationRecord
  extend ActiveHash::Associations::ActiveRecordExtensions
  belongs_to :category
end
ActiveHashを使ったアソシエーションの設定はextend~~のように記述してmoduleを取り込む必要がある。

今度はCategoryモデル側の設定。
  include ActiveHash::Associations
  has_many :requests
この記述を追加する。
これでアソシエーションの設定は終了。

htmlでは、collection_selectでプルダウン形式でカテゴリーを表示させる。
<%= form.collection_select(保存されるカラム名, オブジェクトの配列, カラムに保存される項目, 選択肢に表示されるカラム名, オプション, htmlオプション) %>

大雑把にまとめたけども、今日やった作業はこんな感じ!
休みの日に実際に投稿してみて保存されるかを試したい。