レビュー投稿から新着投稿表示ページの実装
初めましてこんにちは!すんぎぃです。
SoundReviewというアプリができるまでの流れをやっています。
今回は、レビューの新着投稿一覧ページ、レビューの投稿機能について話していきます。
1.目的
音声でのレビューなどの投稿をできるようにします。
基本的に変更されないデータを、データベースに保存せずに取り扱えるようにします。
また、投稿したものを表示して見ることができるようにします。
2.扱うgem
carrierwave
audiojs-rails
active_hash
3.方法
○投稿表示ページ
①まず、routingにて以下のように書き込みます。
Rails.application.routes.draw do
mount RailsAdmin::Engine => '/admin', as: 'rails_admin'
devise_for :users
root to: 'items#index'
resources :items ,only: [:index]
end
まず、コントローラーを動かすためにルーティングを作成します。
railsではresourcesメソッドを使うことで図1のような7つのアクションを使うことができるようになります。
今回は、表示を行いたいのでindexアクションを使い指定したアクションのみのルーティングを生成するためにonlyオプションを使い resources :items ,only: [:index]としています。
②itemsコントローラーを作成します。terminalにて以下のコマンドを実行します。
% rails g controller items
③indexアクションをコントローラーに定義していきます。
以下のようにitemsコントローラーにindexアクションを定義します。
def index
@items = Item.all.order('created_at DESC').limit(12)
end
④投稿一覧のビューを作成します。
app/views/itemsディレクトリにビューファイルindex.html.erbを作成します。
⑤index.html.erbを以下のように編集します。
<%=render "shared/header"%>
<div class='main'>
<div class="main-class">
<div class="categories">
<h1>Category</h1>
<div class="category">
<%= link_to "映画", "/" ,class:"category-name"%>
</div>
<div class="category">
<%= link_to "アニメ", "/" ,class:"category-name"%>
</div>
<div class="category">
<%= link_to "音楽", "/",class:"category-name" %>
</div>
<div class="category">
<%= link_to "本", "/",class:"category-name" %>
</div>
<div class="category">
<%= link_to "その他", "/" ,class:"category-name"%>
</div>
</div>
<video autoplay muted playsinline src="/videos/sound1.mp4",playsinline autoplay loop ></video>
</div>
<div class='item-contents'>
<h2 class='title'>Pick Up</h2>
<div class="subtitle" >
new reviews
</div>
<ul class='item-lists'>
<%@items.each do |item|%>
<li class='list'>
<%= link_to "/",class:"link" do %>
<div class='item-info'>
<%= item.name%>
</div>
<div class='item-img-content'>
<%if item.image.attached? %>
<%= image_tag item.image, class: "item-img" %>
<%else%>
<%= image_tag 'soundreview2.png', class: "item-img" %>
<%end%>
</div>
<div class="profile-all">
<div class="profile2">
<% if item.user.image.attached? %>
<%=link_to "/",class:"user-icon4" do %>
<%= image_tag item.user.image, class: "user-icon4" %>
<%end%>
<% else %>
<%=link_to "/",class:"user-icon4" do %>
<%= image_tag "willy.png", alt: "user-icon", class: "user-icon4" %>
<%end%>
<% end %>
</div>
<div class="item-create">
<div class="item-user-name">
<%= link_to item.user.nickname, "/" %>
</div>
<div class="created_at" >
<%=item.created_at.strftime('%Y/%m/%d')%>
</div>
</div>
<% end %>
</li>
<%end%>
</ul>
</div>
<%= link_to new_item_path, class: 'item-btn' do %>
<span class='item-btn-text'>Post Review</span>
<%= image_tag 'icon_camera.png' , size: '79x50' ,class: "item-btn-icon" %>
<% end %>
</div>
<%=render "shared/footer"%>
背景に使われている画像などは、好きな画像をapp/assets/imagesディレクトリに配置します。
投稿されたものの表示は、eachメソッドを使い表示させます。
○投稿の保存の実装
①Itemモデルの作成を行います。
terminalにて以下のコマンドを実行します。
% rails g model item
②テーブルを作成します。
モデル作成時に作られたdb/migrateディレクトリ以下にあるマイグレーションファイルの編集を行います。
class CreateItems < ActiveRecord::Migration[6.0]
def change
create_table :items do |t|
t.string :name, null: false
t.text :text
t.integer :category_id, null: false
t.string :audio, null: false
t.string :url
t.references :user, null: false, foreign_key: true
t.timestamps
end
end
end
t.に続くのが「型」で、:に続くのが「カラム名」です。
③マイグレーションファイルの実行を行います。
terminalにて以下を実行します。
%rails db:migrate
④音声を投稿するために新たなgemの追加を行います(carrierwave)。
また、音声ファイルの再生機能とダウンロード機能を実装するためのgemの追加を行います(audiojs-rails)。
gem 'carrierwave'
gem 'audiojs-rails'
terminalにて以下を実行します。
% bundle install
④audiojsの読み込みをするためにapp/javascript/packs/application.jsに以下を追記します。
//= require audiojs
④アップローダーの作成を行います。
terminalで以下のコマンドを実行します。
% rails generate uploader Audiofile
⑤データベースに保存したものを使わずにカテゴリー選択をしなくて済むようにします。
まずGemfileに以下を追加します。
gem 'active_hash'
そしてterminalで以下を実行します。
% bundle install
⑥app/modelsディレクトリ以下にcategory.rbを作成します。
⑦Categoryモデルにデータを定義します。
category.rbを以下のように編集します。
class Category < ActiveHash::Base
self.data = [
{ id: 1, name: '---' },
{ id: 2, name: '映画' },
{ id: 3, name: 'アニメ' },
{ id: 4, name: '音楽' },
{ id: 5, name: '本' },
{ id: 6, name: 'その他' }
]
include ActiveHash::Associations
has_many :items
end
ActiveHash::Baseは、モデル内でActiveHashを用いる際に必要となるクラスです。また、ActiveHash::Baseを継承することで、ActiveRecordと同じようなメソッドを使用できるようになります。
⑥itemモデル、Categoryモデル、Userモデルのアソシエーション、バリデーションを設定します。(ここでは、itemモデルについてだけあげています。)
class Item < ApplicationRecord
extend ActiveHash::Associations::ActiveRecordExtensions
belongs_to :category
belongs_to :user
has_one_attached :image
with_options presence: true do
validates :name
validates :category_id
validates :audio
end
validates :category_id, numericality: { other_than: 1 }
mount_uploader :audio, AudiofileUploader
end
ActiveHashを用いてアソシエーションを設定する場合は、ActiveHashで定義されている*moduleをモデルに取り込む必要があります。
ここでは
extend ActiveHash::Associations::ActiveRecordExtensions
としてmoduleを取り込んでいます。
*moduleとは、特定の役割を持つメソッドや定数に役割をつけてまとめたものです。
⑦routingにて以下のように書き込みます。
Rails.application.routes.draw do
mount RailsAdmin::Engine => '/admin', as: 'rails_admin'
devise_for :users
root to: 'items#index'
resources :items ,only: [:index,:new]
end
生成、保存をしたいのでnewアクション、createアクションをつけました。
⑧itemsコントローラーを以下のように編集します。
class ItemsController < ApplicationController
before_action :authenticate_user!, except: [:index]
def new
@item = Item.new
end
def create
@item = Item.new(item_params)
if @item.save
redirect_to root_path
else
render :new
end
end
def index
@items = Item.all.order('created_at DESC').limit(12)
end
private
def item_params
params.require(:item).permit(:name, :text, :audio, :category_id, :url, :image, :minicategory).merge(user_id: current_user.id)
end
end
createアクションは、フォームで送られてきたデータをもとに、レコードを保存します。ここでは、*ストロングパラメーターを使った保存を行なっています。また、privateメソッドを使うことによってclassの外部から呼ばれたら困るメソッドを隔離することができ、また可読性を高めることができます。
*ストロングパラメーターを使う理由としては、悪意のあるユーザーによる意図しないデーターベースの読み書きを防ぐためです。
ストロングパラメーターの定義として、*requireメソッドと、*permitメソッドを組み合わせてします。
*requireメソッドは、パラメーターからどの情報を取得するか選択します。ストロングパラメーターとして使用する場合は、主にモデル名を使用します。
*permitメソッドは、取得したいキーを指定でき、指定したキーと値のセットのみを取得します。permitメソッドを使うことによってカラムに保存したいものだけに絞ることができます。
⑨app/views/itemsディレクトリにビューファイルnew.html.erbを作成します。
⑩new.html.erbを以下のように編集します。
<div class="items-sell-contents">
<header class="items-sell-header">
<%= link_to image_tag('soundreview.png' , size: '185x50'), "/" %>
</header>
<div class="items-sell-main">
<h2 class="items-sell-title">レビューの情報を入力</h2>
<%= render 'shared/error_messages', model: @item %>
<%= form_with model:@item, local: true do |f| %>
<div class="items-detail">
<div class="weight-bold-text">
カテゴリー
<span class="indispensable">*必須</span>
</div>
<div class="form">
<%= f.collection_select(:category_id, Category.all, :id, :name, {}, {class:"select-box", id:"item-category"}) %>
</div>
</div>
<div class="new-items">
<div class="weight-bold-text">
サブカテゴリー
</div>
<%= f.text_area :minicategory, class:"items-text", id:"item-name" %>
</div>
<div class="img-upload">
<div class="weight-bold-text">
レビュー画像
</div>
<div class="click-upload">
<p>
クリックしてファイルをアップロード
</p>
<%= f.file_field :image, id:"item-image" %>
</div>
</div>
<div class="new-items">
<div class="weight-bold-text">
作品名<span class="counter">(40文字まで)</span>
<span class="indispensable">*必須</span>
</div>
<%= f.text_area :name, class:"items-text", id:"item-name" %>
</div>
<div class="sound-upload">
<div class="weight-bold-text">
音声レビュー
<span class="indispensable">*必須</span>
</div>
<div class="click-upload">
<p>
クリックしてファイルをアップロード
</p>
<%= f.file_field :audio, id:"item-sound" %>
</div>
</div>
<div class="new-items">
<div class="items-explain">
<div class="weight-bold-text">
文字レビュー
</div>
<%= f.text_area :text, class:"items-text", id:"item-info"%>
</div>
</div>
<div class="item-url">
<div class="weight-bold-text">
作品URL
</div>
<%= f.text_area :url, class:"url", id:"item-name",class:"url" %>
</div>
<div class="sell-btn-contents">
<%= f.submit "投稿する" ,class:"sell-btn" %>
<%=link_to 'もどる', root_path, class:"back-btn" %>
</div>
</div>
<% end %>
<footer class="items-sell-footer">
<%= link_to image_tag('soundreview.png' , size: '185x50'), "/" %>
<p class="inc">
©︎SoundReview,Inc.
</p>
</footer>
</div>
form_withを使い、レビュー投稿フォームを実装します。form_withは、modelオプションに指定されたインスタンス変数の状態により、リクエストを送るアクションを判断しています。@itemは、newアクションで定義したインスタンス変数であるため、リクエストは、createアクションに送信されます。
11.app/assets/stylesheets/items以下にindex.scssとnew.cssを追加し以下のように編集します。
index.scss
* {
box-sizing: border-box;
margin:0;
padding:0;
}
.wrap{
overflow: hidden;
}
.main{
width:100vw;
position:relative;
}
.ma{
background-color: rgb(156, 206, 221);
}
video{
width: 100%;
}
.categories{
position: absolute;
left: 10vh;
top: 3vh;
z-index: 1;
}
.category{
font-family: serif;
color: white;
font-size: 3.5vw;
cursor: pointer;
}
.categories h1{
font-family:serif;
color: white;
font-size:5vw;
}
.item-name{
color:black;
}
.item-contents {
background-color: #FFF;
display: flex;
flex-direction: column;
align-items: center;
padding: 10vh 0;
}
.item-contents>.title {
font-size: 5vh;
line-height: 1.4;
font-weight: bold;
}
.item-contents>.subtitle {
font-size: 3vh;
margin: 1vh 0;
text-decoration: none;
color: black;
font-weight: bold;
}
.item-user-name{
font-size:26px;
margin:2.7vh 0 0 0;
}
.item-img {
width: 33vh;
height: 33vh;
}
.good-county{
margin-top:4vh;
margin-left:23px;
}
.item-btn {
width: 130px;
background: rgba(87, 89, 109,0.5);
text-align: center;
border-radius: 10%;
bottom: 32px;
right: 32px;
position: fixed;
z-index: 10;
padding: 10px ;
text-decoration: none;
}
.item-btn-text {
color: #fff;
display: block;
font-size: 18px;
text-decoration: none;
margin-bottom: 5px;
}
.item-btn-icon {
width: 60%;
}
.title,.subtitle,.item-info{
font-family:inherit;
}
.item-info{
font-weight: bold;
}
.item-lists {
width: 100vw;
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
.item-lists>.list {
width:25%;
padding: 1vw;
background-color: #FFF;
}
new.css
.items-sell-contents {
background-color: rgb(156, 206, 221);
font-family: "Source Sans Pro", Helvetica, Arial, "游ゴシック体", "YuGothic", "メイリオ", "Meiryo", sans-serif;
font-size: 14px;
display: flex;
align-items: center;
flex-direction: column;
}
.items-sell-header {
padding: 5vh 0;
text-align: center;
}
.items-sell-main {
background-color: #FFF;
width: 70vw;
min-width: 450px;
padding: 10vh 15vw;
font-size:18px;
}
.items-sell-title {
font-size: 24px;
font-weight: bold;
text-align: center;
}
.items-comment-title {
font-size: 20px;
font-weight: bold;
text-align: center;
margin-top:50px;
border:1px solid black;
}
.indispensable {
margin-left: 8px;
padding: 3px;
border-radius: 3px;
color: #ea352d;
font-size: 12px;
font-weight: bold;
}
.weight-bold-text {
font-weight: bold;
min-width: 170px;
margin-bottom: 10px;
}
.counter{
font-size:18px;
}
.select-box {
height: 30px;
width: 100%;
background-color: #f5f5f5;
padding-left: 20px;
}
.img-upload {
display: flex;
flex-direction: column;
flex-wrap: wrap;
padding: 2vh 0;
}
.click-upload {
margin: 15px 0;
min-width: 250px;
}
.click-upload>p {
margin-bottom: 15px;
font-size:14px;
}
.new-items {
padding: 2vh 0;
}
.items-text {
width: 100%;
padding: 16px 16px 8px;
border-radius: 4px;
border: 1px solid #ccc;
font-size: 16px;
resize: none;
}
.items-detail {
padding: 2vh 0;
}
.items-detail>.form {
width: 200px;
padding: 2vh 0;
}
.select-box {
margin: 2vh 0;
}
.item-name{
width:100vh;
}
.url{
width:100%;
}
.sell-btn-contents {
width:100%;
text-align: center;
display:flex;
flex-direction: column;
align-items: center;
}
.sell-btn {
background: #f85408;
border: 1px solid #f85408;
color: #fff;
width: 53%;
height: 50px;
font-size: 18px;
margin: 4vh 0;
border-radius: 30px;
}
.back-btn {
background: #aaa;
border: 1px solid #aaa;
color: #fff;
padding: 15px 20%;
text-decoration: none;
border-radius: 30px;
}
.items-sell-footer {
background-color: rgb(156, 206, 221);
width: 60vw;
padding: 3ch 0;
display: flex;
flex-direction: column;
align-items: center;
color:white;
}
.items-sell-footer>.menu {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: 3vh 0;
}
.menu>li>a {
color:white;
font-size: 12px;
margin: 2vw;
text-decoration: none;
}
.inc {
font-size: 13px;
}
img{
max-width:100%;
height:auto;
}
.f-img{
max-width:450px;
height:450px;
}こ
また、app/viewsディレクトリにsharedディレクトリを作成し以下のファイルをダウンロードし配置します。
app/assets/stylesheets/shared以下にもファイルを配置します。
terminalにて以下のコマンドを実行し
% rails s
以下のように表示せれユーザーログインし投稿ができ表示することができたら完成です。
これにて完成です。ありがとうございました。