概要
作成したアプリケーションの名前や、サービスの説明、なぜこのようなサービスにしたのかを説明いたします。
技術的にどう実装しているかは、システム概要で説明いたします。
名前
DietS
サービス内容
・体重やBMIの管理・グラフによる確認をすることができる
・同志を応援したり、逆に応援してもらったり、コメントをして交流をすることができる
システム概要
作成したアプリケーションの技術的な仕組み、使用した技術等をモデルごとに説明いたします。
技術的なコードレベルの実装の説明が必要な場合は、引用の表示で説明いたします。
User
モデル
機能
基本的なCRUD機能
ユーザの作成・更新・削除ができます。削除に関しては管理者のみができる仕様としています。
管理者機能
管理者のみがすることのできる機能をいくつか実装しました。管理者であるかどうかはAdminコラムがtrueであれば、管理者というような設定にしており、デフォルトではFalseとなっています。
例えば、ユーザの削除は管理者のみしかできない仕様になっている他、後述するコメント機能においては、基本的に「コメント欄のプロフィール本人」「コメントをした人」がコメントを削除することができるが、管理者であればすべてのコメントを削除できるなどがあります。
削除に関してはビューレベルで削除リンクを見えなくするだけではなく、コントローラのBefore_Action(以下BA)や、アクション内でもAdmin属性のチェックを入れることで、確実に削除は管理者しかできないようにしてあります。
def admin
unless current_user.admin?
flash[:danger] = "管理者でログインしてください"
redirect_to "/"
end
end
def destroy
if current_user.admin?
like = Like.where(pro_id:params[:id]).or(Like.where(user_id:params[:id]))
like.destroy_all
User.find(params[:id]).destroy
flash[:success] = "削除しました"
redirect_to ("/users")
else
flash.now[:danger] = "権限がありません"
@incpriusers = User.all.paginate(page: params[:page], per_page:20).reorder(created_at: :desc)
render 'users/index'
end
end
非公開設定機能
体重や身長・BMIが表示されるサービスであることから、1人で記録用に使いたいという人のために非公開ユーザという仕様を作成しました。
非公開ユーザはチェックボックス一つで設定することができ、非公開ユーザにすればユーザ一覧ページには表示されず、またプロフィールページも表示されることはなくなるため、体重やBMIが見られるといった心配はなくなります。
BMI計算機能
体重だけでなく身長もデータに登録した場合は、BMIが自動で計算され、グラフに表示されます。
もちろん、身長が誤差でも変わればプロフィールから更新し、それを元に過去の体重データと対応したBMIが新たに計算され表示されるため、身長データを更新しても最新の身長に基づいたBMIがわかります。
グラフはChartkickというgemを使用して実装しています。
Chartkickのグラフに入れるデータとして「体重」「日付」や「BMI」「日付」といった2種類のコラムからなるデータが必要だったため、それは下記のようにpluckメソッドを使用して取得しました。
またグラフ表示部分は繰り返し何度も使用するため、リファクタリングの段階でパーシャルにし、コードの可読性や保守性をあげることができました。
<% data = user.graphs.pluck(:calc_date,:weight) %>
<% databmi = user.graphs.pluck(:calc_date,:bmi) %>
<% if user.height? %>
<%= line_chart [{name:"体重",data:data},{name:"BMI",data:databmi}],label: "体重(kg)",curve: false ,points: false%>
<% else %>
<%= line_chart data ,label: "体重(kg)",curve: false ,points: false%>
<% end %>
目標設定機能
ユーザが目標体重を設定した場合、最新の体重から計算された差を表示するようにプロフィールページが変わります。
またその差が0以下になった時、つまり目標を達成した場合は、目標を達成したユーザのみに付与されるアイコンが名前の横に表示されるようにし、目標達成を頑張る動機付けとして作りました。
アイコンは他にも管理者アイコンや、非公開ユーザのアイコンがあり、それぞれAdmin、Private、Goal(目標達成済ユーザ)の真偽値のコラムで判断し、それぞれに応じたアイコンが表示されます。
↓
アイコンのビューに関しては、いろいろなところで使用するため、パーシャルで用意し、様々な場所で使用しています。
<% if user.private? %>
<span class="fas fa-key data-toggle=" tooltip" title=" 非公開ユーザ"="非公開ユーザ""></span>
<% end %>
<% if user.admin? %>
<span class="fas fa-check-square official-btn data-toggle=" tooltip" title=" 公式ユーザー""="公式ユーザー"""></span>
<% end %>
<% if user.goal? %>
<span class="fas fa-star goal-btn data-toggle=" tooltip" title=" 目標達成済ユーザー""="目標達成済ユーザー"""></span>
<% end %>
一覧表示機能
文字通り全ユーザの一覧が表示される機能ですが、一般的な公開ユーザからは非公開ユーザ以外のユーザが表示されるようになっており、また管理者だけ非公開ユーザを含めた全てのユーザが表示されるようになっています。
ただ非公開ユーザ本人からは、自分だけは一覧に表示されるという仕組みも考えましたが、「非公開なのになぜ表示されてるの?」という不安に繋がる可能性が考えられたため、たとえ自分自身でもユーザ一覧には表示されない仕組みとしています。
プロフィール画像設定機能
画像全般は、本番環境ではAmazon AWS S3というクラウドストレージサービスを利用しています。
プロフィール画像ではできませんが、投稿した画像はすべてリサイズされた画像となっており、下のサイズの画像も「拡大表示」リンクから見ることができるようになっています。
↓拡大表示リンクで元の画像が違うタブで表示されます。
拡大表示の際のURLは、"/image/:id"ですが、:idに画像が入っていないコメントテーブルのレコードのIDなどを指定されてもエラーが出ないようにしています。
画像の場合は、コラム名はimageですが、存在をチェックする際は.image_urlでないと指定することができません。
def expand
@image = Comment.find_by(id:params[:id])
if @image && @image.image_url
render ('images/expand')
else
flash[:danger] = "無効なURLです。"
redirect_to "/users/#{current_user.id}"
end
end
Like
ユーザ同士でコミュニケーションをとることで、1人では辛いダイエットも頑張ることができるというのはこのサービスの大事にしている要素で、そのコミュニケーションの一つとして「応援している」を表現するいいねを実装しました。
モデル
一意(セット):user_idとpro_idの組み合わせは一意であるという制限。
機能
いいね機能全体としては、Ajaxの非同期通信をここで使用することで、ページ遷移をすることなく、いいねやいいねの取り消しができるようになっています。
いいねを送る機能
いいねを削除する機能
いいねを送るページは、「ユーザのプロフィールページ」「ユーザの一覧ページ」「ユーザのいいねリスト(以下LIKESリスト)」「ユーザのいいねされたリスト(以下LIKEDリスト)」です。
元々Ajaxを取り入れずページ遷移でのいいねを実装していましたが、それだと例えばユーザ一覧で2ページ目にいるユーザにいいねをした時など、いちいちページが移ってまた元いた場所にスクロールして戻らなければいけないというとてもユーザーフレンドリーとは言えない仕様になっていました。
そこでAjaxで部分的に更新することを可能にしたことで、上述したような不親切なアクションは起きず、いいねのアイコンだけが変化するといった仕様にすることができています。
※管理者での画面であるため、非公開ユーザも一覧に表示されています。
いいねをユーザ一覧画面で行う時、仮にその一覧に自分も表示されていた場合、自分のLIKESリストも数が増減するようにしました。
具体的には、再読み込みする場所を指定するJSで、現在のログインユーザ、つまり自分自身の場所も更新するものとして指定することで実現しています。
$('.likeindex_<%=@user.id%>').html("<%= j(render partial: 'shared/like', locals: {user:@user}) %>");
$('.likeindex_<%=@current_user.id%>').html("<%= j(render partial: 'shared/like', locals: {user:@current_user}) %>");
また、ユーザ一覧画面は同じ表示が繰り返されるため、再読み込みするクラス名を固有のものにしないと、表示される全ての部分が更新されるという仕様になってしまいます。
それを防ぐため、クラスを下記のようにユーザ個別にすることで、更新されるのを特定の場所のみにしています。
userには、each文で繰り返し入れられる値が入っています。
〜
<span class="likeindex_<%=user.id%>">
<%= render 'shared/like',user:user%>
</span>
〜
LIKESリストを表示する機能
LIKEDリストを表示する機能
リストにはプロフィールページからアクセスすることができ、そのページのユーザが誰にいいねをしているのか、誰からいいねをされているのかということがわかるようになっています。
もちろんそのページからもいいねやいいねの削除ができます。
また、誰からもいいねをもらっていないユーザの時は、いいねを促進するようなメッセージを表示することでユーザ間のコミュニケーションを助長させる仕組みにしています。
Graph
体重を計測日とともにグラフで記録することで、自身体重の変化を視覚的にわかりやすくすることができました。
また必要であれば身長をプロフィールに登録しておくことで、BMIも体重と計測日とともに表示されます。
モデル
※黄色は外部キー制約のコラムです。
Userモデルのuser_idを外部キーにしています。
機能
体重・BMIと日付の登録機能
ユーザはプロフィールページとグラフ編集ページから、体重や日付を記録することができます。
また、ユーザがプロフィールに身長を登録していた場合、自動でBMIが計算され、BMIも同じグラフに表示されるようになります。
日付の登録に関しては、「XXXX年XX月XX日」に変換した上でデータとして保存しないと、グラフに何も登録されていなかった場合に、グラフに表示されないと言う問題がありました。
そのため、下記のようにしてXXXX-XX-XXを「XXXX年XX月XX日」に変換して保存させています。
def dayTransform(date)
date = Date.strptime(date,'%Y-%m-%d')
date = date.strftime("%Y年%m月%d日")
return date
end
end
def calculate
user = User.find_by(id:current_user.id)
date = dayTransform(params[:graph][:calc_date])
if registerBmi(user,date,params[:graph][:weight])
flash.now[:success] = "投稿しました"
redirect_to("/users/#{current_user.id}")
else
@comment = Comment.new
@graph = Graph.new(weight:params[:graph][:weight])
flash.now[:danger] = "体重は半角数字のみ、5桁以内で入力してください"
render ('users/show')
end
end
体重・BMIと日付の削除機能
BMI・身長データの削除機能
ユーザはグラフ編集ページから登録された体重と日付のデータを削除することができます。
また、身長のデータがある場合は「BMI削除」ボタンが表示されるようになっており、それを使うと登録されているBMIと身長のデータが削除されます。
Comment
このサービスの目指すところである「みんなとダイエット」を実現するための二つ目のコミュニケーションツールとしてコメントを実装しました。
コメントは各ユーザのプロフィールページに投稿されますが、文章だけでなく画像も一緒に投稿することもできます。コメント欄から交流が始まったり、そこで交流を深めることができます。
モデル
機能
コメントの投稿
コメントに関してはどんなユーザでも投稿することができます。たとえ非公開ユーザであっても、対象が公開ユーザであればコメントをすることができます。(非公開ユーザにはコメントができません)
また、コメント欄では自分自身の名前がわかりやすくハイライトされるので、自分のコメントがどれか探す必要はなく、一眼でわかるようにしています。
また画像に関しては拡大表示リンクを用意しており、そちらから遷移すれば元のサイズの画像を別タブで確認することができます。
拡大表示の際のURLは、"/image/:id"ですが、:idに画像が入っていないコメントテーブルのレコードのIDなどを指定されてもエラーが出ないようにしています。
画像の場合は、コラム名はimageですが、存在をチェックする際は.image_urlでないと指定することができません。
def expand
@image = Comment.find_by(id:params[:id])
if @image && @image.image_url
render ('images/expand')
else
flash[:danger] = "無効なURLです。"
redirect_to "/users/#{current_user.id}"
end
end
コメントの削除
コメントの削除はセキュリティの観点から、「管理者」、「該当のプロフィールページのユーザ」、そして「コメントを投稿したユーザ本人」のみが行える仕様にしました。
当然ですが、他人のコメントを削除することはできません。
コメント削除のアクションの前のBAとして、「管理者であるか」と「削除するコメントが存在しているか」をチェックするメソッドを用意しました。そのため、ビューレベルで削除リンクが見えないのはもちろんのこと、直接URLを投げられても削除することはできません。
def correct_user_comment_and_exist
comment = Comment.find_by(id:params[:id])
if comment
unless comment.put_id == current_user.id || comment.user_id == current_user.id || current_user.admin?
flash[:danger] = "正しいユーザーでログインしてください"
redirect_to request.referer || "/users/#{current_user.id}"
end
else
flash[:danger] = "削除するコメントがありません"
redirect_to request.referer || "/users/#{current_user.id}"
end
end
Contact
不具合や要望を管理者に連絡するための手段として、問合せフォームを作成しました。
問合せフォームでは、文字による報告だけでなく、画像による問題箇所などの報告もすることができます。
モデル
機能
問い合わせ送信
ユーザは問い合わせフォームから管理者に連絡したいことを伝えることができます。
ボタンは「送信」となっていますが、実際には「投稿」が正しく、管理者からはユーザからの投稿されたcontentやimageを同じビューで確認することができます。
表示はAdmin属性の真偽値で判断し、内容を変えています。
また、管理者のページではここでも画像拡大リンクを用意したので、リサイズされる前の元のサイズの画像を確認することができるようになっています。
拡大表示の際のURLは、"/contact/image/:id"ですが、:idに画像が入っていないコンタクトテーブルのレコードのIDなどを指定されてもエラーが出ないようにしています。
画像の場合は、コラム名はimageですが、存在をチェックする際は.image_urlでないと指定することができません。
def contactexpand
@image = Contact.find_by(id:params[:id])
if @image && @image.image_url
render ('images/contactexpand')
else
flash[:danger] = "無効なURLです。"
redirect_to "/contact"
end
end
end
問い合わせ削除
問い合わせの削除は、管理者しか行えないように制限しています。
ここでもビューレベルだけでなく、サーバサイドレベルでフィルターをかけています。
def correct_user_contact_and_extst
contact = Contact.find_by(id:params[:id])
if !current_user.admin?
flash.now[:danger] = "正しいユーザーでログインしてください"
@contact = Contact.new
render ('contacts/form')
elsif contact.nil?
@contact = Contact.new
flash.now[:danger] = "削除する問い合わせがありません"
render ('contacts/form')
end
end