Railsにこれから初めて触れる方を対象にしたチュートリアルです ログイン機能もある簡単なQ&Aサイトを作成するチュートリアルになります
まず、rails newを実行し、Railsアプリのひな型を作成します。
rails new qa --webpack=stimulus
--webpackはRailsでWeboackを使いやすくしたWebpackerというものを使用するというオプションです
Vue、React、Angular、Elm、Stimulusを使用することができます
今回はStimulusを使用するので--webpack=stimulusとしています
ちなみに、StimulusはRailsの生みの親であるDHH氏がCTOを務めるBasecampが開発したJavaScriptフレームワークです
非常にシンプルで動きのあるWebを実現できます
次に、作成したRailsアプリのディレクトリへと移動します。
cd qa
Webpackerを使う場合、ruby ./bin/webpack-dev-serverというコマンドを実行しつつ、rails sでローカルサーバーを起動する必要があります
その為、現状のままではターミナルを複数開いておく必要があり、少々面倒です
そこで、複数のコマンドを並列して実行できるforemanを使用します
まず、Gemfileにgem 'foreman'を追記します
gem 'foreman'
その後、bundle install
bundle install
次に、foremanで使用するProcfile.devを作成します
web: bundle exec rails s
webpacker: ruby ./bin/webpack-dev-server
あとは、foreman start -f Procfile.devをターミナルで実行するだけです
foreman start -f Procfile.dev
localhost:5000にアクセスできればOkです(foremanを使用する場合、使用するポートが5000へと変更されています)
rails g scaffold コマンドを使い、質問投稿のひな型を作成します
rails g scaffold question title:string content:text
その後、rails db:migrateでマイグレーションを行います
rails db:migrate
config/routes.rbを修正し、rootへのルーティングを作成します
Rails.application.routes.draw do
root 'questions#index'
resources :questions
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
あとはforeman start -f Procfile.devを実行して、localhost:3000にアクセスします
ページが表示されていればOKです
質問投稿サイトですので、コメントを投稿できるようにしたいと思います
まず、コメントを取り扱うCommentモデルを作成します。
rails g model comment content:text qustion:references
マイグレーションを行います
rails db:migrate
app/models/question.rbにCommentとの関連付けを記述します
class Question < ApplicationRecord
has_many :comments
end
そして、config/routes.rb にてルーティングを設定します
Rails.application.routes.draw do
root 'questions#index'
resources :questions do
resources :comments, :only => [:create, :destroy]
end
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
次に、各質問へのコメントフォームを作成していきます。
まずは下記のようにapp/views/questions/show.html.erbを変更します
<p id="notice"><%= notice %></p>
<p>
<strong>Title:</strong>
<%= @question.title %>
</p>
<p>
<strong>Content:</strong>
<%= @question.content %>
</p>
<h2>Comments</h2>
<div id="comments">
<%= render @question.comments %>
</div>
<%= render 'comments/new', question: @question %>
<%= link_to 'Edit', edit_question_path(@question) %> |
<%= link_to 'Back', questions_path %>
変更箇所としてはこの部分になります
<h2>Comments</h2>
<div id="comments">
<%= render @question.comments %>
</div>
<%= render 'comments/new', question: @question %>
<%= render @question.comments %> の部分でコメントを一覧できるviewファイルをパーシャルとして呼び出しています
また、<%= render 'comments/new', question: @question %> の部分では新規作成するコメントのviewファイルをパーシャルとして呼び出しています
パーシャルとして呼び出している部分をそれぞれ作っていきます
まず、app/views/comments/_comment.html.erb を作成し、下記のようにします。
<p><%= comment.content %></p>
<p><%= link_to "Delete", [@question, comment], method: :delete, data: { confirm: 'Are you sure?' } %>
次に、app/views/comments/_new.html.erbを作成します
<%= form_with(model: [ @question, Comment.new ], remote: true) do |form| %>
Your Comment:<br>
<%= form.text_area :content, size: '50x20' %><br>
<%= form.submit %>
<% end %>
これで、コメントの入力フォームが表示されるようになっているはずです
次に、コメントの作成と削除を行うアクションを作成します
app/controllers/comments_controller.rbを作成します
class CommentsController < ApplicationController
before_action :set_question
def create
@question.comments.create! comments_params
redirect_to @question
end
def destroy
@question.comments.destroy params[:id]
redirect_to @question
end
private
def set_question
@question = Question.find(params[:question_id])
end
def comments_params
params.required(:comment).permit(:content)
end
end
foreman start -f Procfile.dev でローカルサーバを建てて、実際にコメントが作成&削除できていればOKです。
まず、app/javascript/controllers/question_controller.jsとapp/javascript/controllers/comment_controller.jsを作成します
import { Controller } from "stimulus"
export default class extends Controller {
static targets = ["content", "preview"]
content() {
this.previewTarget.innerHTML = this.contentTarget.value
}
}
import { Controller } from "stimulus"
export default class extends Controller {
static targets = ["content", "preview"]
content() {
this.previewTarget.innerHTML = this.contentTarget.value
}
}
次に、app/views/questions/_form.html.erbとapp/views/comments/_new.html.erbを以下のように編集します
<div data-controller="question">
<div class="form-left">
<%= form_with(model: question, local: true) do |form| %>
<% if question.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(question.errors.count, "error") %> prohibited this question from being saved:</h2>
<ul>
<% question.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :content %>
<%= form.text_area :content, 'data-target': 'question.content', 'data-action': 'input->question#content' %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
</div>
<div class="form-right">
<h1>Content</h1>
<div data-target="question.preview"></div>
</div>
</div>
<%= javascript_pack_tag 'application.js' %>
<div data-controller="comment">
<div class="container">
<div data-target="comment.preview"></div>
</div>
<%= form_with(model: [ @question, Comment.new ], remote: true) do |form| %>
Your Comment:<br>
<%= form.text_area :content, 'data-target': 'comment.content', 'data-action': 'input->comment#content', size: '50x20' %><br>
<%= form.submit %>
<% end %>
</div>
<%= javascript_pack_tag 'application.js' %>
最後に、app/assets/stylesheets/questions.scssを以下のように修正します
// Place all the styles related to the posts controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
.form-left{
width: 50%;
float: left;
}
.form-right{
width: 50%;
float: right;
}
これでリアルタイムプレビュー機能が実装できました!
まず、config/routes.rbを編集し、:editと:updateをルーティングに追加します
Rails.application.routes.draw do
root 'questions#index'
resources :questions do
resources :comments, :only => [:edit, :create, :update, :destroy]
end
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
その後、app/controllers/comments_controller.rbにeditアクションとupdateアクションを追加します
またこの時、before_actionを使用し、@commentをセットできるようにしています
class CommentsController < ApplicationController
before_action :set_question
before_action :set_comment, only: [:edit, :update]
def edit
end
def create
@question.comments.create! comments_params
redirect_to @question
end
def update
@comment.update(comments_params)
redirect_to @question
end
def destroy
@question.comments.destroy params[:id]
redirect_to @question
end
private
def set_question
@question = Question.find(params[:question_id])
end
def set_comment
@comment = Comment.find(params[:id])
end
def comments_params
params.required(:comment).permit(:content)
end
end
あとは、app/views/comments/_form.html.erbとapp/views/comments/edit.html.erbを作成します
<div data-controller="comment">
<div class="container">
<div data-target="comment.preview"></div>
</div>
<%= form_with(model: [ @question, @comment ], remote: true) do |form| %>
Your Comment:<br>
<%= form.text_area :content, 'data-target': 'comment.content', 'data-action': 'input->comment#content', size: '50x20' %><br>
<%= form.submit %>
<% end %>
</div>
<h1>Editing Comments</h1>
<%= render 'form' %>
<%= link_to 'Show', @question %> |
<%= link_to 'Back', questions_path %>
これで、コメント欄のEditリンクからコメントを編集できるようになります
trixをGemfileに追加します
gem 'trix'
その後、bundle install を実行します
bundle install
bundle installした後、app/assets/javascripts/application.jsに//= require trixを追加します
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require rails-ujs
//= require activestorage
//= require trix
//= require turbolinks
//= require_tree .
次に、app/assets/javascripts/application.cssをapp/assets/javascripts/application.scssにリネームして以下のように変更します
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/
@import "trix";
@import "trix"; を追加しただけですね
最後に、app/views/questions/_form.html.erb、app/views/questions/index.html.erb、
app/views/questions/show.html.erb、app/views/comments/_form.html.erb、
app/views/comments/_new.html.erb、app/views/comments/_comment.html.erbを以下のように変更します。
<div data-controller="question">
<div class="form-left">
<%= form_with(model: question, local: true) do |form| %>
<% if question.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(question.errors.count, "error") %> prohibited this question from being saved:</h2>
<ul>
<% question.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :content %>
<%= form.hidden_field :content, id: :content_text %>
<trix-editor input="content_text" data-target="question.content", data-action="input->question#content"></trix-editor>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
</div>
<div class="form-right">
<h1>Content</h1>
<div data-target="question.preview"></div>
</div>
</div>
<%= javascript_pack_tag 'application.js' %>
<p id="notice"><%= notice %></p>
<h1>Questions</h1>
<table>
<thead>
<tr>
<th>Title</th>
<th>Content</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @questions.each do |question| %>
<tr>
<td><%= question.title %></td>
<td><%= sanitize question.content, tags: %w(h1 h2 h3 h4 h5 h6 ul ol li p a img table tr td em br strong div), attributes: %w(id class href) %></td>
<td><%= link_to 'Show', question %></td>
<td><%= link_to 'Edit', edit_question_path(question) %></td>
<td><%= link_to 'Destroy', question, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Question', new_question_path %>
<p id="notice"><%= notice %></p>
<p>
<strong>Title:</strong>
<%= @question.title %>
</p>
<p>
<strong>Content:</strong>
<%= sanitize @question.content, tags: %w(h1 h2 h3 h4 h5 h6 ul ol li p a img table tr td em br strong div), attributes: %w(id class href) %>
</p>
<h2>Comments</h2>
<div id="comments">
<%= render @question.comments %>
</div>
<%= render 'comments/new', question: @question %>
<%= link_to 'Edit', edit_question_path(@question) %> |
<%= link_to 'Back', questions_path %>
<div data-controller="comment">
<div class="container">
<div data-target="comment.preview"></div>
</div>
<%= form_with(model: [ @question, @comment ], remote: true) do |form| %>
Your Comment:<br>
<%= form.hidden_field :content, id: :comment_content %>
<trix-editor input="comment_content" data-target="comment.content", data-action="input->comment#content"></trix-editor>
<%= form.submit %>
<% end %>
</div>
<div data-controller="comment">
<div class="container">
<div data-target="comment.preview"></div>
</div>
<%= form_with(model: [ @question, Comment.new ], remote: true) do |form| %>
Your Comment:<br>
<%= form.hidden_field :content, id: :comment_content %>
<trix-editor input="comment_content" data-target="comment.content", data-action="input->comment#content"></trix-editor>
<%= form.submit %>
<% end %>
</div>
<p><%= sanitize comment.content, tags: %w(h1 h2 h3 h4 h5 h6 ul ol li p a img table tr td em br strong div), attributes: %w(id class href) %></p>
<p><%= link_to "Delete", [@question, comment], method: :delete, data: { confirm: 'Are you sure?' } %>
<p><%= link_to "Edit", edit_question_comment_path(@question, comment) %></p>
これで、リッチなテキストエディタが使用できるようになります。
このままではデザインなどが簡素すぎるのでBootstrapを使いたいと思います。
まず、Gemfileにgem 'bootstrap', '~> 4.2.1'とgem 'jquery-rails'を追加し、bundle installします。
gem 'bootstrap', '~> 4.2.1'
gem 'jquery-rails'
bundle install
もし、gemの依存関係でうまくいかない場合は、bundle updateを実行してからbundle installを実行してください
bundle update
bundle install
その後、app/assets/javascripts/application.jsとapp/assets/stylesheets/application.scssを下記のように変更します
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require rails-ujs
//= require activestorage
//= require trix
//= require jquery3
//= require popper
//= require bootstrap-sprockets
//= require turbolinks
//= require_tree .
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/
@import "trix";
@import "bootstrap";
その後、config/boot.rbを以下のように修正します
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
ENV['EXECJS_RUNTIME'] = 'Node'
require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
これでBootstrapが使用できるようになりました。
最後に、app/views/layouts/_header.html.erbを作成し、app/views/layouts/application.html.erbでパーシャルとして呼び出します。
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<%= link_to "QA Web", root_path, class: "navbar-brand" %>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Menu
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<%= link_to 'Questoions', questions_path, class: "dropdown-item" %>
</div>
</div>
</nav>
<!DOCTYPE html>
<html>
<head>
<title>Qa</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<%= render 'layouts/header'%>
<div class="container">
<%= yield %>
</div>
</body>
</html>
foreman start -f Procfile.devでサーバを起動し、ナビゲーションバーが表示されていればOKです。
質問サイトなどでは、使用するフレームワークや言語などのカテゴリを登録して質問しています 今回のチュートリアルでもカテゴリを使用できるようにしたいと思います
まずはrails g scaffoldでカテゴリのひな型を作成します
rails g scaffold category name:string
マイグレーションも実行します
rails db:migrate
マイグレーション後、app/views/layouts/_header.html.erbにリンクを追加します
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<%= link_to "QA Web", root_path, class: "navbar-brand" %>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Menu
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<%= link_to 'Questoions', questions_path, class: "dropdown-item" %>
<%= link_to 'Categories', categories_path, class: "dropdown-item" %>
</div>
</div>
</nav>
これで、localhost:5000/categoriesで新しいカテゴリなどを作成できます!
せっかくカテゴリ作れるようにしたので、質問投稿時にカテゴリを指定できるようにしたいですよね? というわけで、カテゴリを使用できるように実装します
まず、rails g migrationを実行し、Questionモデルにcategoryカラムを追加します
rails g migrationコマンドを使用することで作成済みのモデルに新しくカラムを追加したり、既にあるカラムを削除することもできます
rails g migration AddCategoryToQuestion category:string
マイグレーションも実行しておきます
rails db:migrate
最後にapp/controllers/questions_controller.rb、app/views/questions/_form.html.erb、
app/views/questions/index.html.erb、app/views/questions/show.html.erbを以下のように編集します
app/controllers/questions_controller.rbでは新しいbefore_actionにてカテゴリ一覧を@categoriesというインスタンス変数にセットしています
また、question_paramsのpermitに:categoryを追加しています
app/views/questions/_form.html.erbではフォームの項目にCategoryを追加し、選択できるようにしています
app/views/questions/index.html.erb、app/views/questions/show.html.erbでは入力されたカテゴリを表示できるようにしています
class QuestionsController < ApplicationController
before_action :set_question, only: [:show, :edit, :update, :destroy]
before_action :set_category, only: [:new, :edit]
# GET /questions
# GET /questions.json
def index
@questions = Question.all
end
# GET /questions/1
# GET /questions/1.json
def show
end
# GET /questions/new
def new
@question = Question.new
end
# GET /questions/1/edit
def edit
end
# POST /questions
# POST /questions.json
def create
@question = Question.new(question_params)
respond_to do |format|
if @question.save
format.html { redirect_to @question, notice: 'Question was successfully created.' }
format.json { render :show, status: :created, location: @question }
else
format.html { render :new }
format.json { render json: @question.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /questions/1
# PATCH/PUT /questions/1.json
def update
respond_to do |format|
if @question.update(question_params)
format.html { redirect_to @question, notice: 'Question was successfully updated.' }
format.json { render :show, status: :ok, location: @question }
else
format.html { render :edit }
format.json { render json: @question.errors, status: :unprocessable_entity }
end
end
end
# DELETE /questions/1
# DELETE /questions/1.json
def destroy
@question.destroy
respond_to do |format|
format.html { redirect_to questions_url, notice: 'Question was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_question
@question = Question.find(params[:id])
end
def set_category
@categories = Category.all
end
# Never trust parameters from the scary internet, only allow the white list through.
def question_params
params.require(:question).permit(:title, :content, :category)
end
end
<div data-controller="question">
<div class="form-left">
<%= form_with(model: question, local: true) do |form| %>
<% if question.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(question.errors.count, "error") %> prohibited this question from being saved:</h2>
<ul>
<% question.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :category %>
<%= form.collection_select(:category, @categories, :name, :name, include_blank: true) %>
</div>
<div class="field">
<%= form.label :content %>
<%= form.hidden_field :content, id: :content_text %>
<trix-editor input="content_text" data-target="question.content", data-action="input->question#content"></trix-editor>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
</div>
<div class="form-right">
<h1>Content</h1>
<div data-target="question.preview"></div>
</div>
</div>
<p id="notice"><%= notice %></p>
<h1>Questions</h1>
<table>
<thead>
<tr>
<th>Title</th>
<th>Category</th>
<th>Content</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @questions.each do |question| %>
<tr>
<td><%= question.title %></td>
<td><%= question.category %></td>
<td><%= sanitize question.content, tags: %w(h1 h2 h3 h4 h5 h6 ul ol li p a img table tr td em br strong div), attributes: %w(id class href) %></td>
<td><%= link_to 'Show', question %></td>
<td><%= link_to 'Edit', edit_question_path(question) %></td>
<td><%= link_to 'Destroy', question, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Question', new_question_path %>
<p id="notice"><%= notice %></p>
<p>
<strong>Title:</strong>
<%= @question.title %>
</p>
<p>
<strong>Category:</strong>
<%= @question.category %>
</p>
<p>
<strong>Content:</strong>
<%= sanitize @question.content, tags: %w(h1 h2 h3 h4 h5 h6 ul ol li p a img table tr td em br strong div), attributes: %w(id class href) %>
</p>
<h2>Comments</h2>
<div id="comments">
<%= render @question.comments %>
</div>
<%= render 'comments/new', question: @question %>
<%= link_to 'Edit', edit_question_path(@question) %> |
<%= link_to 'Back', questions_path %>
select2-railsを使い、プルダウン内でカテゴリ検索ができるようにします!
Gemfileにgem 'select2-rails'と追加し、bundle installを実行します
gem 'select2-rails'
bundle install
次に、app/assets/javascripts/application.jsとapp/assets/stylesheets/application.scssを修正します
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require rails-ujs
//= require activestorage
//= require trix
//= require jquery3
//= require popper
//= require bootstrap-sprockets
//= require select2
//= require turbolinks
//= require_tree .
$(document).ready(function() {
$('select#question_category').select2();
});
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/
@import "trix";
@import "bootstrap";
@import "select2";
@import "select2-bootstrap";
これで、localhost:5000/questions/newでカテゴリを登録する際にプルダウン検索が使用できるようになりました!
RailsにはDeviseというログイン機能などを簡単に実装できるgemがあります
今回はDeviseを使用してログイン機能を実装します
まず、Gemfileにgem deviseを追加します
gem 'devise'
Gemfileに追加後、bundle installを実行します
bundle install
bundle install実行後、Deviseのインストールを行います
rails g devise:install
rails g devise:views
rails g devise users
上記のコマンドを簡単に説明すると
rails g devise:installにてDeviseのインストールを実行していますrails g devise:viewsにてDeviseで使用するViewを作成していますrails g devise usersにてDeviseで使用するModelを作成していますUserモデルを作成しましたので、rails db:migrateを実行します
rails db:migrate
次に、ナビゲーションバーにログイン画面などへのリンクを追加します
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<%= link_to "QA Web", root_path, class: "navbar-brand" %>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Menu
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<%= link_to 'Questoions', questions_path, class: "dropdown-item" %>
<%= link_to 'Categories', categories_path, class: "dropdown-item" %>
<% if user_signed_in? %>
<%= link_to 'Sign Out', destroy_user_session_path, method: :delete, class: "dropdown-item" %>
<% else %>
<%= link_to 'Sign Up', new_user_registration_path, class: "dropdown-item" %>
<%= link_to 'Sign In', new_user_session_path, class: "dropdown-item" %>
<% end %>
</div>
</div>
</nav>
user_signed_in?はDeviseを導入したことでしようできるヘルパーメソッドになります
これだけでユーザーがログインしているかどうかをチェックできます
<% if user_signed_in? %>ではユーザーがログインしている状態に表示する内容を記述しています。
<% else %>以降はログインしていない場合に表示する内容になります
これでログイン画面へのリンクなどが作成できました
次に、QuestionやCommentとUserと紐づけて、QuestionやComentを投稿した本人しか削除できないようにします
rails g migrationコマンドを使用して新しいカラムをQuestionとCommentに追加します
rails g migration AddUserToQuestion user:references
rails g migration AddUserToComment user:references
モデルに新しいカラムを追加したので、rails db:migrateでマイグレーションを実行します
rails db:migrate
そして、app/controllers/questions_controller.rb、app/controllers/comments_controller.rb、app/controllers/categories_controller.rbを下記のように変更します
app/controllers/questions_controller.rb、app/controllers/comments_controller.rbのcreateアクションで現在ログインしているユーザーのIDを保存するようにしています
また、before_actionでcheck_loginを呼び出してログイン状態と投稿者本人かどうかを判定しています
ログインしていない又は投稿者本人でない場合は、localhost:5000/questions/1/editなどにアクセスしてもlocalhost:5000へとリダイレクトされます
class QuestionsController < ApplicationController
before_action :set_question, only: [:show, :edit, :update, :destroy]
before_action :set_category, only: [:new, :edit]
before_action :check_login, only: [:new, :edit, :create, :update, :destroy]
# GET /questions
# GET /questions.json
def index
@questions = Question.all
end
# GET /questions/1
# GET /questions/1.json
def show
end
# GET /questions/new
def new
@question = Question.new
end
# GET /questions/1/edit
def edit
end
# POST /questions
# POST /questions.json
def create
@question = Question.new(question_params)
@question.user_id = current_user.id
respond_to do |format|
if @question.save
format.html { redirect_to @question, notice: 'Question was successfully created.' }
format.json { render :show, status: :created, location: @question }
else
format.html { render :new }
format.json { render json: @question.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /questions/1
# PATCH/PUT /questions/1.json
def update
respond_to do |format|
if @question.update(question_params)
format.html { redirect_to @question, notice: 'Question was successfully updated.' }
format.json { render :show, status: :ok, location: @question }
else
format.html { render :edit }
format.json { render json: @question.errors, status: :unprocessable_entity }
end
end
end
# DELETE /questions/1
# DELETE /questions/1.json
def destroy
@question.destroy
respond_to do |format|
format.html { redirect_to questions_url, notice: 'Question was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_question
@question = Question.find(params[:id])
end
def set_category
@categories = Category.all
end
def check_login
redirect_to :root if current_user == nil
end
# Never trust parameters from the scary internet, only allow the white list through.
def question_params
params.require(:question).permit(:title, :content, :category)
end
end
class CommentsController < ApplicationController
before_action :set_question
before_action :set_comment, only: [:edit, :update]
before_action :check_login, only: [:edit, :create, :update, :destroy]
def edit
end
def create
@question.comments.create! comments_params
@question.comments.update(:user_id => current_user.id)
redirect_to @question
end
def update
@comment.update(comments_params)
redirect_to @question
end
def destroy
@question.comments.destroy params[:id]
redirect_to @question
end
private
def set_question
@question = Question.find(params[:question_id])
end
def set_comment
@comment = Comment.find(params[:id])
end
def check_login
redirect_to :root if current_user == nil
end
def comments_params
params.required(:comment).permit(:content)
end
end
class CategoriesController < ApplicationController
before_action :set_category, only: [:show, :edit, :update, :destroy]
before_action :check_login, only: [:new, :edit, :create, :update, :destroy]
# GET /categories
# GET /categories.json
def index
@categories = Category.all
end
# GET /categories/1
# GET /categories/1.json
def show
end
# GET /categories/new
def new
@category = Category.new
end
# GET /categories/1/edit
def edit
end
# POST /categories
# POST /categories.json
def create
@category = Category.new(category_params)
respond_to do |format|
if @category.save
format.html { redirect_to @category, notice: 'Category was successfully created.' }
format.json { render :show, status: :created, location: @category }
else
format.html { render :new }
format.json { render json: @category.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /categories/1
# PATCH/PUT /categories/1.json
def update
respond_to do |format|
if @category.update(category_params)
format.html { redirect_to @category, notice: 'Category was successfully updated.' }
format.json { render :show, status: :ok, location: @category }
else
format.html { render :edit }
format.json { render json: @category.errors, status: :unprocessable_entity }
end
end
end
# DELETE /categories/1
# DELETE /categories/1.json
def destroy
@category.destroy
respond_to do |format|
format.html { redirect_to categories_url, notice: 'Category was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_category
@category = Category.find(params[:id])
end
def check_login
redirect_to :root if current_user == nil
end
# Never trust parameters from the scary internet, only allow the white list through.
def category_params
params.require(:category).permit(:name)
end
end
あとは、app/views/questions/index.html.erb、app/views/questions/show.html.erb、
app/views/comments/_comment.hmtl.erb、app/views/categories/index.html.erb、app/views/categories/show.html.erbを以下のように変更することで実装は完了です!
Deviseのヘルパーメソッドのuser_signed_in?を使うことでログインしているかを判定しています
また@question.user_id == current_user.idとすることで投稿した本人かどうかを判定しています
<p id="notice"><%= notice %></p>
<h1>Questions</h1>
<table>
<thead>
<tr>
<th>Title</th>
<th>Category</th>
<th>Content</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @questions.each do |question| %>
<tr>
<td><%= question.title %></td>
<td><%= question.category %></td>
<td><%= sanitize question.content, tags: %w(h1 h2 h3 h4 h5 h6 ul ol li p a img table tr td em br strong div), attributes: %w(id class href) %></td>
<td><%= link_to 'Show', question %></td>
<% if user_signed_in? && question.user_id == current_user.id %>
<td><%= link_to 'Edit', edit_question_path(question) %></td>
<td><%= link_to 'Destroy', question, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
<br>
<% if user_signed_in? %>
<%= link_to 'New Question', new_question_path %>
<% end %>
<p id="notice"><%= notice %></p>
<p>
<strong>Title:</strong>
<%= @question.title %>
</p>
<p>
<strong>Category:</strong>
<%= @question.category %>
</p>
<p>
<strong>Content:</strong>
<%= sanitize @question.content, tags: %w(h1 h2 h3 h4 h5 h6 ul ol li p a img table tr td em br strong div), attributes: %w(id class href) %>
</p>
<h2>Comments</h2>
<div id="comments">
<%= render @question.comments %>
</div>
<% if user_signed_in? %>
<%= render 'comments/new', question: @question %>
<% end %>
<% if user_signed_in? && @question.user_id == current_user.id %>
<%= link_to 'Edit', edit_question_path(@question) %> |
<% end %>
<%= link_to 'Back', questions_path %>
<p><%= sanitize comment.content, tags: %w(h1 h2 h3 h4 h5 h6 ul ol li p a img table tr td em br strong div), attributes: %w(id class href) %></p>
<% if user_signed_in? && comment.user_id == current_user.id %>
<p><%= link_to "Delete", [@question, comment], method: :delete, data: { confirm: 'Are you sure?' } %>
<p><%= link_to "Edit", edit_question_comment_path(@question, comment) %></p>
<% end %>
<p id="notice"><%= notice %></p>
<h1>Categories</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @categories.each do |category| %>
<tr>
<td><%= category.name %></td>
<td><%= link_to 'Show', category %></td>
<% if user_signed_in? %>
<td><%= link_to 'Edit', edit_category_path(category) %></td>
<td><%= link_to 'Destroy', category, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
<br>
<% if user_signed_in? %>
<%= link_to 'New Category', new_category_path %>
<% end %>
<p id="notice"><%= notice %></p>
<p>
<strong>Name:</strong>
<%= @category.name %>
</p>
<% if user_signed_in? %>
<%= link_to 'Edit', edit_category_path(@category) %> |
<% end %>
<%= link_to 'Back', categories_path %>
これでQ&Aサイトは完成です!