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

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

オリジナルアプリ作成記 10 ~ 画像の投稿 ~

チャット画面にて画像の投稿をできるように実装する。

Active Storage
Active Storageは画像を投稿するためのGem。
Railsに最初から統合されている。


Image Magick
Image Magickという画像加工ツールをダウンロードする。
画像の作成、サイズ変更、保存形式の変更などができるようになる。
Image MagickはソフトウェアなのでHomebrewでダウンロードする必要がある。
さらにこのImage Magickを使用するためには、Mini MagickというGemをインストールする。

おまけに画像のサイズ変更をするためには、ImageProcessingというGemをインストールする必要がある。

Image Magick = Mini Magick + Image Processing

とまぁこんな感じ。

image magickをターミナルでインストール。

% brew install imagemagick

Gemの追加。

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

追加したなら、ターミナルでインストール。

% bundle install


これでようやくActive Storageを導入する準備ができた。

Active Storageの導入

% rails active_storage:install

するとマイグレーションファイルが生成されるのでマイグレートを実行する。

% rails db:migrate

 

画像の保存

画像を保存するためには対象となるモデルとアソシエーションを組む必要がある。
この場合、チャット画面(=Messageモデル)と組むことになる。

f:id:Kosei_Program:20210120212621p:plain

has_one_attachedを記述したモデルの各レコードは、それぞれ一つのファイルを添付することができる。
この場合はimageを添付することができるようになる。
=>Message.imageと記述することで、モデルに添付された画像へとアクセスすることができるようになる。

もちろんcontrollerでストロングパラメータの追記を行う必要もある。

f:id:Kosei_Program:20210117233844p:plain

前回も貼り付けた画像と同じだが
message_paramsの部分のpermitの項目にimageを追加している。

これで画像の保存ができるようになった。

保存した画像の表示

f:id:Kosei_Program:20210120213350p:plain

image_tagでimage要素を生成することができる。
複雑なディレクトリパスを指定しなくても、モデルから画像を呼び出して、引数に記述するだけで画像を表示することができる。
<%= image_tag message.image %>

これに加えて、画像が存在しない時に発生するエラーを防ぐために
条件分岐でattached?を記述する。
これにより画像が存在する場合のみ、image_tagが実行されるようになる。

画像の大きさは、variant(resize: ~~~)で変更することができる。
モデル.ファイル名.variant(resize: ~~~)


バリデーションの変更

画像かテキストどちらかの場合でもチャットできるように、バリデーションに条件分岐を追加する。
上の画像ですでに記述しているが、
validates :content, presence: true, unless: :was_attached?

def was_attached?
 self.image.attached?
end
の部分がこの条件分岐にあたる。
文章に対して、unlessを追加し、画像が添付されているか確かめるメソッドを定義し
添付されていなければ(unlessがfalse)文章に対してのバリデーションが行われる。
これにより、画像がなければ文章のみ投稿され、文章がなければ画像が存在しなければならないという処理になる。

次回は、実際にチャット機能を動かしてみる。

オリジナルアプリ作成記 9 ~チャット画面の実装~

前回でチャットルームへのアクセス制限を行った。
その際には、チャットルームへのアクセス制限というよりは、
チャット画面への移動を制限することでアクセス制限をした。
=> チャット機能のコントローラーへ記述。

チャット画面

f:id:Kosei_Program:20210118231446p:plain

f:id:Kosei_Program:20210118231526p:plain

実際の画面はこんな感じ。
ビューファイルのコードは

f:id:Kosei_Program:20210118231657p:plain

form_withで実際にチャットのメッセージを投稿する際にmodelにroomとmessageに対して送信していることに注意する。
これはroomにmessageをネスト、ルーティングを入れ子しているためにこのように記述する必要がある。
=> roomに属するmessageというように親子関係である。
この@roomにはどのチャットルームか、@messageにはMessageモデルからのインスタンスを予めセットしておく必要がある。
=> コントローラー(indexアクション)でセットする。

f:id:Kosei_Program:20210117233844p:plain

投稿された全てのメッセージを表示するために@messagesとして、
@messages = @room.messages.includes(:user)
取得した特定の部屋@roomに紐づいている、messages(全てのメッセージ)を取得。
=> アソシエーションを組んでいるためにこのような記述が可能。
さらにこの取得したメッセージにはユーザーの情報も取得したいので、
N+1問題(必要なデータを取得するために何度もアクセスすること)を防ぐために
includesメソッドを使用して、取得する。
=>メッセージに紐づいたユーザー情報も一度に取得することが可能。

メッセージの投稿

実際にメッセージを投稿(保存)する際には、
投稿する目的の部屋(@room)を取得して、
それを利用して、投稿するメッセージ(@message)を
@room.messages.new(message_params)として保存。
message_paramsのストロングパラメーターには、
messageモデルへ、本文と画像(後に実装)と投稿するユーザーのidを一緒に保存する。
保存に成功すれば(条件分岐)、投稿したページ(チャット画面へ)
失敗すれば、その時点で投稿されていたメッセージを全て取得して表示。
=>renderでindexアクションへ持っていく。

これはindexのビューファイルに
<%= render partial: 'message', collection: @messages %>
と記述して、このcreateアクションのrenderをここに持っていくといったイメージ。

次回は画像投稿についてまとめる。

オリジナルアプリ作成記 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

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

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