20200314のRailsに関する記事は22件です。

Rails + Vue.js + AjaxでCRUDのサンプルプロジェクト [Hello World]

Vue.js初心者が公式サイトで基礎を学んだ後に作るVue.js + AjaxによるCRUD(作成/読み込み/更新/削除)のサンプルプロジェクトです。

vue_crud_1.png

動作確認はChrome、FireFox、Microsoft Edge、IE11です。恐らくマックさんのブラウザでも動作するはずです。

DEMO

https://www.petitmonte.com/rails-demo/vue_crud

ソース一式

https://github.com/TakeshiOkamoto/mpp_vue_crud

※学習用の為、ライセンスはパブリックドメイン

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails~~投稿とユーザーを紐付けする

テーブルに新しくカラムを追加する

まずターミナルからマイグレーションファイルを作成。データベースへ反映させる。

ターミナル

rails g migration add_user_id_to_posts
※マイグレーションを変更
rails db:migrate

マイグレーションファイルの中身

class AddUserIdToPosts
  def change
    add_column :posts, :user_id, :integer
  end
end

バリデーションの設定

class Post<ApplicationRecord

 validates :user_id,{presence: true}
end

投稿したユーザーのidを保存しよう

controller

def create
   @post=Post.new(
       content: params[:content],
       user_id: @current_user.id
   )
end

新規投稿後、user_idに投稿したユーザーの値を加える

user_idからユーザー情報を取得する

ユーザー名やユーザー画像を表示するためには、user_idカラムの値からそのユーザーの情報を取得する必要がある。投稿詳細ページにて反映させたいため、postsコントローラのshowアクション内で@post.user_idを用いて、そのidに該当するユーザーの情報をデータベースから取得する。

controller

def show
  @post = Post.find_by(id:params[:id])
  @user = User.find_by(id: @post.user_id)
end

@userから始まる処理は@post.user_idの値からユーザー情報を取得するという意味となる

view

<img src="<%="/user_images/#{@user.image_name}"%>">

<%=link_to(@user.name,"/users#{@user.id}")%>

Postモデルにuserメソッドを定義する

models

class Post<ApplicationRecord
  def user
    return User.find_by(id:self.user_id)
  end
end

ユーザー詳細ページに投稿を表示する

find_byメソッドはその条件に合致するデータを1件だけ取得することができる。今回は複数の情報を取得しなければならない。複数の情報を取得するためには「where」メソッドを使用する

rails console
posts = Post.where(user_id:1)
=>[...]

consoleでこのコードを取得することでuser_idの値が「1」である投稿をすべて取得する。

Userモデルにpostsメソッドを定義

models

class User<ApplicationRecord
  def posts
     return Post.where(user_id: self.id)
  end
end

投稿の表示

@user.postsを用いて、各投稿にそれぞれ表示を行う。
whereメソッドで取得した値は配列に入っているので、view側でeach文を用いて1つずつ投稿を表示する

view

<% @user.posts.each do |post|%>

<%end%>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでフォーム作成時にHTMLで書いたコードをform_tagに変換する方法

Ruby on Railsでヘルパーメソッドを使って、HTMLのフォームをform_tagに書き換える方法を説明します。

ヘルパーメソッドとは

ある動作を処理する場合にメソッド化して扱えるようにRailsにあらかじめ組み込まれた機能のこと。使うことでビューをシンプルに美しく書くことができるといったメリットがあリます。

form_tagメソッドとは

フォームを実装するためのヘルパーメソッドです。

今回は以下のコードを書き換えます。

sample.html.erb
<form action="/posts" method="post">
  <input type="text" name="content">
  <input type="submit" value="投稿する">
</form>

これをform_tagに書き換えると

sample.html.erb
<%= form_tag('/posts', method: :post) do %> 
  <input type="text" name="content"> 
  <input type="submit" value="投稿する"> 
<% end %>

となります。変わっているのは以下の部分です。

sample.html.erb
HTML:
<form action="/posts" method="post">
</form>
form_tag:
<%= form_tag('/posts', method: :post) do %> 
<% end %>

form_tagのdo〜endは〜の部分がフォームであることを示しています。
記載を変換する際は、<%= %>(erbタグ)doを忘れないように気をつけましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC2にapache + rails環境を構築する

参考サイト

es2の設定
(デプロイ編①)世界一丁寧なAWS解説。EC2を利用して、RailsアプリをAWSにあげるまで
Linuxグループ作成方法!groupaddで意外とすぐできる
usermodコマンドについて詳しくまとめました 【Linuxコマンド集】

apacheインストール
Red Hat

passengerインストール
phusion/passenger - Github

mysqlインストール
CentOS7.3 に MySQL5.7 をインストールした時のメモ - Qiita

apache,rbenvインストール参考サイト
EC2にRails5環境を構築する - Qiita

getの設定
Git
GitHubでssh接続する手順~公開鍵・秘密鍵の生成から~ - Qiita

WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!エラー
SSH接続エラー回避方法:.ssh/known_hostsから特定のホストを削除する/削除しないで対処する3つの方法 - Qiita

バージョン

アプリケーション version
AMI Amazon Linux 2
Ruby 2.5.3
Rails 5.2.3
Apache 2.4.41
MySQL 5.7
Passenger 6.0.4

インスタンス設定

・ユーザの作成

$ sudo adduser <ユーザ名>
$ sudo passwd <ユーザ名>
$ sudo visudo
##vim~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
root     ALL=(ALL) ALL # の下に追記
<ユーザ名> ALL=(ALL) NOPASSWD:ALL
##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ su - <ユーザ名>

・グループの作成、グループにユーザ追加

$ sudo groupadd <グループ名>
$ tail /etc/group
$ sudo usermod -g <グループ名> <ユーザ名>
$ id <ユーザ名>

・タイムゾーンを日本時間に設定

$ date
$ sudo cp -r /usr/share/zoneinfo/Japan /etc/localtime
$ sudo vim /etc/sysconfig/clock
ZONE="Asia/Tokyo"
UTC=true
$ date

apacheインストール

・apacheインストール

$ sudo yum -y update
$ sudo yum -y install httpd
$ sudo systemctl start httpd
$ sudo systemctl status httpd
$ sudo systemctl enable httpd.service
$ httpd -version

mysqlインストール、設定

・mysqlインストール

$ sudo yum -y install mysql-devel
$ sudo yum -y localinstall https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm
$ sudo yum info mysql-community-server
$ sudo yum -y install mysql-community-server
$ mysqld --version

・mysqlの設定

$ sudo systemctl start mysqld
$ sudo systemctl status mysqld
$ sudo systemctl enable mysqld
$ sudo cat /var/log/mysqld.log | grep password
$ sudo mysql_secure_installation
$ mysql -u root -p

# ポリシー変更してパスワードの変更
mysql> SHOW VARIABLES LIKE 'validate_password%';
mysql> set global validate_password_length=6;
mysql> set global validate_password_policy=LOW;
mysql> use mysql
mysql> update user set authentication_string=password('パスワード') where user='root'; 
mysql> flush privileges;

Gitのインストール、設定

gitのインストール

$ sudo yum -y install git
$ git --version

$ cd
$ mkdir .ssh
$ chmod 700 .ssh
$ ll -al
$ cd .ssh
$ ssh-keygen
$ cat <キー名>.pub

# 公開鍵をgithubに登録

# キーを命名した場合↓
$ sudo vi config
##vim~~~~~~~~~~~~~~~~~~~~~~~~
Host github github.com
  HostName github.com
  IdentityFile ~/.ssh/<キー名>
  User git
##~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ ssh -T git@github.com

rbenvインストール手順

・rbenv install

$ sudo yum install -y git gcc gcc-c++ openssl-devel readline-devel
$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ cat ~/.bash_profile
$ source ~/.bash_profile
$ rbenv -v
$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
$ rbenv install -l

Railsアプリをインスタンス上に配置、立ち上げまで

$ sudo mkdir /sample
$ chown <ユーザ名>:<グループ名> /sample/
$ cd /sample
$ mkdir rails
$ cd rails
$ git clone <クローンするアプリ>

・Rubyのバージョン確認

$ cd <アプリのパス>
$ cat .ruby-version

・rubyのインストール

$ rbenv install -v 2.5.3
$ rbenv rehash
$ rbenv global 2.5.3
$ ruby -v

・bundlerのインストール

$ cat Gemfile.lock
##cat~~~~~~~~~~~~~~~~~
BUNDLED WITH
   2.0.2
##~~~~~~~~~~~~~~~~~~~~

$ gem install bundler -v 2.0.2

・ railsアプリ立ち上げ

$ bundle install --path vendor/bundle
$ bundle exec rails db:create
$ bundle exec rails db:migrate
$ bundle exec rails s -d
$ ps aux | grep puma
$ curl http://localhost:3000
$ $ kill -9 <pumaのプロセスid>

Passengerのインストール

・passengerのインストール

$ gem install passenger
$ passenger-install-apache2-module

apache設定

$ sudo vim /etc/httpd/conf.d/sample.conf

##vim~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<VirtualHost *:80>
  ServerName <ip または ドメイン>
  DocumentRoot /sample/rails/<アプリ名>/public
  RackEnv production
  <Directory /sample/rails/<アプリ名>/public>
    AllowOverride all
    Require all granted
  </Directory>
</VirtualHost>

# passengerのモジュールインストール時に出でくるものを貼る
LoadModule passenger_module /home/*****/.rbenv/versions/2.5.3/lib/ruby/gems/2.5.0/gems/passenger-6.0.4/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
  PassengerRoot /home/*****/.rbenv/versions/2.5.3/lib/ruby/gems/2.5.0/gems/passenger-6.0.4
  PassengerDefaultRuby /home/*****/.rbenv/versions/2.5.3/bin/ruby
</IfModule>

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

$ sudo systemctl restart httpd

でブラウザーからアクセスするとアプリが立ち上がっていると思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

マイクロポストにコメント機能をつける

はじめに

ご訪問いただきありがとうございます。初めての投稿ですが学んだことを備忘録的にまとめようと思い記事にしてみました。レベル的にはrailsチュートリアルを終えたくらいのレベルです。もし間違えているところ等あればアドバイスいただけると幸いです。

作るもの

今回は投稿されたマイクロポストに自由にコメントできるような機能を付けます。(題材は自分のポートフォリオサイトです。尚、すでにUserテーブルとMicropostテーブルは作成済みです。)

対象読者

railsチュートリアルに機能を追加したい等自分と同じ位のレベルの人を対象としています。

作成の流れ

まず初めにコメント機能を追加するまでの流れです。
1.Commentモデルの作成、変更
2.モデルの関連付け、バリデーションの設定
3.Commentsコントローラーの作成、ルーディングの設定
4.対応するビューの作成
5.コントローラのアクションの作成

1.Commentsモデルの作成

まずデータベースとやりとりを行う、Commentモデルの作成を行います。
ちなみにテーブルの中身はこんな形です。

カラム
id integer
user_id integer
micropost_id integer
content text

誰がコメントしたかわかるようにuser_idはUserモデルと関連付けを行い、同じくmicropost_idもどのマイクロポストにコメントされたかわかるようにMicropostモデルと関連付けをこないます。
それではモデルを作成します。

rails g model comment user:references micropost:references content:text
db/migrate/_create_comments.rb
class CreateComments < ActiveRecord::Migration[5.1]
  def change
    create_table :comments do |t|
      t.references :user, foreign_key: true
      t.references :micropost, foreign_key: true
      t.text :content, null: false

      t.timestamps
    end
  end
end

モデルを作成する際にreferencesとすることでindexと外部キー制約(foregin_key: true)が自動で追加されます。indexをuser_idとmicropost_idに追加することによってそれぞれに関連したコメントを探す際にデータを高速に調べられるようになります。また、外部キー制約がつくことによってuser_id(micropost_id)にはUserテーブルに存在するidのみデータベースレベルで保存するようになります。
また、コメントが空だとコメント機能の意味をなさないためnull:falseを追加します。
それではテーブルを作成します。

rails:db:migrate

2.モデルの関連付け、バリデーションの設定

作成されたモデルはこんな感じです。

app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :micropost
  validates :content, presence: true, length: { maximum: 140 }
end

自動でbelongs_toで一対一の関連付けができています。User,Micropostモデルにはそれぞれ手動で追加する必要があるので追加していきます。また、バリテーションも追加しています。テーブル作成の時点でcontentにはnull:falseで空で保存させないようにしていますが空文字("")は保存できます。なのでpresence:trueを追加することによって空文字も拒否するようになります。また、バリデーションに基づいたエラーメッセージも保存されます。文字数制限に関してTwitterと同じく140文字としています。

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :comments, dependent: :destroy
app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy

UserもMicropostも多数のコメントを持てるためhas_manyで1対多の関連付けを行います。dependent: :destroyでUser、Micropostが消えた際に関連するコメントも消えるようにしています。

3Comenntsコントローラーの作成、ルーディングの設定

続いてコントローラーを作成します。今回はcreateとdestroyアクションのみ使用します。

rails g controller comments
app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create 
  end

  def destroy
  end
end

ルーディングの設定。

config/routes.rb
resources :microposts, only: [:new, :show, :create, :destroy] do
   resources :comments, only: [:create, :destroy]
end

commetsはmicropostsとネストして親子の関係を持たせます。こうすることによってコメントを作成する際に関連しているmicropostのidを取得することが容易になります。ネスト有無の違いはこんな感じです。commentsがmicropstsの下の階層についているのがわかります。

ネスト有

micropost_comments POST   /microposts/:micropost_id/comments(.:format) comments#create
micropost_comment DELETE /microposts/:micropost_id/comments/:id(.:format) comments#destroy

ネスト無し

comments POST   /comments(.:format) comments#create
comment DELETE /comments/:id(.:format) comments#destroy

4対応するビューの作成

今回はMicropostの詳細ページからコメントするような形をとります。
コメントを表示するのはMicroposts/:idになるのでまずはコントローラーにコメント表示とコメントフォーム用にインスタンス変数を作成しておきます。

app/controllers/microposts_controller.rb
def show

    @comment =Comment.new
    @micropost = Micropost.find(params[:id])
    @comments =micorpost.comments#適時ページネーション等利用してください
end

コメント表示(投稿者と中身のみ)

app/views/microposts/show.html.erb
<% @comments.each do |comment| %>
<%= comment.user.name %>
<%= comment.content %>
<% end %>

コメントフォーム

app/views/microposts/show.html.erb
<%= form_with model: [@micropost, @comment], local: true do |f| %>
  <%= render 'shared/error_messages', object: f.object %>

    <div class = 'form-group'>
      <%= f.text_area :content, class: 'form-control', id:'content'\
         placeholder: "コメントを記入してください" %>
    </div>

    <%= f.submit "コメントする", class: "btn btn-primary" %>
<% end %>

コメントはいずれかのマイクロポストと関連づいているため、どのマイクロポストのコメントなのかという情報が必要になります。
そのため、form_withにmicropostのidも渡します。@comment=Comment.newで新規に値を作成しているので作成ボタンを押すと自動的にcreateアクションが動きます。

5.コントローラのアクションの作成

次はコメントを作成するcreateアクションを作っていきます。

app/contorollers/comments_contoroller.rb
class CommentsController < ApplicationController
  before_action :logged_in_user, only: :create

  def create 
    @comment = current_user.comments.build(comment_params)
    @comment.micropost_id = params[:micropost_id]
    if @comment.save
      flash[:success] = 'コメントしました'
      redirect_to @comment.micropost
    else
      @micropost = Micropost.find(params[:micropost_id])  
      @comments = @micropost.comments
      render template: 'microposts/show'
    end

 private

  def comment_params
    params.require(:comment).permit(:content)
  end
end

createアクション失敗後に再度同じページを表示するため、コメントを取得してます。(このやり方があっているかはわかりません。)コメントフォーム専用のページを設ければこの取得はいらないです。

コメントを作成する際に、ユーザーのidを渡す必要がありますが、current_user.comments.buildとすることによりログイン中ユーザーのidを入れ込みます。このままだとマイクロポストのidがなくコメントを作成できないのでform_withから送られてくるmicropost_idを取得しています。
(ストロングパラメータでmicoropst_idを取得する方法がわかりませんでした。)

これでコメントフォームからコメントをすることができるようになりました。
続いてコメント削除です。
まずはviewsに削除リンクを埋め込みます。

app/views/microposts/show.html.erb
<% @comments.each do |comment| %>
<%= comment.user.name %>
<%= comment.content %>
<%= link_to '削除', micropost_comment_path(@micropost, comment), method: :delete %>
<% end %>

destroyアクションを動かすためmethod: :deleteを指定します。
pathにはコメントがいずれかのマイクロポストに紐づいている関係上(@micropost,comment)を渡す必要があります。

micropost_comment DELETE /microposts/:micropost_id/comments/:id(.:format) comments#destroy
app/contorollers/comments_contoroller.rb
class CommentsController < ApplicationController
  before_action :correct_user,   only: :destroy
  def destroy
    @comment = Comment.find(params[:id])
    @comment.destroy
    flash[:success] = 'コメントを削除しました'
    redirect_to @comment.micropost
  end

 private
  def correct_user
    @comment = current_user.comments.find_by(id: params[:id])
    redirect_to root_url if @comment.nil?
  end
end

終わりに

初めての記事作成で至らぬ点ばかりだったと思います。
何か間違いや、認識違い等があればアドバイスいただければ幸いです。
それではここまで読んんでいただいきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Mailer viewでヘルパーメソッドを使用する

Mailer viewでヘルパーを使いたい

郵便番号にハイフンをいれてくれるメソッド

application_helper.rb
def format_zipcode_include_hyphen(zipcode)
    zipcode.include?('-') ? zipcode : zipcode.insert(3,'-')
end

mailer viewでいきなり使用するとエラーになった。

application_mailer.rb

application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  add_template_helper(ApplicationHelper)
end

と記述する

view

mailer.ftml.slim
p #{format_zipcode_include_hyphen(zipcode)}

でうまくいきました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsにおけるセッション管理とCookieについて

セッション管理とCookieの働きについてあやふやだったのでまとめてみました

HTTP通信

  • クライアントとサーバ間はHTTPというルールでやり取りをする
  • クライアントからサーバに対して「リクエスト」が送られる
    • リクエスト = 「要求」(このURLの情報ちょうだい、とか)
    • レスポンス = 「返答」(情報はこれだよ、とか) スクリーンショット 2020-03-14 14.52.48.png

HTTPはステートレス

  • ステートレス=記憶力がない
  • たとえばyahooにアクセスするときには、クライアントからサーバへ「yahooの情報ちょうだい」とリクエストを送るが、その直後には送ったことすら覚えていない
  • yahooの情報がレスポンスで送られてきて画面表示をしたとしても、送られてきたこと自体を覚えていない
  • 要求しっぱなし。もらいっぱなし。 スクリーンショット 2020-03-14 17.19.39.png

ステートレスだと、Webアプリでログインする時にどんな問題が発生するか

  • ログインできても、ログインしたことすら覚えていない
  • そのため、次にアクセスした時には、またログインしなければならない
  • ショッピングサイトの場合、カートに入れた商品情報が保持されない

そこでCookieの登場

  • ログインできた時点で、サーバでセッションIDをCookieに入れてクライアントへ送り返す

コード例:

session[:user_id] = user.id

上記の場合は、user_idを元にセッションIDを生成している

session[:email] = user.email

こんなこともできる

スクリーンショット 2020-03-14 15.18.07.png
- クライアントはCookieを受け取り、ブラウザに保存する
スクリーンショット 2020-03-14 15.18.46.png

  • 次にリクエストを送る時には、保存しているCookieをリクエストに含めて送る
    スクリーンショット 2020-03-14 15.20.15.png

  • リクエストで送られてきたCookieをサーバが受け取って、Cookieの中にセッションIDがあることを確認する
    スクリーンショット 2020-03-14 15.21.12.png

  • そのセッションIDから情報(user_idなど)を割り出し、そのidと合致するユーザをログイン状態として処理をする
    コード例:

@current_user ||= User.find_by(id: session[:user_id])
  • ログイン中は、セッションIDをCookieの中に含めたままクライアント〜サーバ間をグルグルと回っている状況
  • ログアウト時には、サーバにてCookieからセッションIDを消去する

コード例:

session.delete(:user_id)

とか

session[:user_id] = nil

など。

  • セッションIDが除かれたCookieが、サーバからクライアントに渡る(セッションIDが存在しない=ログアウト状態)
  • ログアウト中は、セッションIDが除かれたCookieがクライアント〜サーバ間をグルグルと回ることになる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

『非同期でのメッセージ投稿』が理解できる最低限のRailsアプリを丁寧に作る(Ajax苦手の自分とお別れしよう)

この記事の基本的な方針

Ajaxはなんだか難しい!は、勘違いです。一歩一歩きちんと進んでいけば、普通のことだと思えてくるでしょう。
ここではAjax非同期通信を理解するためだけの簡易なアプリを一つ丁寧に作成して、Ajax学習の基礎を完了することを目的としています。

この記事は、以下の「登録画面」「ログイン画面」「TOP画面」の3画面の簡単なアプリを元に拡張していきます
【TOP画面(ログイン前)】     【TOP画面(ログイン後)】
a0.png a9.png
【登録画面】
a1.png
【ログイン画面】
a2.png

手を動かしながら読みたいようでしたら、以下でこの3画面アプリを手に入れてください。

Terminal
$ git clone -b 超最低限のRailsアプリ(messageコントローラVer)  https://github.com/annaPanda8170/minimum_rails_application.git
$ bundle install
$ bundle exec rake db:create
$ bundle exec rake db:migrate

これ自体の作り方はこちら

想定する読み手

既に一度Railsアプリをチュートリアルやスクール等で作ったことがある方であり、JQueryの基本文法を理解している方を想定しております。
Mac使用で、パソコンの環境構築は完了していることが前提です。

取り急ぎ同期通信のメッセージ投稿機能をつくる

※本筋ではないので急ぎ足で説明します。詳しい説明が必要な方は、別記事をご覧ください(newアクションを使わず、formをindexに置くという違いがあります)。

① メッセージテーブルを作る

マイグレーションファイルモデルを作るため、

Terminal
$ rails g model message

を打ちます。
マイグレーションファイルに

db/migrate/2020xxxxxxxxxx_create_messages.rb
class CreateMessages < ActiveRecord::Migration[5.2]
  def change
    create_table :messages do |t|
      t.string :message
      t.references :user, foreign_key: true
      t.timestamps
    end
  end
end

となるように追記し、

Terminal
$ rails db:migrate

します。
これを済ませたら、メッセージテーブルは完成です。一応ちゃんと出来ているかデータベースを見に行ってみましょう。
mysql.png
私はSequelProを使っていてこんな感じです。

モデルにテーブル同士の関係を書きましょう。これが無くても投稿できなくはないのですが、投稿した内容を引き出して扱う上で便利なので今済ませてしまいましょう。以下を追記します。

app/models/message.rb
belongs_to :user
app/models/user.rb
has_many :messages

②メッセージだけを投稿できるようにする

ルーティング、ビュー、コントローラを編集します。今回はindexにフォームを置くのでnewアクションはなしです。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root 'messages#index'
  resources :messages, only: [:index, :create]
end
app/views/messages/index.html.erb
<% if user_signed_in? %>
  <%= current_user.email %>
  <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %>
  <%= form_with(model: @message, local: true, class: "form") do |f| %>
    <%= f.text_field :message %>
    <%= f.submit "投稿" %>
  <% end %>
<% else %>
  <%= link_to '新規登録', new_user_registration_path %>
  <%= link_to 'ログイン', new_user_session_path %>
<% end %>

この時点で、localhost:3000でもlocalhost:3000/messagesでも
log.png
が表示されるはずです。この時点ではこの投稿フォームただの飾りなので、データベースに保存できるよう中身を作ります。
コントローラは以下です。

app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  before_action :to_root, except: [:index]
  def index
    @message = Message.new
  end
  def create
    @message = Message.new(message_params)
    @message.save
    redirect_to root_path
  end
  private
  def message_params
    params.require(:message).permit(:message).merge(user_id: current_user.id)
  end
  def to_root
    redirect_to root_path unless user_signed_in?
  end
end

これで一度投稿してみましょう。

goodafternoon.png
問題なさそうですね。

③投稿を表示

あとはTOP画面に投稿されたものを全て表示させます。以下を追記します。

app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  〜省略〜
  def index
    @messages = Message.all
    @message = Message.new
  end
  〜省略〜
end
app/views/messages/new.html.erb
〜省略〜
<% @messages.each do |m| %>
  <div style="margin-top: 20px;"><span style="color: red;"><%= m.user.email %></span><%= m.message %></div>
<% end %>

hello.png
大丈夫ですね。

投稿するときに画像の上の丸い矢印が一瞬×になって投稿が反映されると思いますが、これなしに反映されるのが非同期通信です。

④JQueryを導入し、turbolinksを削除する

turbolinksを削除する理由は、JQueryの動きを阻害する可能性があるからです。(リロードすればjsが動作するのに、リンクで移動すると動作しない、等)

Gemfilegem 'jquery-rails'を加え、gem 'turbolinks'をコメントアウトするか消し、

gemfile
×  gem 'turbolinks'

   gem 'jquery-rails'

bundle installしサーバの再起動します。
app/assets/javascripts/application.js//= require jquery//= require jquery_ujs//= require_tree .より上に追記し、//= require turbolinksを消します。

app/assets/javascripts/application.js
× //= require turbolinks

  //= require jquery
  //= require jquery_ujs
  //= require_tree .

以下を修正します。

app/views/layouts/application.html.erb`
× <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
   <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>

       |
       v

◯ <%= stylesheet_link_tag    'application', media: 'all' %>
   <%= javascript_include_tag 'application' %>

続いてapp/assets/javascriptsmessages.jsを作ります。app/assets/javascripts/messages.coffeeがあると作成したファイルが機能しないので削除します。

app/assets/javascripts/messages.js
$(function () {
  console.log("OK")
});

を書いて、ブラウザをどの画面でもいいのでリロードします。
コンソールにOKが表示されたら成功です。

これで準備は終わりです。

いよいよ本筋、非同期実装

完成品GitHub(masterではなく一つのブランチなので注意して下さい)

①投稿ボタンを押すとイベント発火させる

以下のようにjsファイルを直しフォームの投稿ボタンが押されたときにコンソールにOkが出てくるか確認します。
function後の()にeをお忘れなく。

app/assets/javascripts/messages.js
$(function (e) {
  $(".form").on("submit", function () {
    console.log("Ok")
  })
});

一瞬だけ表示されてすぐに消えますね。投稿されたらTOP画面に(つまり同じ画面に)リダイレクトするのですから当然ですね。今はリダイレクトせずに投稿が反映されるようにするためにこの動きをjs内で止めます。以下に直してください。

app/assets/javascripts/messages.js
$(function () {
  $(".form").on("submit", function (e) {
    console.log("Ok")
    e.preventDefault();
  })
});

これでOkが残るようになりました。

②formの情報をjsで受け取り、createアクションに渡して投稿する

まずjsでformの情報を受け取る型は以下のような感じです。

app/assets/javascripts/messages.js
$(function () {
  $(".form").on("submit", function (e) {
    e.preventDefault();
    $.ajax({
      url:  (1) ,
      type: (2) ,
      data:  (3) ,  
      dataType: 'json',
    })
  })
});

加わったのは$.ajaxのくだりですね。
4つの項目がありますが、dataTypeはとりあえず'json'でいいです。jsonとはデータの形式で、{a: b,c: d}みたいなやつです。Rubyでいうハッシュ、JavaScriptでいくオブジェクトですね。簡単です。
(json以外にも、XMLやHTMLでもできるみたいですね)

(1)〜(3)を埋めて行きます。
(1)はcreateアクションにいくurlです。rails routeで確認すれば一発ですね。今回の私の場合は/messagesです。
(2)は、HTTPメソッドです。createアクションに行くので、'POST'ですね。
(3)は、検証ツールでメッセージを書き込むinputタグのname属性をみればわかります。
jjjj.png
ありました。このように参照される値なので、{message: {message: <投稿内容> }}のように渡せばいいですね。
では投稿内容はどうすれば良いかというとidがmessage_messageになっているので、$("#message_message").val()で取れます。(詳しい説明は省きます)
これを埋めて、投稿完了したときにたどり着くdoneメソッドをajaxメソッドに連ねて書きます。

app/assets/javascripts/messages.js
$(function () {
  $(".form").on("submit", function (e) {
    e.preventDefault()
    $.ajax({
      url:  "/messages" ,
      type: "POST" ,
      data:  {message: {message: $("#message_message").val() }} ,  
      dataType: 'json',
    }).done(function (data) {
      console.log("ok");
    });
  })
});

これで一度投稿してみましょう。データベースを見れば投稿は成功しているのがみて取れます。
以下に同期・非同期の両方でのターミナルでの状態を掲載します。

html.png
json.png

このような違いが出てますね。そしてなぜかコントローラのcreateアクションの最後のredirect_to root_pathが効かなくなりました。今コンソールにokは見られません。コントローラに残って機能しなくなったredirect_to root_pathが阻害しているようです。これを削除してもう一度投稿すれば、コンソールにokが見られるはずです。

このあと,投稿が完了した時に、formの値を全てなくして、投稿ボタンを蘇るようにします。
formの値をまとめてなくすには$('.form')[0].reset();を追記します。[0]がなぜ必要なのかはよくわかりません。
続いて投稿ボタンはerbファイルでボタンに適当にidを指定して(私は<%= f.submit "投稿", id:"bbb" %>こうしました)、$('#bbb').prop('disabled', false);を追記します。
全体を見てみます。

app/assets/javascripts/messages.js
$(function () {
  $(".form").on("submit", function (e) {
    e.preventDefault()
    $.ajax({
      url:  "/messages" ,
      type: "POST" ,
      data:  {message: {message: $("#message_message").val() }} ,  
      dataType: 'json',
    }).done(function () {
      $('.form')[0].reset();
      $('#bbb').prop('disabled', false);
    });
  })
});

これでリロードしなくでも、何回でも投稿できるようになりました。

あとは表示できるようにすればOKですね。

③投稿内容を非同期で表示させる

Ajaxでやってきたデータを扱うにはrespond_toメソッドを使います。

createアクションの最後のredirect_to root_pathがあった場所に、以下を追記します。

app/controllers/messages_controller.rb
respond_to do |format|
  format.json {render json: { ccc: @message.message , ddd: @message.user.email}}
end

メッセージ投稿内容とメッセージを送った人のEmailをそれぞれcccとdddに格納してjson形式でレンダーしますよってことですね。

あとは、doneイベント内で情報を受け取ってHTMLに整形してappendするだけです。
doneイベント内のfunction後の()内に何か文字をおけばそこに上のjsonデータが格納されます。今回はeeeとしてみました。これをコンソール出力してみます。

app/assets/javascripts/messages.js
〜省略〜
}).done(function (eee) {
  console.log(eee);
  $('.form')[0].reset();
  $('#bbb').prop('disabled', false);
});
〜省略〜

これで投稿してみると、コンソールで

Output
{ccc: "ハロー", ddd: "aaa@aaa"}

大丈夫そうですね。 これがeeeの中に入っているわけですから、"ハロー"を取得するにはeee.cccで、"aaa@aaa"を取得するには…大丈夫ですね。

あとは、これを表示させます。appendするために

app/views/messages/new.html.erb
〜省略〜
<% @messages.each do |m| %>
  <div style="margin-top: 20px;"><span style="color: red;"><%= m.user.email %></span><%= m.message %></div>
<% end %>

これをdivタグで囲って、適当なidをつけます。今回はaaaとしました。

app/views/messages/new.html.erb
〜省略〜
<div id="aaa">
  <% @messages.each do |m| %>
    <div style="margin-top: 20px;"><span style="color: red;"><%= m.user.email %> </span><%= m.message %></div>
  <% end %>
</div>

あとは

app/assets/javascripts/messages.js
$("#aaa").append(`<div style="margin-top: 20px;"><span style="color: red;">${eee.ddd}</span>${eee.ccc}</div>`)

を追記するだけです。
これで完成です。投稿して確認してください。

最後に再掲します。

app/assets/javascripts/messages.js
$(function () {
  $(".form").on("submit", function (e) {
    e.preventDefault();
    $.ajax({
      url: "/messages",
      type: "POST",
      data: { message: { message: $("#message_message").val() } },
      dataType: 'json',
    }).done(function (eee) {
      $('.form')[0].reset();
      $('#bbb').prop('disabled', false);
      $("#aaa").append(`<div style="margin-top: 20px;"><span style="color: red;">${eee.ddd}</span>${eee.ccc}</div>`)
    });
  })
});
app/views/messages/new.html.erb
<% if user_signed_in? %>
  <%= current_user.email %>
  <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %>
  <%= form_with(model: @message, local: true, class: "form") do |f| %>
    <%= f.text_field :message %>
    <%= f.submit "投稿" , id: "bbb"%>
  <% end %>
  <div id="aaa">
    <% @messages.each do |m| %>
      <div style="margin-top: 20px;"><span style="color: red;"><%= m.user.email %> </span><%= m.message %></div>
    <% end %>
  </div>
<% else %>
  <%= link_to '新規登録', new_user_registration_path %>
  <%= link_to 'ログイン', new_user_session_path %>
<% end %>

まとめ

本当に最低限です。

これを、整えて十分な状態にするための続きをまた書きます。
フォローしてお待ち下さい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.11 - Test coding -

はじめに

第11回目ですね。
前回はテストを自動化するためにRSpecSeleniumCapybaraなどを導入しましたね。

今日は今まで作ってきたアプリケーションに対してテストコードをコーディングしていきます。
本当はアプリをコーディングする前にテストをコーディングしてRedのフェーズにするべきなのですが、
まぁ最初なのでご愛嬌ということでGreenの状態から始めましょう。

前回のソースコード

前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。

どういうふうにテストコード書いてくの?

ここは人によってやりやすいようにでいいと思うのですが、このハンズオンでは基本的には作りたい機能(ユーザーストーリー)ごとにテストファイルを分けて記述していきます。
例えば、今までだと「サインアップ」とか「サインイン」とかそういうやつです。

例えば以下のようにテストシナリオを考えてみます。

1. ユーザーとして、ページにダイレクトアクセスしたい

  1. 未サインインのユーザーが、トップページにダイレクトアクセスしたとき、トップページが表示されること
  2. 未サインインのユーザーが、サインアップページにダイレクトアクセスしたとき、サインアップページが表示されること
  3. 未サインインのユーザーが、サインインページにダイレクトアクセスしたとき、サインインページが表示されること
  4. 未サインインのユーザーが、ユーザー詳細ページにダイレクトアクセスしたとき、ユーザー詳細ページが表示されること
  5. サインイン済のユーザーが、トップページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること
  6. サインイン済のユーザーが、サインアップページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること
  7. サインイン済のユーザーが、サインインページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること
  8. サインイン済のユーザーが、ユーザー詳細ページにダイレクトアクセスしたとき、ユーザー詳細ページが表示されること

2. ユーザーとして、ヘッダーリンクからページ遷移できること

  1. 未サインインのユーザーが、トップページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
  2. 未サインインのユーザーが、トップページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
  3. 未サインインのユーザーが、トップページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
  4. 未サインインのユーザーは、トップページでヘッダーに「Profile」リンクが存在しないこと
  5. 未サインインのユーザーは、トップページでヘッダーに「Sign out」リンクが存在しないこと
  6. 未サインインのユーザーが、サインアップページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
  7. 未サインインのユーザーが、サインアップページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
  8. 未サインインのユーザーが、サインアップページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
  9. 未サインインのユーザーは、サインアップページでヘッダーに「Profile」リンクが存在しないこと
  10. 未サインインのユーザーは、サインアップページでヘッダーに「Sign out」リンクが存在しないこと
  11. 未サインインのユーザーが、サインインページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
  12. 未サインインのユーザーが、サインインページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
  13. 未サインインのユーザーが、サインインページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
  14. 未サインインのユーザーは、サインインページでヘッダーに「Profile」リンクが存在しないこと
  15. 未サインインのユーザーは、サインインページでヘッダーに「Sign out」リンクが存在しないこと
  16. 未サインインのユーザーが、ユーザー詳細ページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
  17. 未サインインのユーザーが、ユーザー詳細ページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
  18. 未サインインのユーザーが、ユーザー詳細ページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
  19. 未サインインのユーザーは、ユーザー詳細ページでヘッダーに「Profile」リンクが存在しないこと
  20. 未サインインのユーザーは、ユーザー詳細ページでヘッダーに「Sign out」リンクが存在しないこと
  21. サインイン済のユーザーが、ユーザー詳細ページでヘッダーのロゴをクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
  22. サインイン済のユーザーは、ユーザー詳細ページでヘッダーに「Home」リンクが存在しないこと
  23. サインイン済のユーザーは、ユーザー詳細ページでヘッダーに「Sign in」リンクが存在しないこと
  24. サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Profile」リンクをクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
  25. サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Sign out」リンクが存在すること

3. ユーザーとして、サインアップしたい

  1. 未サインインのユーザーが、トップページで「Sign up now!」ボタンを選択したとき、サインアップページに遷移すること
  2. サインアップページで「お名前」を入力できること
  3. サインアップページで「メールアドレス」を入力できること
  4. サインアップページで「パスワード」を入力できること
  5. サインアップページで「パスワード」はマスク化されること
  6. サインアップページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスにチェックをいれたとき、「パスワード」が表示されること
  7. サインアップページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスのチェックを外したとき、「パスワード」がマスク化されること
  8. サインアップページで「お名前」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「お名前」未入力のエラーメッセージが表示されること
  9. サインアップページで「お名前」を51文字以上入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「お名前」文字数超過のエラーメッセージが表示されること
  10. サインアップページで「メールアドレス」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」未入力のエラーメッセージが表示されること
  11. サインアップページで「メールアドレス」を256文字以上入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」文字数超過のエラーメッセージが表示されること
  12. サインアップページで「メールアドレス」を誤ったフォーマットで入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」フォーマットチェックエラーのエラーメッセージが表示されること
  13. サインアップページで「メールアドレス」がすでに登録済みのメールアドレスを入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」重複のエラーメッセージが表示されること
  14. サインアップページで「パスワード」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「パスワード」文字数不足のエラーメッセージが表示されること
  15. サインアップページで「パスワード」を5文字以下で入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「パスワード」文字数不足のエラーメッセージが表示されること
  16. サインアップページで「お名前」「メールアドレス」「パスワード」を正しく入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは成功し、そのユーザーのユーザー詳細ページにサインイン済状態で遷移すること
  17. サインアップに成功したユーザーは、遷移後のユーザー詳細ページで自分の入力した「お名前」を確認できること
  18. サインアップに成功したユーザーは、遷移後のユーザー詳細ページで自分の入力した「メールアドレス」を確認できること
  19. サインアップに成功したユーザーは、遷移後のユーザー詳細ページでウェルカムメッセージを確認できること
  20. サインアップに成功したユーザーは、遷移後のユーザー詳細ページをリロードしたとき、ウェルカムメッセージを確認できなくなること
  21. サインアップページで「登録済みの方はこちら」リンクを選択したとき、サインインページに遷移すること

4. ユーザーとして、サインインしたい

  1. サインインページで「メールアドレス」を入力できること
  2. サインインページで「パスワード」を入力できること
  3. サインインページで「パスワード」はマスク化されること
  4. サインインページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスにチェックをいれたとき、「パスワード」が表示されること
  5. サインインページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスのチェックを外したとき、「パスワード」がマスク化されること
  6. サインインページで「メールアドレス」を入力していないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
  7. サインインページで「メールアドレス」として登録されていないメールアドレスを入力したユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
  8. サインインページで「メールアドレス」は正しいが「パスワード」を入力していないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
  9. サインインページで「メールアドレス」は正しいが「パスワード」が正しくないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
  10. サインインページで「メールアドレス」「パスワード」に正しい値を入力したユーザーが、「Sign in」ボタンをクリックしたとき、サインイン済状態でそのユーザーのユーザー詳細ページに遷移すること
  11. サインインに成功したユーザーは、遷移後のユーザー詳細ページでサインイン成功メッセージを確認できること
  12. サインインに成功したユーザーは、遷移後のユーザー詳細ページをリロードしたとき、サインイン成功メッセージを確認できなくなること
  13. サインインページで「登録がまだの方はこちら」リンクを選択したとき、サインアップページに遷移すること

5. ユーザーとして、サインアウトしたい

  1. サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Sign out」リンクをクリックしたとき、未サインイン状態になりトップページに遷移すること

6. ユーザーとして、他のユーザーの情報を閲覧したい

  1. ユーザーが、存在するユーザーのユーザー詳細ページにアクセスしようとしたとき、そのユーザーの「お名前」「メールアドレス」を確認できること
  2. ユーザーが、存在しないユーザーのユーザー詳細ページにアクセスしようとしたとき、エラーが発生すること

ざっとあげただけでもこれだけのテストシナリオがあります。
こんなにコード書かなきゃいけないのかよ!と思うかもしれませんが、コードを書かないと少しのリファクタリングの度にこれら全てのテストを手動で行わなければ安心してデプロイできないという修羅の道を選ぶことになります。
今日でテストコードへのハードルを爆下げして気軽にリファクタできるエンジニアをめざしましょう!

テストコードを書いていこう

ここから実際にテストコードを書いていきます。
上でナンバリングで章立てしてましたね。それごとにスペックファイルを作って管理します。

1. ユーザーとして、ページにダイレクトアクセスしたい

ファイル名は01_direct_access_spec.rbにしておきましょう。

$ mkdir spec/system/
$ touch spec/system/01_direct_access_spec.rb
01_direct_access_spec.rb
feature "ユーザーとして、ページにダイレクトアクセスしたい", type: :system do

  background do
    @user1 = User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
    @user2 = User.create(name: "Taro Tanaka", email: "taro@sample.com", password: "taro1234")
  end

  scenario "未サインインのユーザーが、トップページにダイレクトアクセスしたとき、トップページが表示されること" do
    visit root_path
    expect(current_path).to eq root_path
  end

  scenario "未サインインのユーザーが、サインアップページにダイレクトアクセスしたとき、サインアップページが表示されること" do
    visit sign_up_path
    expect(current_path).to eq sign_up_path
  end

  scenario "未サインインのユーザーが、サインインページにダイレクトアクセスしたとき、サインインページが表示されること" do
    visit sign_in_path
    expect(current_path).to eq sign_in_path
  end

  scenario "未サインインのユーザーが、ユーザー詳細ページにダイレクトアクセスしたとき、ユーザー詳細ページが表示されること" do
    visit user_path(@user1)
    expect(current_path).to eq user_path(@user1)

    visit user_path(@user2)
    expect(current_path).to eq user_path(@user2)
  end

  feature nil, type: :system do

    background do
      visit sign_in_path
      fill_in :user_email, with: @user1.email
      fill_in :user_password, with: @user1.password
      click_on :sign_in_button      
    end

    scenario "サインイン済のユーザーが、トップページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること" do
      visit root_path
      expect(current_path).to eq user_path(@user1)
    end

    scenario "サインイン済のユーザーが、サインアップページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること" do
      visit sign_up_path
      expect(current_path).to eq user_path(@user1)
    end

    scenario "サインイン済のユーザーが、サインインページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること" do
      visit sign_in_path
      expect(current_path).to eq user_path(@user1)
    end

    scenario "サインイン済のユーザーが、ユーザー詳細ページにダイレクトアクセスしたとき、ユーザー詳細ページが表示されること" do
      visit user_path(@user1)
      expect(current_path).to eq user_path(@user1)

      visit user_path(@user2)
      expect(current_path).to eq user_path(@user2)
    end

  end

end

このテストをパスさせるために、サインインページに少し細工をします。

app/views/sessions/new.html.erb
- <%= form.submit "Sign in", class: "btn btn-primary form-control" %>
+ <%= form.submit "Sign in", class: "btn btn-primary form-control", id: :sign_in_button %>

id: :sign_in_buttonを追記しました。これでid属性を追加できます。

まずはテストがパスするのを体感しましょうか!

$ docker-compose up -d
$ docker-compose exec web ash
# rspec spec/system/01_direct_access_spec.rb
Capybara starting Puma...
* Version 4.3.1 , codename: Mysterious Traveller
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:45497
........

Finished in 11.85 seconds (files took 5.4 seconds to load)
8 examples, 0 failures

テストパスしましたね!
どんなテストが実行されたのか、ちょっと紹介させてください!

構文

前回も紹介しましたが、RSpecのシステムテストの構文は

feature "test name", type: :system do
  scenario "test scenario" do
    # テストコード
  end
end

です。scenarioは複数あります。

feature内の全てのscenarioに適用する初期条件を記述する場合はbackgroundを使います。

feature "test name", type: :system do
  background do
    # 前提条件
  end

  scenario "test scenario" do
    # テストコード
  end
end

また、featureを入れ子にすることも可能です。これによって特定のscenarioたちにだけ前提条件をつけることも可能です。

feature "test name", type: :system do
  scenario "test scenario" do
    # 前提条件が適用されない
  end

  feature "test detail name", type: :system do
    background do
      # 前提条件
    end

    scenario "test scenario" do
      # 前提条件が適用される    
    end
  end
end

まずはこの構文を身に付けましょう。

background

backgroundは前提条件を定義するためのブロックです。そのファイルのシナリオに共通して行われる処理をここで定義します。
よく使われる場面としては、今回のようにモデルを作っておく、とかですね。
モデルは今までのRubyコードと同じように、User.createUser.newが使えます。また、インスタンス変数にしないとscenario側では参照できないので注意してくださいね。

visit

visitは引数に名前付きルート(xxxx_path)やURLをとって、そこにアクセスします。

visit root_path

これでroot_path、つまり/にアクセスをしようとします。

expect().to

expect().to()内をto以降と検証します。
例えばexpect().to eq xxxxxのようにeqと組み合わせることで()内とxxxxxが等しいことを検証します。
この検証がfalseの場合はそのシナリオをFailureになります。

current_path

current_pathは現在表示されているパス(/とか/users/1とか)を取得します。

expect(current_path).to eq root_path

で、今表示されているページのパスがルートパス、つまりトップページであるかどうかを検証しているのです。

fill_in

fill_ininputに文字を入力する操作を実行します。

fill_in [id], with: [入力したい文字列]

で、ページからid属性が[id]input[入力したい文字列]を入力してくれます。

click_on

click_on<a>または<button>タグをクリックする操作を実行します。

click_on [id]

で、ページからid属性が[id]<a><button>タグをクリックしてくれます。

ここまでを理解すると

background do
  visit sign_in_path
  fill_in :user_email, with: @user1.email
  fill_in :user_password, with: @user1.password
  click_on :sign_in_button
end

が、サインインページにアクセスして@user1でサインインしようとしていることがわかりますね?

こんな感じで操作と検証を組み合わせてテストを自動化していきます!
ではどんどんテストコードを書いていきましょう!!

2. ユーザーとして、ヘッダーリンクからページ遷移できること

ファイル名は02_header_spec.rbでいきましょうか。

# touch spec/system/02_header_spec.rb

また少しidを仕込んでおきましょう。

app/views/layouts/application.html.erb
  <div class="container">
-   <%= link_to "sample app", root_path, class: "navbar-brand" %>
+   <%= link_to "sample app", root_path, class: "navbar-brand", id: :header_logo %>
    <ul class="navbar-nav">
      <% if signed_in? %>
        <%# サインイン済みの場合のリンク %>
-       <li class="nav-item"><%= link_to "Profile", current_user, class: "nav-link" %></li>
-       <li class="nav-item"><%= link_to "Sign out", sign_out_path, method: :delete, class: "nav-link" %></li>
+       <li class="nav-item"><%= link_to "Profile", current_user, class: "nav-link", id: :header_profile_link %></li>
+       <li class="nav-item"><%= link_to "Sign out", sign_out_path, method: :delete, class: "nav-link", id: :header_sign_out_link %></li>
      <% else %>
        <%# 未サインインの場合のリンク %>
-       <li class="nav-item"><%= link_to "Home", root_path, class: "nav-link" %></li>
-       <li class="nav-item"><%= link_to "Sign in", sign_in_path, class: "nav-link" %></li>
+       <li class="nav-item"><%= link_to "Home", root_path, class: "nav-link", id: :header_home_link %></li>
+       <li class="nav-item"><%= link_to "Sign in", sign_in_path, class: "nav-link", id: :header_sign_in_link %></li>
      <% end %>
    </ul>
  </div>

そしてテストシナリオを書きます。

spec/system/02_header_spec.rb
feature "ユーザーとして、ヘッダーリンクからページ遷移できること", type: :system do
  background do
    @user1 = User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
    @user2 = User.create(name: "Taro Tanaka", email: "taro@sample.com", password: "taro1234")
  end

  scenario "未サインインのユーザーが、トップページでヘッダーのロゴをクリックしたとき、トップページに遷移すること" do
    visit root_path
    click_on :header_logo
    expect(current_path).to eq root_path
  end

  scenario "未サインインのユーザーが、トップページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること" do
    visit root_path
    click_on :header_home_link
    expect(current_path).to eq root_path
  end

  scenario "未サインインのユーザーが、トップページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること" do
    visit root_path
    click_on :header_sign_in_link
    expect(current_path).to eq sign_in_path
  end

  scenario "未サインインのユーザーは、トップページでヘッダーに「Profile」リンクが存在しないこと" do
    visit root_path
    expect(page).not_to have_selector "#header_profile_link"
  end

  scenario "未サインインのユーザーは、トップページでヘッダーに「Sign out」リンクが存在しないこと" do
    visit root_path
    expect(page).not_to have_selector "#header_sign_out_link"
  end

  scenario "未サインインのユーザーが、サインアップページでヘッダーのロゴをクリックしたとき、トップページに遷移すること" do
    visit sign_up_path
    click_on :header_logo
    expect(current_path).to eq root_path
  end

  scenario "未サインインのユーザーが、サインアップページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること" do
    visit sign_up_path
    click_on :header_home_link
    expect(current_path).to eq root_path
  end

  scenario "未サインインのユーザーが、サインアップページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること" do
    visit sign_up_path
    click_on :header_sign_in_link
    expect(current_path).to eq sign_in_path
  end

  scenario "未サインインのユーザーは、サインアップページでヘッダーに「Profile」リンクが存在しないこと" do
    visit sign_up_path
    expect(page).not_to have_selector "#header_profile_link"
  end

  scenario "未サインインのユーザーは、サインアップページでヘッダーに「Sign out」リンクが存在しないこと" do
    visit sign_up_path
    expect(page).not_to have_selector "#header_sign_out_path"
  end

  scenario "未サインインのユーザーが、サインインページでヘッダーのロゴをクリックしたとき、トップページに遷移すること" do
    visit sign_in_path
    click_on :header_logo
    expect(current_path).to eq root_path
  end

  scenario "未サインインのユーザーが、サインインページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること" do
    visit sign_in_path
    click_on :header_home_link
    expect(current_path).to eq root_path
  end

  scenario "未サインインのユーザーが、サインインページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること" do
    visit sign_in_path
    click_on :header_sign_in_link
    expect(current_path).to eq sign_in_path
  end

  scenario "未サインインのユーザーは、サインインページでヘッダーに「Profile」リンクが存在しないこと" do
    visit sign_in_path
    expect(page).not_to have_selector "#header_profile_path"
  end

  scenario "未サインインのユーザーは、サインインページでヘッダーに「Sign out」リンクが存在しないこと" do
    visit sign_in_path
    expect(page).not_to have_selector "#header_sign_out_path"
  end

  scenario "未サインインのユーザーが、ユーザー詳細ページでヘッダーのロゴをクリックしたとき、トップページに遷移すること" do
    visit user_path(@user1)
    click_on :header_logo
    expect(current_path).to eq root_path
  end

  scenario "未サインインのユーザーが、ユーザー詳細ページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること" do
    visit user_path(@user1)
    click_on :header_home_link
    expect(current_path).to eq root_path
  end

  scenario "未サインインのユーザーが、ユーザー詳細ページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること" do
    visit user_path(@user1)
    click_on :header_sign_in_link
    expect(current_path).to eq sign_in_path
  end

  scenario "未サインインのユーザーは、ユーザー詳細ページでヘッダーに「Profile」リンクが存在しないこと" do
    visit user_path(@user1)
    expect(page).not_to have_selector "#header_profile_link"
  end

  scenario "未サインインのユーザーは、ユーザー詳細ページでヘッダーに「Sign out」リンクが存在しないこと" do
    visit user_path(@user1)
    expect(page).not_to have_selector "#header_sign_out_link"
  end

  feature nil, type: :system do

    background do
      visit sign_in_path
      fill_in :user_email, with: @user1.email
      fill_in :user_password, with: @user1.password
      click_on :sign_in_button            
    end

    scenario "サインイン済のユーザーが、ユーザー詳細ページでヘッダーのロゴをクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること" do
      visit user_path(@user2)
      click_on :header_logo
      sleep 1
      expect(current_path).to eq user_path(@user1)
    end

    scenario "サインイン済のユーザーは、ユーザー詳細ページでヘッダーに「Home」リンクが存在しないこと" do
      visit user_path(@user2)
      expect(page).not_to have_selector "#header_home_link"
    end

    scenario "サインイン済のユーザーは、ユーザー詳細ページでヘッダーに「Sign in」リンクが存在しないこと" do
      visit user_path(@user2)
      expect(page).not_to have_selector "#header_sign_in_link"
    end

    scenario "サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Profile」リンクをクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること" do
      visit user_path(@user2)
      click_on :header_profile_link
      expect(current_path).to eq user_path(@user1)
    end

    scenario "サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Sign out」リンクが存在すること" do
      visit user_path(@user2)
      expect(page).to have_selector "#header_sign_out_link"
    end

  end

end

ダイレクトアクセスのテストと似ているところが多いですが、新出のコードを紹介していきます!

page

expect(page)という形で現れましたね。これは今表示されているページ全体のことです。

not_to

expect().not_toという表現がでてきましたね。
toの反対で()not_to以降がアンマッチであることを検証します。
マッチした場合にfalseになり、そのシナリオがFailureになります。

have_selector

have_selectorは指定したタグや属性を持っているかを検証します。
例えば、以下のような要素があるとします。

<h1 id="title" class="main-title">Title</h1>

これをタグ、id属性、class属性でそれぞれhave_selectorで検証しようとすると以下のようになります。

# タグで検証
expect(page).to have_selector("h1")

# id属性で検証
expect(page).to have_selector("#title")

# class属性で検証
expect(page).to have_selector(".main-title")

ページ内や子要素に特定の要素がないかを調べる時に使うので覚えておくとよしです!

sleep

sleepは指定した秒数、次のコードの実行を待つコードです。sleep 1であれば1秒待った後に次の行に進みます。
今回は、アプリケーション側でサインイン状態を確認した後リダイレクトする処理を入れていますが、自動化されたテストがそのままのスピードで検証を進めてしまうとリダイレクト処理が終わる前に検証が完了してしまい、思ったとおりの結果を得られないことがあります。
こういった自体を防ぐために、sleepを挟むことで処理を待たせることも必要になります。

ただし、sleepの使用は最低限にするべきです。なぜならそのせいでテストの実行時間が長くなってしまっては自動化した意味が失われかねないからです。

3. ユーザーとして、サインアップしたい

ファイル名は03_sign_up_spec.rbでいきます!

# touch spec/system/03_sign_up_spec.rb

そして、今回もidを仕込みます。

app/views/static_pages/home.html.erb
- <%= link_to "Sign up now!", sign_up_path, class: "btn btn-lg btn-primary mt-5" %>
+ <%= link_to "Sign up now!", sign_up_path, class: "btn btn-lg btn-primary mt-5", id: :sign_up_link %>
app/views/users/new.html.erb
  <%= form_with model: @user, url: create_user_path, local: true do |form| %>
    ...
    <div class="form-group mt-5">
-     <%= form.submit "Sign up!", class: "form-control btn btn-primary" %>
+     <%= form.submit "Sign up!", class: "form-control btn btn-primary", id: :sign_up_button %>
    </div>
  <% end %>
- <p class="text-center">登録済みの方は<%= link_to "こちら", sign_in_path %></p>
+ <p class="text-center">登録済みの方は<%= link_to "こちら", sign_in_path, id: :sign_in_link %></p>

はい。ではテストコードです。

spec/system/03_sign_up_spec.rb
feature "ユーザーとして、サインアップしたい", type: :system do
  background do
    @user = User.new(name: "John Smith", email: "john@sample.com", password: "john1234")
  end

  scenario "未サインインのユーザーが、トップページで「Sign up now!」ボタンを選択したとき、サインアップページに遷移すること" do
    visit root_path
    click_on :sign_up_link
    expect(current_path).to eq sign_up_path
  end

  scenario "サインアップページで「お名前」を入力できること" do
    visit sign_up_path
    fill_in :user_name, with: @user.name
    expect(find("#user_name").value).to eq @user.name
  end

  scenario "サインアップページで「メールアドレス」を入力できること" do
    visit sign_up_path
    fill_in :user_email, with: @user.email
    expect(find("#user_email").value).to eq @user.email
  end

  scenario "サインアップページで「パスワード」を入力できること" do
    visit sign_up_path
    fill_in :user_password, with: @user.password
    expect(find("#user_password").value).to eq @user.password
  end

  scenario "サインアップページで「パスワード」はマスク化されること" do
    visit sign_up_path
    fill_in :user_password, with: @user.password
    expect(find("#user_password")[:type]).to eq "password"
  end

  scenario "サインアップページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスにチェックをいれたとき、「パスワード」が表示されること" do
    visit sign_up_path
    fill_in :user_password, with: @user.password
    check :visible_password
    expect(find("#user_password")[:type]).to eq "text"
  end

  scenario "サインアップページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスのチェックを外したとき、「パスワード」がマスク化されること" do
    visit sign_up_path
    fill_in :user_password, with: @user.password
    check :visible_password
    uncheck :visible_password
    expect(find("#user_password")[:type]).to eq "password"
  end

  scenario "サインアップページで「お名前」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「お名前」未入力のエラーメッセージが表示されること" do
    error_message = "お名前を入力してください"

    visit sign_up_path
    fill_in :user_name, with: ""
    fill_in :user_email, with: @user.email
    fill_in :user_password, with: @user.password
    click_on :sign_up_button

    expect(current_path).to eq sign_up_path
    expect(page).to have_text error_message
  end

  scenario "サインアップページで「お名前」を51文字以上入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「お名前」文字数超過のエラーメッセージが表示されること" do
    error_message = "お名前は50文字以内で入力してください"

    visit sign_up_path
    fill_in :user_name, with: "a" * 51
    fill_in :user_email, with: @user.email
    fill_in :user_password, with: @user.password
    click_on :sign_up_button

    expect(current_path).to eq sign_up_path
    expect(page).to have_text error_message

    fill_in :user_name, with: "a" * 50
    fill_in :user_password, with: @user.password
    click_on :sign_up_button

    expect(current_path).not_to eq sign_up_path
    expect(page).not_to have_text error_message
    expect(current_path).to eq user_path(User.find_by(email: @user.email))
  end

  scenario "サインアップページで「メールアドレス」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」未入力のエラーメッセージが表示されること" do
    error_message = "メールアドレスを入力してください"

    visit sign_up_path
    fill_in :user_name, with: @user.name
    fill_in :user_email, with: ""
    fill_in :user_password, with: @user.password
    click_on :sign_up_button

    expect(current_path).to eq sign_up_path
    expect(page).to have_text error_message
  end

  scenario "サインアップページで「メールアドレス」を256文字以上入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」文字数超過のエラーメッセージが表示されること" do
    error_message = "メールアドレスは255文字以内で入力してください"

    visit sign_up_path
    fill_in :user_name, with: @user.name
    fill_in :user_email, with: "a" * 245 + "@sample.com"
    fill_in :user_password, with: @user.password
    click_on :sign_up_button

    expect(current_path).to eq sign_up_path
    expect(page).to have_text error_message

    fill_in :user_email, with: "a" * 244 + "@sample.com"
    fill_in :user_password, with: @user.password
    click_on :sign_up_button

    expect(current_path).not_to eq sign_up_path
    expect(page).not_to have_text error_message
    expect(current_path).to eq user_path(User.find_by(email: "a" * 244 + "@sample.com")) 
  end

  scenario "サインアップページで「メールアドレス」を誤ったフォーマットで入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」フォーマットチェックエラーのエラーメッセージが表示されること" do
    error_message = "メールアドレスは不正な値です"

    visit sign_up_path
    fill_in :user_name, with: @user.name
    fill_in :user_email, with: "sample.com"
    fill_in :user_password, with: @user.password
    click_on :sign_up_button

    expect(current_path).to eq sign_up_path
    expect(page).to have_text error_message
  end

  scenario "サインアップページで「メールアドレス」がすでに登録済みのメールアドレスを入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」重複のエラーメッセージが表示されること" do
    error_message = "メールアドレスはすでに存在します"
    @user.save

    visit sign_up_path
    fill_in :user_name, with: @user.name
    fill_in :user_email, with: @user.email.upcase
    fill_in :user_password, with: @user.password
    click_on :sign_up_button

    expect(current_path).to eq sign_up_path
    expect(page).to have_text error_message
  end

  scenario "サインアップページで「パスワード」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「パスワード」文字数不足のエラーメッセージが表示されること" do
    error_message = "パスワードは6文字以上で入力してください"

    visit sign_up_path
    fill_in :user_name, with: @user.name
    fill_in :user_email, with: @user.email
    fill_in :user_password, with: ""
    click_on :sign_up_button

    expect(current_path).to eq sign_up_path
    expect(page).to have_text error_message
  end

  scenario "サインアップページで「パスワード」を5文字以下で入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「パスワード」文字数不足のエラーメッセージが表示されること" do
    error_message = "パスワードは6文字以上で入力してください"

    visit sign_up_path
    fill_in :user_name, with: @user.name
    fill_in :user_email, with: @user.email
    fill_in :user_password, with: "john1"
    click_on :sign_up_button

    expect(current_path).to eq sign_up_path
    expect(page).to have_text error_message

    fill_in :user_password, with: "john12"
    click_on :sign_up_button

    expect(current_path).not_to eq sign_up_path
    expect(page).not_to have_text error_message
    expect(current_path).to eq user_path(User.find_by(email: @user.email))
  end

  feature nil, type: :system do
    background do
      @welcome_message = "サインアップありがとう"

      visit sign_up_path
      fill_in :user_name, with: @user.name
      fill_in :user_email, with: @user.email
      fill_in :user_password, with: @user.password
      click_on :sign_up_button
    end

    scenario "サインアップページで「お名前」「メールアドレス」「パスワード」を正しく入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは成功し、そのユーザーのユーザー詳細ページにサインイン済状態で遷移すること" do
      expect(current_path).to eq user_path(User.find_by(email: @user.email))
      expect(page).not_to have_selector "#header_sign_in_link"
      expect(page).to have_selector "#header_sign_out_link"
    end

    scenario "サインアップに成功したユーザーは、遷移後のユーザー詳細ページで自分の入力した「お名前」を確認できること" do
      expect(page).to have_text @user.name
    end

    scenario "サインアップに成功したユーザーは、遷移後のユーザー詳細ページで自分の入力した「メールアドレス」を確認できること" do
      expect(page).to have_text @user.email
    end

    scenario "サインアップに成功したユーザーは、遷移後のユーザー詳細ページでウェルカムメッセージを確認できること" do
      expect(page).to have_text @welcome_message
    end

    scenario "サインアップに成功したユーザーは、遷移後のユーザー詳細ページをリロードしたとき、ウェルカムメッセージを確認できなくなること" do
      visit current_path
      expect(page).not_to have_text @welcome_message
    end
  end

  scenario "サインアップページで「登録済みの方はこちら」リンクを選択したとき、サインインページに遷移すること" do
    visit sign_up_path
    click_on :sign_in_link
    expect(current_path).to eq sign_in_path
  end
end

また、はじめましての書き方を紹介していきます。

find

モデルのときにつかったfindとはまた別ですよ。
find()でページの中から()内で指定した要素を取得してくれます。1つ以上該当するものがあるとエラーになってしまうので、id属性に対して使うのが好ましいでしょう。
例えば今回のテストコードでは、

expect(find("#user_name").value).to eq @user.name

のように使っていますが、これでid属性がuser_nameに定義されている要素を取得します。

value

find("#user_name").value

のようにvalueを使っています。これはinputvalue属性を取得しています。
value属性にはinput type="text"などの場合にはテキストボックスにデフォルトで入力しておきたい文字列を入力しておいたりしますが、Capybaraではvalue属性を取得することで今入力されている文字列を取得することができます。

[:type]

find("#user_password")[:type]

のように使っています。Capybaraではvaluetextは要素と.でつなぐことで取得できるのですが、それ以外の属性は[:attribute_name]の形式で取得します。[:type]だとtype属性を取得してきていることになりますね。
今回はpasswordtype属性をjavascriptでtextpasswordを切り替えているので、これでチェックができます。textはマスク化なし、passwordはマスク化ありはHTML5の仕様なので、今回のテストではtype属性が正しく指定されているかを検証しました。

check

checkはチェックボックスにチェックする操作です。
今回は

check :visible_password

の形式でid属性がvisible_passwordのチェックボックスにチェックを入れています。

uncheck

uncheckcheckの反対でチェックボックスからチェックを外す操作です。

have_text

have_textは指定した文字列が存在するかどうかを検証するために使います。

expect(page).to have_text xxxxxxxxxx

と記述することでページのどこかにでもxxxxxxxxxxの文字列が存在しないかを検証します。
pageの箇所をfind()などで限定した要素にすることで、その要素内にxxxxxxxxxxの文字列が存在するかどうかを検証するように範囲を狭めることもできます。

visit current_path

以前お話したようにcurrent_pathは現在のパスです。そこにvisitしているということは...

そう!これはリロード操作ですね。

はい。今回のテストコードで新しく出てきた表現はこのくらいではないでしょうか。
そろそろ慣れてきましたか?一回書き始めると案外それらの組み合わせだけでいろいろなテストを実行できることがわかってきたんじゃないかと思います。
それでは次はサインインのテストコードを記述していきましょう!

4. ユーザーとして、サインインしたい

まずはシナリオファイルの作成から。

# touch spec/system/04_sign_in_spec.rb

そして、必要な箇所にid属性を振ります。

app/views/sessions/new.html.erb
- <p class="text-center">登録がまだの方は<%= link_to "こちら", sign_up_path %></p>
+ <p class="text-center">登録がまだの方は<%= link_to "こちら", sign_up_path, id: :sign_up_link %></p>

以下、テストコードです。今回は目新しい表現はないので、下のコードを見ずに書いてみてもらっても面白いかもしれないです。

spec/system/04_sign_in_spec.rb
feature "ユーザーとして、サインインしたい", type: :system do
  background do
    @user = User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
  end

  scenario "サインインページで「メールアドレス」を入力できること" do
    visit sign_in_path
    fill_in :user_email, with: @user.email
    expect(find("#user_email").value).to eq @user.email
  end

  scenario "サインインページで「パスワード」を入力できること" do
    visit sign_in_path
    fill_in :user_password, with: @user.password
    expect(find("#user_password").value).to eq @user.password
  end

  scenario "サインインページで「パスワード」はマスク化されること" do
    visit sign_in_path
    fill_in :user_password, with: @user.password
    expect(find("#user_password")[:type]).to eq "password"
  end

  scenario "サインインページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスにチェックをいれたとき、「パスワード」が表示されること" do
    visit sign_in_path
    fill_in :user_password, with: @user.password
    check :visible_password
    expect(find("#user_password")[:type]).to eq "text"
  end

  scenario "サインインページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスのチェックを外したとき、「パスワード」がマスク化されること" do
    visit sign_in_path
    fill_in :user_password, with: @user.password
    check :visible_password
    uncheck :visible_password
    expect(find("#user_password")[:type]).to eq "password"
  end

  scenario "サインインページで「メールアドレス」を入力していないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること" do
    error_message = "メールアドレスまたはパスワードをもう一度確認してください。"

    visit sign_in_path
    fill_in :user_email, with: ""
    fill_in :user_password, with: @user.password
    click_on :sign_in_button

    expect(current_path).to eq sign_in_path
    expect(page).to have_text error_message
  end

  scenario "サインインページで「メールアドレス」として登録されていないメールアドレスを入力したユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること" do
    error_message = "メールアドレスまたはパスワードをもう一度確認してください。"

    visit sign_in_path
    fill_in :user_email, with: "dummy@sample.com"
    fill_in :user_password, with: @user.password
    click_on :sign_in_button

    expect(current_path).to eq sign_in_path
    expect(page).to have_text error_message
  end

  scenario "サインインページで「メールアドレス」は正しいが「パスワード」を入力していないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること" do
    error_message = "メールアドレスまたはパスワードをもう一度確認してください。"

    visit sign_in_path
    fill_in :user_email, with: @user.email
    fill_in :user_password, with: ""
    click_on :sign_in_button

    expect(current_path).to eq sign_in_path
    expect(page).to have_text error_message  
  end

  scenario "サインインページで「メールアドレス」は正しいが「パスワード」が正しくないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること" do
    error_message = "メールアドレスまたはパスワードをもう一度確認してください。"

    visit sign_in_path
    fill_in :user_email, with: @user.email
    fill_in :user_password, with: @user.password + "a"
    click_on :sign_in_button

    expect(current_path).to eq sign_in_path
    expect(page).to have_text error_message      
  end

  feature nil, type: :system do
    background do
      @sign_in_message = "サインインしました。"

      visit sign_in_path
      fill_in :user_email, with: @user.email
      fill_in :user_password, with: @user.password
      click_on :sign_in_button        
    end

    scenario "サインインページで「メールアドレス」「パスワード」に正しい値を入力したユーザーが、「Sign in」ボタンをクリックしたとき、サインイン済状態でそのユーザーのユーザー詳細ページに遷移すること" do
      expect(current_path).to eq user_path(@user)
      expect(page).not_to have_selector "#header_sign_in_link"
      expect(page).to have_selector "#header_sign_out_link"
    end

    scenario "サインインに成功したユーザーは、遷移後のユーザー詳細ページでサインイン成功メッセージを確認できること" do
      expect(page).to have_text @sign_in_message
    end

    scenario "サインインに成功したユーザーは、遷移後のユーザー詳細ページをリロードしたとき、サインイン成功メッセージを確認できなくなること" do
      visit current_path
      expect(page).not_to have_text @sign_in_message
    end
  end

  scenario "サインインページで「登録がまだの方はこちら」リンクを選択したとき、サインアップページに遷移すること" do
    visit sign_in_path
    click_on :sign_up_link

    expect(current_path).to eq sign_up_path
  end
end

5. ユーザーとして、サインアウトしたい

次はサインアウトについてですね。

# touch spec/system/05_sign_out_spec.rb

そしてコーディング。これも今までのコードの組み合わせで表現可能ですね。

spec/system/05_sign_out_spec.rb
feature "ユーザーとして、サインアウトしたい", type: :system do
  scenario "サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Sign out」リンクをクリックしたとき、未サインイン状態になりトップページに遷移すること" do
    user = User.create(name: "John Smith", email: "john@sample.com", password: "john1234")

    visit sign_in_path
    fill_in :user_email, with: user.email
    fill_in :user_password, with: user.password
    click_on :sign_in_button

    visit user_path(user)

    click_on :header_sign_out_link

    expect(current_path).to eq root_path
    expect(page).to have_selector "#header_sign_in_link"
    expect(page).not_to have_selector "#header_sign_out_link"
  end
end

コメントアウトなどで説明文を書いたりは省いていますが、今までの内容が理解できていれば何をしているのか想像できると思います。
サインインページにアクセスしてサインインし、ユーザー詳細ページでヘッダーのサインアウトリンクをクリックし、トップページにリダイレクトされたと同時に未サインイン状態になっていることを検証していますね。

6. ユーザーとして、他のユーザーの情報を閲覧したい

まずはシナリオファイルです。

# touch spec/system/06_show_user_info_spec.rb

今回のテストでは、NotFoundのユーザーのユーザー詳細ページを表示しようとした時に、NotFoundのページが表示される、という項目があります。
Railsでは、NotFoundの例外が発生した場合、production環境の場合はデフォルトでNotFound用のページが表示されるようになっています。
test環境でも同じようにNotFoundページが表示されるようにconfigを変更します。

config/environments/test.rb
- config.consider_all_requests_local       = true
+ config.consider_all_requests_local       = false
- config.action_dispatch.show_exceptions = false
+ config.action_dispatch.show_exceptions = true

これで準備完了です。
NotFoundの場合、public/404.htmlが表示されるようになります。
中身を見ると、

<h1>The page you were looking for doesn't exist.</h1>

と記述されているので、この文字列があるかどうかをチェックするようにします。

ではテストコードです。

spec/system/06_show_user_info_spec.rb
feature "ユーザーとして、他のユーザーの情報を閲覧したい", type: :system do
  background do
    @user1 = User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
    @user2 = User.create(name: "Taro Yamada", email: "taro@sample.com", password: "taro1234")    
  end

  scenario "ユーザーが、存在するユーザーのユーザー詳細ページにアクセスしようとしたとき、そのユーザーの「お名前」「メールアドレス」を確認できること" do

    # Before sign in
    visit user_path(@user1)
    expect(page).to have_text @user1.name
    expect(page).to have_text @user1.email
    expect(page).not_to have_text @user2.name
    expect(page).not_to have_text @user2.email

    visit user_path(@user2)
    expect(page).not_to have_text @user1.name
    expect(page).not_to have_text @user1.email
    expect(page).to have_text @user2.name
    expect(page).to have_text @user2.email

    # After sign in
    visit sign_in_path
    fill_in :user_email, with: @user1.email
    fill_in :user_password, with: @user1.password
    click_on :sign_in_button

    visit user_path(@user1)
    expect(page).to have_text @user1.name
    expect(page).to have_text @user1.email
    expect(page).not_to have_text @user2.name
    expect(page).not_to have_text @user2.email

    visit user_path(@user2)
    expect(page).not_to have_text @user1.name
    expect(page).not_to have_text @user1.email
    expect(page).to have_text @user2.name
    expect(page).to have_text @user2.email
  end

  scenario "ユーザーが、存在しないユーザーのユーザー詳細ページにアクセスしようとしたとき、エラーが発生すること" do
    not_found_message = "The page you were looking for doesn't exist."
    not_found_id = @user2.id + 1
    expect{User.find(not_found_id)}.to raise_exception(ActiveRecord::RecordNotFound)

    # Before sign in
    visit user_path(not_found_id)
    expect(page).to have_text not_found_message

    # After sign in
    visit sign_in_path
    fill_in :user_email, with: @user1.email
    fill_in :user_password, with: @user1.password
    click_on :sign_in_button

    visit user_path(not_found_id)
    expect(page).to have_text not_found_message
  end
end

基本的には今までと変わりありませんね。
一つだけexceptionの検証の仕方だけ新出があるのでそれの説明を。

expect{}.to raise_exception()

今までと違うのはexpectの検証ターゲットを()ではなく{}でかこっていることですね。
そしてraise_exceptionの後に期待する例外を記述します。
今回は@user2id+1したidのユーザーを検索しています。idはシーケンシャルに払い出されるので@user2よりも大きいidを持っているユーザーはいないはず。
なのでUser.find(not_found_id)ActiveRecord::RecordNotFoundの例外が発生するはずです。

はい。ここまでで今のところ考えられる全てのテストケースをコーディングしてみました。
テストを実行してみましょう!

# rspec
Capybara starting Puma...
* Version 4.3.1 , codename: Mysterious Traveller
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:46237
......................................................................

Finished in 1 minute 21.27 seconds (files took 5.82 seconds to load)
70 examples, 0 failures

全てパスしてますね!
もしパスしないテストケースがある場合は、もう一度アプリのコードかテストコードを見返してみてくださいね。

また、あえてエラーになるようにテストコードを書き直してみてエラーになることを確認してみるのも面白いと思います。

さて、では本日はここまでにしましょう!

まとめ

今日は今まで作ってきたアプリに対してテストコードをコーディングしてみました。
これによって今後リファクタリングの都度自動テストを回すだけで全ての動作を確認することができるようになりましたね。

テスト自動化、楽しいですよね??

テスト自動化は新しい機能を作ったり、アプリの仕様自体を変更する時にテストコードも記述する必要があるのでその分稼働が必要になることもあります。
しかし、リファクタリングや新機能開発時のデグレテストを簡略化でき、自動テストをパスしていればデプロイを自信をもって行える安心感を得ることができます。特にアプリが大きくなっていくと、これは稼働以上に嬉しい恩恵です。

今後はこのハンズオンでもTDDで開発を進めていきます!

では、次回も乞うご期待!ここまでお読みいただきありがとうございました!

後片付け

では後片付けしていきますー。

前回もお話した通り、RSpecのシステムテストはテストが終わるとDBを勝手にリセットしてくれます。
ので、コンテナを落として終了ですね。

$ docker-compose down

本日のソースコード

Reference

Other Hands-on Links

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

js→railsへのajax通信で404エラーを出しまくった話。

はじめに

はい皆の衆よくお聞き
初心者様による
初心者でもできるエラー殺し講座を始める

よく聞かないと死にます

前回、ancestryとjQueryで多階層型カテゴリの入力フォームを段階的に表示させてみたを投稿するためにサンプルを作ったのですが、さすが初心者。
サンプルを作り終える間に多大なエラーを出しては修正し、修正しては出ししていました。
今回はそのエラーから「404 NotFound」を切り出して、どのように解決していったかを記事にしていきます。

今回取り扱うエラーについて

404 (Not Found)
HTTP 404、またはエラーメッセージ Not Found(「未検出」「見つかりません」の意)は、HTTPステータスコードの一つ。 クライアントがサーバに接続できたものの、クライアントの要求に該当するもの (ウェブページ等) をサーバが見つけられなかったことを示すもの。(wikipediaより抜粋)

第一の404 (Not Found)

まず、「js書くべし」で以下のjsを書きました。

assets/javascript/items.js
$(function() {
  function buildHTML(result){
    var html =
      `<option value= ${result.id}>${result.name}</option>`
    return html
  }

  $("#parent").on("change",function(){
    var int = document.getElementById("parent").value
    if(int == 0){
      $('#child').remove();
      $('#item_category_id').remove();
    }else{
      $.ajax({
        url: "categories/",
        type: 'GET',
        dataType: 'json',
        data: {id: int}
      })
      .done(function(categories) {
        var insertHTML = `<select name="child" id="child">
                          <option value=0>---</option>`;
        $.each(categories, function(i, category) {
          insertHTML += buildHTML(category)
        });
        insertHTML += `</select>`
        if($('#child').length){
          $('#child').replaceWith(insertHTML);
          $('#item_category_id').remove();
        } else {
          $('.items__child').append(insertHTML);
        };
      })
      .fail(function() {
      });
    };
  });
})

はい、もーなんか既に分かる人には分かりますね。

これで親の選択フォームを変更するとエラーになります。
スクリーンショット 2020-03-14 15.18.01.png

原因

原因はこれでした。
url: "categories/",
以下のように変更しました。
url: "categories/", => url: "/categories",

気付いたきっかけ

ターミナルを見た時に以下のようになっていました。
Started GET "/items/categories/?id=1" for ::1 at 2020-03-14 15:17:23 +0900
ActionController::RoutingError (No route matches [GET] "/items/categories"):

?なんでitemにネストしたurlになってるの?
ということで、url修正しました。

第二の404 (Not Found)

しかし、再度親の選択フォームを変更するとエラーになります。
スクリーンショット 2020-03-14 15.18.01.png

今度のターミナルエラーはこれ。
Started GET "/categories?id=1" for ::1 at 2020-03-14 15:36:36 +0900
ActionController::RoutingError (No route matches [GET] "/categories"):

原因

config/routes.rb
Rails.application.routes.draw do
  root "items#new"
  resources :items ,only: [:index,:new,:create]
end

ただのroutingの設定忘れでした。

config/routes.rb
Rails.application.routes.draw do
  root "items#new"
  resources :items ,only: [:index,:new,:create]
  resources :categories ,only: :index
end

修正します。

第三の404 (Not Found)

しかし、再度親の選択フォームを変更するとエラーになります。
スクリーンショット 2020-03-14 15.18.01.png

ターミナルエラーはこれ。
Started GET "/categories?id=1" for ::1 at 2020-03-14 15:45:21 +0900
ActionController::RoutingError (uninitialized constant CategoriesController):

原因

categories_controller.rbが無い・・・だと?
ちゃんとあるじゃ・・・ある・・・あ?
スクリーンショット 2020-03-14 16.00.36.png

/controllers/category_controller.rb
class CategoryController < ApplicationController
  def index
    @categories = Category.where(ancestry: params[:id])
    respond_to do |format|
      format.json
    end
  end
end

・・・controller作る時に単数形にしてますね・・・。
消して作り直します。

terminal.
$ rails d controller category
$ rails g controller categories
/controllers/categories_controller.rb
class CategoriesController < ApplicationController
  def index
    @categories = Category.where(ancestry: params[:id])
    respond_to do |format|
      format.json
    end
  end
end

結果

ちゃんと動作するようになりました!
スクリーンショット 2020-03-14 15.54.46.png

今回の教訓

①エラーが出たらターミナルをよく見よう。
②routingはちゃんとやっておこう。
③controllerは複数形で作ろう。

今回は以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

営業マンが独学でSNSをリリースし、スクールに「来なくていいよ」と言われた

ITとは無縁の製造業で営業をしながら、友人とRailsアプリを約半年でリリースしました。

お悩み相談SNS "Probless"
機能:ログイン/投稿/タグ付け/2層コメント/リアルタイムチャット/いいね/通知/検索/フィード/メール認証/理論削除/ + インフラ(AWS/自動デプロイ)
使用技術:Ruby on Rails/PostgreSQL/Action Cable/Elastic Search/Devise 認証/acts-as-taggable-on(タグ付け)/無限スクロール/Bootstrap/AWS/GitHub/独自ドメイン/Circle CI/Nginx/Https 認証

結論 ~学んだこと~

・誰かの役に立つという確信をミッションとして持っておくべき
・独学でもWebアプリを作ることができる
・スクールに通わない道もある

ちなみに過去にも独学でiOSアプリをリリースしたことがあり、今回は二つ目のリリースとなります。
営業マンが独学でiOSアプリをリリースし、レビュー4.5をもらうまで

開発経緯

データ分析が仕事の友人と何か作ろう!と意気投合し、様々なアイディアを検討した結果、「お悩み相談SNS」を作ることにしました。既存のお悩み掲示板は、匿名がゆえに中傷的なコメントが多いため、匿名でも親身なアドバイスが得やすい仕組みを備えた相談 SNS を考案しました。

企画段階(1ヶ月)

様々な記事を参考にし、手探りで下記をまとめていきました

  • 解決したい社会課題
  • ミッション定義
  • ユーザ体験&行動の流れをツリーで定義
  • 競合分析(良い点/悪い点/チャンス)
  • DB設計
  • スケジュールと役割分担
  • ワイヤーフレーム作成
  • 言語選び

方法模索段階(1ヶ月)

アイディアをそのまま外注すると数百万円するため断念。スクラッチで作ることにしました。
Ruby、Python、Phpで迷いましたが、プロゲートでRailsのレッスンがあったためRubyを選択。
私はプロゲートのRailsを2週してから1ヶ月で開発スタートしましたが、
友人はプログラミングの経験無く、プロゲートをやってもらいました。(2-3ヶ月)

ローカル開発段階(3ヶ月)

GitHubで2名チーム開発を行いました。
早朝や夜にコードを書き、通勤中にエラーを調べる習慣を続けました。

学んだこと

  • 企画段階でミッションをがっちり定めておかないとブレる
    • 本当にこの機能は必要か?判断基準となる
  • 誰かの役に立つという確信がモチベーションとなり、疲れてても継続できる
  • フロントはBootStrapのテンプレートを使い楽をする
  • デザインは配色は既存のサービスを参考にする
  • 「実現したいこと」を先に決めてから、「そのために必要な技術」を調べ尽くしてとにかくやる!!

特に難しかったのが、Action Cableでした。
Action CableはWebSocket通信技術を用いてリアルタイムな更新を可能にする機能です。
「相談者」と「コメントした人」との間で、悩みという共通のトピックに対してリアルタイムに会話をして欲しいという想いがあり実装しました。1週間何も進まないことありましたが、英語記事を参考になんとか実装しました。

Action Cable 本番使用時のNginxとCable.ymlの設定

デプロイ(2ヶ月)

インフラ環境

  • AWS
  • PostgreSQL
  • Nginx
  • Puma
  • Circle CI
  • Elastic Search (一時は実装しましたが、投稿が増えてからマージ予定)
  • Route 53 (独自ドメイン)
  • Let's Encrypt (SSL)

デプロイにかなり苦労しました。何がわからないのか分からない状況でした。
特に全文検索を可能にするElastic Searchにはかなり苦しめられました。

  • AWSでノード構築
  • AmazonLinuxへインストール、起動(JVM)
  • RailsとElasticSearchとノードの接続
  • 投稿された内容を、ノードにインデックス

しかしせっかく実装し動作確認までしたものの、投稿が増えるまでは威力を発揮しないので、一旦取りやめました。。

その後

Webエンジニアへ転職を決意し、会社を辞めて都内の某著名スクールに通おうとしたところ、スクールの面接で「来なくていいよ」と言われてしまい、結局スクール無しで転職活動中です。
エンジニア特化型のエージェントにも、サービスの目的が「転職のポートフォリオ用」では無く、「社会課題を解決するため」本気で作ってきたことに、企画力と自走力、継続力、実装力を評価頂いております。

感想

偉そうで大変恐縮ですが、何かサービスを作りたいならすぐに取りかかれば良いと思いました。イメージしたことを実現したい時、最初は方法も分からず無理なように感じますが、サービスに情熱と確信を持って調べ尽くせば方法は必ず見つかると思います。SNSの基本的機能くらいなら、独学で調べて実現できることを経験できました。

まとめ

・誰かの役に立つという確信をミッションとして持っておくべき
・独学でもWebアプリを作ることができる
・スクールに通わない道もある

面接にお呼びください

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

営業マンが独学でSNSをリリースし、スクールに「来なくていいよ」と言われた話

ITとは無縁の製造業で営業をしながら、友人とRailsアプリを約半年でリリースしました。

お悩み相談SNS "Probless"
機能:ログイン/投稿/タグ付け/2層コメント/リアルタイムチャット/いいね/通知/検索/フィード/メール認証/理論削除/ + インフラ(AWS/自動デプロイ)
使用技術:Ruby on Rails/PostgreSQL/Action Cable/Elastic Search/Devise 認証/acts-as-taggable-on(タグ付け)/無限スクロール/Bootstrap/AWS/GitHub/独自ドメイン/Circle CI/Nginx/Https 認証

結論 ~学んだこと~

・誰かの役に立つという確信をミッションとして持っておくべき
・独学でもWebアプリを作ることができる
・スクールに通わない道もある

ちなみに過去にも独学でiOSアプリをリリースしたことがあり、今回は二つ目のリリースとなります。
営業マンが独学でiOSアプリをリリースし、レビュー4.5をもらうまで

開発経緯

データ分析が仕事の友人と何か作ろう!と意気投合し、様々なアイディアを検討した結果、「お悩み相談SNS」を作ることにしました。既存のお悩み掲示板は、匿名がゆえに中傷的なコメントが多いため、匿名でも親身なアドバイスが得やすい仕組みを備えた相談 SNS を考案しました。

企画段階(1ヶ月)

様々な記事を参考にし、手探りで下記をまとめていきました

  • 解決したい社会課題
  • ミッション定義
  • ユーザ体験&行動の流れをツリーで定義
  • 競合分析(良い点/悪い点/チャンス)
  • DB設計
  • スケジュールと役割分担
  • ワイヤーフレーム作成
  • 言語選び

方法模索段階(1ヶ月)

アイディアをそのまま外注すると数百万円するため断念。スクラッチで作ることにしました。
Ruby、Python、Phpで迷いましたが、プロゲートでRailsのレッスンがあったためRubyを選択。
私はプロゲートのRailsを2週してから1ヶ月で開発スタートしましたが、
友人はプログラミングの経験無く、プロゲートをやってもらいました。(2-3ヶ月)

ローカル開発段階(3ヶ月)

GitHubで2名チーム開発を行いました。
早朝や夜にコードを書き、通勤中にエラーを調べる習慣を続けました。

学んだこと

  • 企画段階でミッションをがっちり定めておかないとブレる
    • 本当にこの機能は必要か?判断基準となる
  • 誰かの役に立つという確信がモチベーションとなり、疲れてても継続できる
  • フロントはBootStrapのテンプレートを使い楽をする
  • デザインは配色は既存のサービスを参考にする
  • 「実現したいこと」を先に決めてから、「そのために必要な技術」を調べ尽くしてとにかくやる!!

特に難しかったのが、Action Cableでした。
Action CableはWebSocket通信技術を用いてリアルタイムな更新を可能にする機能です。
「相談者」と「コメントした人」との間で、悩みという共通のトピックに対してリアルタイムに会話をして欲しいという想いがあり実装しました。1週間何も進まないことありましたが、英語記事を参考になんとか実装しました。

Action Cable 本番使用時のNginxとCable.ymlの設定

デプロイ(2ヶ月)

インフラ環境

  • AWS
  • PostgreSQL
  • Nginx
  • Puma
  • Circle CI
  • Elastic Search (一時は実装しましたが、投稿が増えてからマージ予定)
  • Route 53 (独自ドメイン)
  • Let's Encrypt (SSL)

デプロイにかなり苦労しました。何がわからないのか分からない状況でした。
特に全文検索を可能にするElastic Searchにはかなり苦しめられました。

  • AWSでノード構築
  • AmazonLinuxへインストール、起動(JVM)
  • RailsとElasticSearchとノードの接続
  • 投稿された内容を、ノードにインデックス

しかしせっかく実装し動作確認までしたものの、投稿が増えるまでは威力を発揮しないので、一旦取りやめました。。

その後

Webエンジニアへ転職を決意し、会社を辞めて都内の某著名スクールに通おうとしたところ、スクールの面接で「来なくていいよ」と言われてしまい、結局スクール無しで転職活動中です。
エンジニア特化型のエージェントにも、サービスの目的が「転職のポートフォリオ用」では無く、「社会課題を解決するため」本気で作ってきたことに、企画力と自走力、継続力、実装力を評価頂いております。

感想

イメージしたことを実現したい時、最初は方法も分からず無理なように感じますが、調べ続ければ方法は必ず見つかると思いました。SNSの基本的機能くらいなら、独学でも実現できることを経験できました。

まとめ

・誰かの役に立つという確信をミッションとして持っておくべき
・独学でもWebアプリを作ることができる
・スクールに通わない道もある

面接にお呼びください

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Cloud9上でPostgreSQLをソースからインストール

概要

 AWS Cloud9 上でpostgreqlとdeviseを使ってRailsアプリを開発をしようとした際に、
postgresqlのバージョンが古いと怒られたのでやってみたことをメモ。

  • 環境
    • AWS Cloud9
      • Amazon Linux AMI release 2018.03
    • postgre
      • postgres (PostgreSQL) 9.2.24

ソースからコンパイル

下記を順に実行
一応コンパイラからインストール

$ sudo yum install -y gcc readline-devel zlib-devel
$ wget https://ftp.postgresql.org/pub/source/v10.4/postgresql-10.4.tar.gz
$ tar -xf postgresql-10.4.tar.gz
$ cd postgresql-10.4
$ ./configure
$ make -C src/bin

ここでエラーが出るのでMakefile内を下記のように修正する。

$ make -C src/bin
make: Entering directory `/home/ec2-user/postgresql-10.4/src/bin'
Makefile:14: ../../src/Makefile.global: No such file or directory
make: *** No rule to make target `../../src/Makefile.global'.  Stop.
make: Leaving directory `/home/ec2-user/postgresql-10.4/src/bin'

Makefileの14行目の'Makefile.global'のところが間違いで
'Makefile.global.in'に訂正すると無事にmakeできます。そのまま続きへ、

$ sudo make -C src/bin install
$ make -C src/include
$ sudo make -C src/include install
$ make -C src/interfaces
$ sudo make -C src/interfaces install
$ make -C doc
$ sudo make -C doc install

以上でエラーが出なければインストールは完了。
今回インストールされた場所は/usr/local/pgsql/bin/psqlなのでそこにパスを通します。

$ vi ~/.bash_profile

として最後の行に

export PATH="/usr/local/pgsql/bin:$PATH"

を追加して保存。

$ source ~/.bash_profile

これで完了。

バージョンを確認する

$ psql --version
psql (PostgreSQL) 10.4

10.4になってます。

因みに、これをアンインストールするにはmakeした場所で、

$ make uninstall

とすると消える。

参照

How To Completely Uninstall PostgreSQL
Installing PostgreSQL Client v10 on AWS Amazon Linux (EC2) AMI
make installしたソフトウェアをアンインストールする

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails|generateコマンドで生成されるファイルに制限をかける(config.generatorsの設定)

はじめに

generateコマンドっていろんなファイルを作成してくれるけど、「helperファイルの自動生成は必要ないのになー」って時などには、かえって煩わしく感じますよね。

そこで今回は、config/application.rb内の設定によって、generate コマンドで生成されるファイルに制限をかけます。(※スキップオプションを付ける方法もあるけど省略)

まずはrails generation時の挙動を確認

ターミナルでrails generation(rails gに省略可)を行うと、色々なファイルが自動生成される。

> rails g controller user show      
Running via Spring preloader in process 
      create   app/controllers/user_controller.rb
       route   get 'user/show'
      invoke   erb
      create    app/views/user
      create    app/views/user/show.html.erb
      invoke   rspec
      create    spec/controllers/user_controller_spec.rb
      create    spec/views/user
      create    spec/views/user/show.html.erb_spec.rb
      invoke   helper
      create    app/helpers/user_helper.rb
      invoke    rspec
      create    spec/helpers/user_helper_spec.rb
      invoke   assets
      invoke    js
      create    app/assets/javascripts/user.js
      invoke    scss
      create    app/assets/stylesheets/user.scss

便利だけど、必要ないファイルがあれば、それだけ生成されないようにする。
(この自動生成したファイル等を削除したければ、ターミナルでrails destroy controller user showを実行しておく)

自動生成するファイルに制限をかける

今回は、config/application.rb内の設定で、
・assetsファイル
・testファイル
・ルーティング
の生成を無効にしてみる。

config/application.rb
puts class Application < Rails::Application



 #以下のように、generateコマンド時に生成されるファイルに制限をかける
   config.generators do |g|
     g.assets  false
     g.test_framework    false
     g.skip_routes   true
   end
 end

上記の設定によって、

> rails g controller user show      
Running via Spring preloader in process 82700
      create  app/controllers/user_controller.rb
      invoke  erb
      create    app/views/user
      create    app/views/user/show.html.erb
      invoke  helper
      create    app/helpers/user_helper.rb

生成ファイルに制限がかかった。

このように、プロジェクトごとに設定を変えたり、自分好みのカスタマイズができる。

おわりに

今回が初のQiita投稿でした!

これからも新しく学んだこと等をQiitaに共有できればと思います。
ご覧いただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

generateコマンドで生成されるファイルに制限をかける(config.generatorsの設定)

はじめに

generateコマンドっていろんなファイルを作成してくれるけど、「helperファイルの自動生成は必要ないのになー」って時などには、かえって煩わしく感じますよね。

そこで今回は、config/application.rb内の設定によって、generate コマンドで生成されるファイルに制限をかけます。(※スキップオプションを付ける方法もあるけど省略)

まずはrails generation時の挙動を確認

ターミナルでrails generation(rails gに省略可)を行うと、色々なファイルが自動生成される。

> rails g controller user show      
Running via Spring preloader in process 
      create   app/controllers/user_controller.rb
       route   get 'user/show'
      invoke   erb
      create    app/views/user
      create    app/views/user/show.html.erb
      invoke   rspec
      create    spec/controllers/user_controller_spec.rb
      create    spec/views/user
      create    spec/views/user/show.html.erb_spec.rb
      invoke   helper
      create    app/helpers/user_helper.rb
      invoke    rspec
      create    spec/helpers/user_helper_spec.rb
      invoke   assets
      invoke    js
      create    app/assets/javascripts/user.js
      invoke    scss
      create    app/assets/stylesheets/user.scss

便利だけど、必要ないファイルがあれば、それだけ生成されないようにする。
(この自動生成したファイル等を削除したければ、ターミナルでrails destroy controller user showを実行しておく)

自動生成するファイルに制限をかける

今回は、config/application.rb内の設定で、
・assetsファイル
・testファイル
・ルーティング
の生成を無効にしてみる。

config/application.rb
puts class Application < Rails::Application



 #以下のように、generateコマンド時に生成されるファイルに制限をかける
   config.generators do |g|
     g.assets  false
     g.test_framework    false
     g.skip_routes   true
   end
 end

上記の設定によって、

> rails g controller user show      
Running via Spring preloader in process 82700
      create  app/controllers/user_controller.rb
      invoke  erb
      create    app/views/user
      create    app/views/user/show.html.erb
      invoke  helper
      create    app/helpers/user_helper.rb

生成ファイルに制限がかかった。

このように、プロジェクトごとに設定を変えたり、自分好みのカスタマイズができる。

おわりに

今回が初のQiita投稿でした!

これからも新しく学んだこと等をQiitaに共有できればと思います。
ご覧いただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

『2ページ遷移して会員登録』できる最低限のRailsアプリを丁寧に作る(deviseをウィザード形式に拡張)

この記事の基本的な方針

deviseで実装したログイン機能は特にカスタマイズしなければ、1ページでEmailとパスワードの2項目を入力して登録完了です。
これを2ページに拡張し、1ページ目にEmailとパスワードに加えニックネームを、2ページ目でケータイ番号の入力を求めます。

この記事は、以下の「登録画面」「ログイン画面」「TOP画面」の3画面の簡単なアプリを元に拡張していきます。

【TOP画面(ログイン前)】     【TOP画面(ログイン後)】
a0.png a9.png
【登録画面】
a1.png
【ログイン画面】
a2.png

手を動かしながら読みたいようでしたら、以下でこの3画面アプリを手に入れてください。

Terminal
$ git clone -b 超最低限のRailsアプリ(messageコントローラVer)  https://github.com/annaPanda8170/minimum_rails_application.git
$ bundle install
$ bundle exec rake db:create
$ bundle exec rake db:migrate

これ自体の作り方はこちら

最終的に以下のように1ページ目にニックネームの項目が加わり、ケータイ番号の登録をする2ページ目が新たに作られます。

222a.png 333a.png

想定する読み手

既に一度Railsアプリをチュートリアルやスクール等で作ったことがある方を想定しております。
Mac使用で、パソコンの環境構築は完了していることが前提です。

具体的なコーディング手順

完成品GitHub(masterではなく一つのブランチなので注意して下さい)

①取り急ぎ1ページ目のみでニックネームカラムを登録項目として増やす

まず1ページのままでニックネーム入力項目を追加してみましょう。
通常、最初にテーブルを作成するときはrails g model <モデル名>としたときに一緒にマイグレーションファイルも作られますが、今回はすでにあるテーブルにカラムを増やすだけなのでマイグレーションファイルのみを作ります。

Terminal
$ rails g migration <クラス名>

を打ちます。
クラス名はなんでもいいです。rails g migration Aaaでも問題ないですが、rails g migration AddColumn<テーブル名>とするのが一般的です。今回はrails g migration AddColumnUsersとしましょう。
そして、作られたマイグレーションファイルを編集します。

db/migrate/20200xxxxxxxxxxx_add_column_users.rb
class AddColumnUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :nickname, :string 
  end
end

カラムを加えるadd_columnメソッドを使います。第一引数がテーブル名、第二引数がカラム名、第三引数がデータ型です。「ユーザテーブルニックネームカラム文字列型で加えます」ってことですね。極めて直感的です。余談ですが、コードを読むときは悶々とコードのまま黙読で理解しようとせずに、きっちりと日本語(自然言語)に直して読んでみることをオススメいたします。

そして

Terminal
$ rails db:migrate

して、テーブルは完成です。一応データベースを見に行ってカラムが増えているかを確認しましょう。

続いてコントローラで、追加したカラムを受け入れられるようにします。ここはapp/controllers/messages_controller.rbではなく、app/controllers/application_controller.rbを編集します。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?
  protected
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:<追加したカラム名>])
  end
end

難しいですが、それぞれのメソッドを完全に理解できなくでも大丈夫です。deviseのGitHubにそうしろと書いてあるのでそれに従います。掃除機を正しく使うのに掃除機を作れるようになる必要はありません。説明書をきちんと読みましょう。
今回はdevise_parameter_sanitizer.permit(:sign_up, keys: [:nickname])ですね。

今回はnicknameなしでは登録できないようにしようと思うので、モデルを編集します。以下を追記します。

app/models/user.rb
〜省略〜
  validates :nickname ,presence: true
〜省略〜

あとはビューにニックネームを入力する欄を加えれば完了ですね。
あれ?ビューファイルはどこにありますか?
ありません。deviseをデフォルトのまま使うにはビューファイルとコントローラファイルの編集が必要ないので見えないところに隠されています。これを編集するには表に出させる必要があります。

Terminal
$ rails g devise:views

すると、隠れていたビューファイル達が表に出てきます。当たり前ですが、ただ表に出てきただけなので編集しなければ何も変わりません。
以下を追記します。

app/views/devise/registrations/new.html.erb
<div class="field">
  <%= f.label :nickname %><br />
  <%= f.text_field :nickname %>
</div>

autofocus: trueに関しては適切な場所に一箇所置きます。詳細は省きます。

これで登録してみます。
大丈夫そうですね。

②2ページ目用モデル

2ページ目で入力されるケータイ番号をデータベースに保存するには、usersテーブルにカラムを追加するのではなく新たにテーブルを作ります。新たにテーブルを作るには、

Terminal
$ rails g model cellphone

して、userテーブルに従属するので、

db/migrate/2020xxxxxxxxxx_create_cellphones.rb
class CreateCellphones < ActiveRecord::Migration[5.2]
  def change
    create_table :cellphones do |t|
      t.integer :cellphone
      t.references :user
      t.timestamps
    end
  end
end

と編集して、

Terminal
$ rails db:migrate

です。慣れたもんです。

ここからモデルにバリデーションとアソシエーションを記述します。
まずapp/models/cellphone.rbvalidates :cellphone ,presence: trueを加えます。バリデーションです。これは簡単ですね。
続いて、アソシエーションです。cellphaneモデルとuserモデルを同時に見てください。

app/models/cellphone.rb
class Cellphone < ApplicationRecord
  validates :cellphone ,presence: true
  belongs_to :user, optional: true
end
app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  validates :name ,presence: true
  has_one :cellphone
end

belongs_toメソッドとhas_oneメソッドが使われています。テーブル同士の関係が1対1の場合に使われます。1対1でどちらにどちらのメソッドを使うかというと、外部キーを置いていない側にbelongs_toを置きます。今回は後から作ったcellphoneの側のテーブルにuser_idカラムがあります。だから、cellphoneテーブルがuserテーブルに従属する(belongs)というイメージですね。そして逆側ににhas_oneを置くことになります。
optional: trueが付いていますが、これはウィザード形式を実装するには必須です。(コントローラでCellphoneモデルのみでインスタンスを生成する場面があるからのようです)

③2ページ目用コントローラ

隠れていたビューファイルとコントローラファイルのうち前者は①で表に出していますが、ここでは後者のコントローラファイルを表に出して編集していく必要があります。 表に出してみましょう。

Terminal
$ rails g devise:controllers <コントローラのディレクトリ名>

コントローラのディレクトリ名はなんでも大丈夫ですが、通常モデル名の複数形なのでrails g devise:controllers usersにします。

さてここで話を整理しましょう。普通、登録画面に関するバックグラウンドの流れは、コントローラのnewアクションを通り、newのビューが表示され、そこのformにクライアントが情報が入力してsubmitが押されると、createアクションに来て、バリデーションを通過すればデータベースに登録するというものであります。図示するとこんな感じです。

newアクション → 登録画面 → createアクション → データベース

ここで2ページ目の登録画面を挟まなくてはならないので、

①newアクション → ①登録画面 → ①createと②newのアクション → ②登録画面 → ②createアクション → データベース

となります。
この①createと②newのアクション②createアクションが難しいです。丁寧に行きましょう。

①createと②newのアクションは、registrations_controller.rbにもとからあるcreateアクション(コメントアウトされています)を使います。②createアクションは新たに適当にアクションを作ります。

まず①createと②newのアクションについてです。ここは①登録画面と②登録画面の橋渡しです。①登録画面の情報を受け取って変数(session変数という特別な変数)に格納して②登録画面に遷移します。

まず、最終的なcreateアクションの全体を掲載します。

app/controllers/users/registrations_controller.rb
# frozen_string_literal: true

class Users::RegistrationsController < Devise::RegistrationsController
〜省略〜
  # POST /resource
  def create
    user = User.new(sign_up_params)
    unless user.valid?
      render :new and return
    end
    session[:registration] = {user: user.attributes}
    session[:registration][:user][:password] = params[:user][:password]
    @cellphone = user.build_cellphone
    render :new_cellphone
  end
〜省略〜
end

これを構築してゆきます。
まず、表に出したapp/controllers/users/registrations_controller.rbのコメントアウトしているcreateアクションを復活させ、superを消してbinding.pryをかませてみましょう。

app/controllers/users/registrations_controller.rb
# frozen_string_literal: true

class Users::RegistrationsController < Devise::RegistrationsController
〜省略〜
  # POST /resource
  def create
    binding.pry
  end
〜省略〜
end

こんな感じです。http://localhost:3000/users/sign_upにアクセスして適当に値を埋めてSign upボタンを押すと停止するので、ターミナルを見ます。

まず①登録画面で入力された情報は

Terminal
> sign_up_params

で取れます。私の場合、

Output
=> {"email"=>"aaa@aaa", "password"=>"123123123", "password_confirmation"=>"123123123", "name"=>"annaPanda"}

こんな感じです。sign_up_paramsとはdeviseが準備しているメソッドでしょう。
さてこのsign_up_paramsを代入してUserモデルのインスタンスを生成してみましょう。

Terminal
> aaa = User.new(sign_up_params)
Output
=> #<User id: nil, email: "aaa@aaa", created_at: nil, updated_at: nil, nickname: "annaPanda">

これでバリデーションを通過できるかどうかはvalid?メソッドを使います。

Terminal
> aaa.valid?
Output
  User Exists (0.8ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'aaa@aaa' LIMIT 1
  ↳ (pry):3
=> true

trueって出ました。OKなようです。
続いて、②登録画面に引き継ぐべき値はattributesメソッドで取れます。

Terminal
> aaa.attributes
Output
=> {"id"=>nil,
 "email"=>"aaa@aaa",
 "encrypted_password"=>"$2a$11$W/lFietoiCkM6Lj6bsuY2eTsvGSg7nakpLCisng73tpv5bp.8OGFu",
 "reset_password_token"=>nil,
 "reset_password_sent_at"=>nil,
 "remember_created_at"=>nil,
 "created_at"=>nil,
 "updated_at"=>nil,
 "nickname"=>"annaPanda"}

パスワードがencrypted_passwordで暗号化されていますね。ですが引き継ぐ段階では元のパスワードが必要なのでそれを引き出します。params変数から、

Terminal
> params[:user][:password]
Output
=> "123123123"

このように取れるのはわかりますね。
さてここでsession変数を使います。普通変数などの値は、ブラウザが読み込まれるたびに一度クリアになります。これは"ステートレス"というHTTP通信の性質です。これに抗って値を保持できるのがsession変数です。
格納するsessionに付随する[:registration]や[:user]や[:password]の名前はなんでもOKですが今回はこうします。
あとはcellphone用のインスタンスを生成できればOKです。これにはnewメソッド出なく特別なbuild_<テーブル名>メソッドを使います。

Terminal
> aaa.build_cellphone
Output
=> #<Cellphone:0x00007fc4c5d1db20 id: nil, cellphone: nil, user_id: nil, created_at: nil, updated_at: nil>

これらの情報を元にcreateアクションを構築すると上のようになるわけです。
再掲します。

app/controllers/users/registrations_controller.rb
# frozen_string_literal: true

class Users::RegistrationsController < Devise::RegistrationsController
〜省略〜
  # POST /resource
  def create
    user = User.new(sign_up_params)
    unless user.valid?
      render :new and return
    end
    session[:registration] = {user: user.attributes}
    session[:registration][:user][:password] = params[:user][:password]
    @cellphone = user.build_cellphone
    render :new_cellphone
  end
〜省略〜
end

そしてこのコントローラが使用されるにはルーティングを変更する必要があります。

config/routes.rb
Rails.application.routes.draw do
  get 'messages/index'
  devise_for :users, controllers: {
    registrations: 'users/registrations',
  }
  devise_scope :user do
    get 'cellphones', to: 'users/registrations#new_cellphone'
    post 'cellphones', to: 'users/registrations#create_cellphone'
  end
  root "messages#index"
end

devise_for :usersの先に加えられているのがcreateアクションへのルーティングです。これがないと上で書いたcreateアクションが使われません。
2ページ目用はdevise_scopeメソッドを使います。その先は普通にURIを設定しているだけですね。

②登録画面のビューは以下です。ファイル自体もないので作ります。

app/views/devise/registrations/new_cellphone.html.erb
<%= form_for @cellphone do |f| %>
  <%= render "devise/shared/error_messages", resource: @cellphone %>

  <div class="field">
    <%= f.label :cellphone %><br />
    <%= f.text_field :cellphone %>
  </div>

  <div class="actions">
    <%= f.submit "Sign up" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

続いて、②createアクションです。名前はcreate_cellphoneです。

app/controllers/users/registrations_controller.rb
# frozen_string_literal: true

class Users::RegistrationsController < Devise::RegistrationsController
〜省略〜
  def create_cellphone
    @user = User.new(session[:registration]["user"])
    @cellphone = Cellphone.new(cellphone_params)
    unless @cellphone.valid?
      flash.now[:alert] = @cellphone.errors.full_messages
      render :new_cellphone and return
    end
    @user.build_cellphone(@cellphone.attributes)
    @user.save
    sign_in(:user, @user)
    redirect_to root_path
  end

  protected

  def cellphone_params
    params.require(:cellphone).permit(:cellphone)
  end
〜省略〜
end

最終的にこうなります。これを構築してゆきます。

createアクションと同じようにbinding.pryをかませて見てみましょう。

app/controllers/users/registrations_controller.rb
# frozen_string_literal: true

class Users::RegistrationsController < Devise::RegistrationsController
〜省略〜
  def create_cellphone
    binding.pry
  end
〜省略〜
end

②createアクションのアクション名はcreate_<テーブル名>でないとならないみたいですね。

まず、session変数に格納されている値を引き出してみましょう。

Terminal
> session[:registration][:user]
Output
=> nil

あれれ?なんででしょう。仕方ないので[:user]を抜いて見てみましょう。

Terminal
> session[:registration]
Output
=> {"user"=>
  {"id"=>nil,
   "email"=>"bbb@bbb",
   "encrypted_password"=>"$2a$11$Gp6J1spbcfu4EiS6EfqJsuGjZV1GB9LZXpWTzTBNDTY0GBmsIDDgm",
   "reset_password_token"=>nil,
   "reset_password_sent_at"=>nil,
   "remember_created_at"=>nil,
   "created_at"=>nil,
   "updated_at"=>nil,
   "nickname"=>"annaPanda",
   "password"=>"321321321"}}

取れました。"user"になっているみたいなんでそれに変えてみましょう。

Terminal
> session[:registration]["user"]
Output
=> {"id"=>nil,
 "email"=>"bbb@bbb",
 "encrypted_password"=>"$2a$11$Gp6J1spbcfu4EiS6EfqJsuGjZV1GB9LZXpWTzTBNDTY0GBmsIDDgm",
 "reset_password_token"=>nil,
 "reset_password_sent_at"=>nil,
 "remember_created_at"=>nil,
 "created_at"=>nil,
 "updated_at"=>nil,
 "nickname"=>"annaPanda",
 "password"=>"321321321"}

OKです。
次に②登録画面で入力されたケータイ番号を引き出してみましょう。
通常のストロングパラメータと同じですね。

Terminal
> params.require(:cellphone).permit(:cellphone)
Output
=> <ActionController::Parameters {"cellphone"=>"09011111111"} permitted: true>

さてコントローラ全体はこんな感じになります。

app/controllers/users/registrations_controller.rb
# frozen_string_literal: true

class Users::RegistrationsController < Devise::RegistrationsController
〜省略〜
  def create
    user = User.new(sign_up_params)
    unless user.valid?
      render :new and return
    end
    session[:registration] = {user: user.attributes}
    session[:registration][:user][:password] = params[:user][:password]
    @cellphone = user.build_cellphone
    render :new_cellphone
  end

  def create_cellphone
    @user = User.new(session[:registration]["user"])
    @cellphone = Cellphone.new(cellphone_params)
    unless @cellphone.valid?
      flash.now[:alert] = @cellphone.errors.full_messages
      render :new_cellphone and return
    end
    @user.build_cellphone(@cellphone.attributes)
    @user.save
    sign_in(:user, @user)
    redirect_to root_path
  end

  protected

  def cellphone_params
    params.require(:cellphone).permit(:cellphone)
  end
〜省略〜
end

これで完成です。
まだケータイ番号用に桁数を制限などはしていません。余裕があったら挑戦してみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

既存の開発環境にDockerを導入する

はじめに

今まではAWSのcloud9でrailsアプリを開発していましたが、実際の開発現場で使われているというDockerを開発環境に導入しようと思います。

※導入する際には、以下の記事を参考にさせていただきました。
丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)

環境

Ruby 2.5.3
Rails 5.2.4
MySQL 5.7
MacBook Pro

1 自分のアプリのディレクトリ直下にDockerfileを作成する

Dockerfile
FROM ruby:2.5.3
RUN apt-get update -qq && apt-get install -y vim nodejs default-mysql-client
COPY . /fishingshares
ENV APP_HOME /fishingshares
WORKDIR $APP_HOME
RUN bundle install
ADD . $APP_HOME

2 docker-compose.ymlを作成する

docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    volumes:
      - ./db/mysql_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: root
    ports:
      - "3306:3306"

  web:
    build: .
    command: rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/fishingshares
    ports:
      - "3000:3000"
    depends_on:
      - db
    links:
      - db

3 database.ymlを修正する

database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  socket: /var/lib/mysql/mysql.sock
  host: db  

4 dockerコンテナを起動

terminal
$ docker-compose build #コンテナを立ち上げる(立ち上げるのに少し時間かかります)

$ docker-compose up #コンテナを起動する

コンテナを起動して、ターミナルにこんなログが出てきたらうまくいってます。
42e21645ac242315e5be02351fa4a428.png

5 データベースを構築する

terminal
 docker-compose run web rails db:create #コンテナ上にDBを作成する
$ docker-compose run web rails db:migrate # コンテナ上のDBにマイグレーションファイルを反映させる

6 ブラウザでlocalhost:3000にアクセスする

ff817755ba5c7d950856db45e18d7a97.jpg
アプリケーションが無事に表示されました!

メモ1

コンテナを立ち上げた後に、以下のコマンドできちんとコンテナを止める必要があるみたいです。

terminal
$ docker-compose down

このコマンドでコンテナを止めないと、次にコンテナを起動したときに、ブラウザでうまく表示されません。

コンテナを起動してから、ブラウザに表示されない時は、以下のコマンドを実施してください。

terminal
$ rm tmp/pids/server.pid

このコマンドを実施してから、もう一度コンテナを起動すると、ブラウザにうまく表示されると思います。

メモ2

ローカルからMySQLコンテナに接続するコマンド

terminal
$ mysql -u root -p -h localhost -P 3306 --protocol=tcp

終わりに

dockerについては学習を始めてから、まだ数週間で慣れない点も多いですが、使いこなせるように勉強をしていきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】 関連モデル先の条件で検索する方法(内部結合)

関連モデル先の条件で検索する方法

すでにたくさんの記事が出ていますが、自分がよく使うものを忘れないようにメモしました。

User.joins(:posts).includes(:posts).where(posts: { post_name: 'test1' })

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails の日付や時間の処理を読みやすく書く方法まとめ

Rails には Time 型、Date 型、Duration 型など時間に関わらる様々な型があり、その全てに様々なエイリアスが生えています。
様々なユースケースにおいて、日付演算が読みやすくなるように配慮された結果だと思いますが、時たま難解な組み合わせに出会うことがあります。極端な例ですが、3.days.from_now(Time.current.yesterday) などのような感じです。正直いつの時点を指しているのかパット見でよくわかりません。

実は読みやすさだけを考えた場合、選べる選択肢はそれほど多くありません。
その読みやすくなるパターンについてまとめてみたいと思います。

シフトさせる時間や日数が具体的にわかっている場合

Duration 型に対して対象の日付を渡す方法が最も自然に読めると思います。
具体例としては、

3.days.from_now # 3日後
3.days.ago # 3日後
3.days.after(some_event) # some_event から3日後
3.days.before(some_event) # some_event の3日前

です。sinceuntil などもありますが、これは継続を表したいパターンで明らかに上手くハマる、みたいなケースを除いて使う必要は無いかなと思います。

パターンは以下のとおりです。

起点\表す時点 過去 未来
現在 ago from_now
過去・未来 before after

some_time.in(3.hours) のように Time や Date へのメソッド呼び出しで自然に書く方法も当然あります。しかし過去を表す際に使えるのが ago のみで、これは some_time.ago(3.hours) となってしまい、わからなくはないけれどなんだか不自然、みたいな表記になります。
統一感を出すためにも、常に Duration へのメソッド呼び出しで書くのが良いと思います。

シフト先の時間はわかっているが具体的な差がわかっていない場合

言ってしまえば、Duration オブジェクトを定義しづらい場合です。
この場合はもちろん Date や Time に対して、メソッド呼び出しを行います。

Time.current.noon # 正午
Time.current.beginning_of_week # 週初め

実はこちらの場合では特に言うことがありません。

まとめ

  • Duration を使える場合は Duration へのメソッド呼び出しにする (e.g. 3.days.ago)
  • Duration に対して使うメソッドは基本 ago, from_now, before, after の4つで良い
  • Duration を使えない場合のみ、Time や Date へのメソッド呼び出しを行う
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails アプリで S3 の権限制御を外さずに署名付きURLの署名を省いてファイルアクセスする方法を調べた

はじめに

Rails アプリを用いて、S3 の権限制御を外さずに署名付きURLを省いてアクセスする方法について調べたので、その方法をまとめておきます。

背景

S3 バケット内のファイルに権限制御を入れ、そのファイルに対してアクセスするとき、署名付きURLを用いることがあると思います。こんなURLですね。(クレデンシャルはマスクしています)

https://example.s3-ap-northeast-1.amazonaws.com/foo/bar/piyo.txt/?X-Amz-Expires=-0000000000&X-Amz-Date=0000000000000000&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AAAAAAAAAAAAAAAAAAAA/00000000/ap-northeast-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=0000000000000000000000000000000000000000000000000000000000000000

しかしURLについた署名が、外部ライブラリを使っているときにノイズになることがあります。

例えば本のビューアーや写真のアルバムのようなライブラリを Rails アプリ上で扱っていると、ライブラリでは画像のファイル群をファイル指定で逐一アクセスするというような処理が走ることがあります。

おおよそそのような外部ライブラリは、アプリの同サーバ上のパブリックにアクセスできるディレクトリに画像ファイル群がおかれ、そこに対してアクセスするというケースを想定して作られていることが多いと思います。外部ライブラリ側では画像ファイルについた署名を考慮せずアクセスし、想定通り動かない、ということが起きがちです。

そのため、権限制御を入れたままで、署名付きURLを省いた URL で S3 バケット内のファイルにアクセスできるようにできないかを調べていました。

どうやったか

Rails アプリのルーティングで、S3 バケットのファイルパスをある種プロキシし、実際の S3 バケットの署名付きURLにリダイレクトするみたいな仕組みにしてみました。

以下、やりかたを簡単にまとめます。

ユースケース

以下のファイルに、署名付きURLなしでリクエストしたいとします。

  • バケットURL: https://example.s3-ap-northeast-1.amazonaws.com
  • ファイルパス:foo/bar/piyo.txt

イメージとしては、Rails の URL を https://rails-sample.com としたら、以下URLで S3 ファイルにアクセスできるようにしたいです。

https://rails-sample.com/remote_storages/proxy/foo/bar/piyo.txt

ワイルドカードセグメントを使った Rails のルーティングを作る

まず、ルーティング用のコントローラーを作りますが、その時のルーティング設定では、ワイルドカードセグメントを使います。

ワイルドカードセグメントとは、最初にアスタリスク(*) がついた部分のパラメータのことで、ルーティングのある位置から下のすべての部分にパラメータを展開させるために利用できます。

例えば RemoteStoragesController というコントローラーで#proxyという get メソッドを作るとしたら、以下のようになります。

resources :remote_storages, only: [] do
  collection do
    get 'proxy/*path', to: 'remote_storages#proxy', as: 'proxy'
  end
end

上記により、path には foo/bar/piyo.txt というようなスラッシュ有りのパラメータを渡すことができるようになります。

コントローラー作成

次にコントローラーを作ります。以下では S3 へのアクセスは fog を使っていますが、AWS SDK を使っても良いと思います。

以下では proxy メソッドに渡ってきた path と拡張子 format に応じて、署名付きURLにリダイレクトするという処理を作っています。
検証してみてわかったのですが、ワイルドカードセグメントには .txt のような拡張子は渡ってきませんでした。代わりに、format というパラメータに txt という文字列が入ってくるため、メソッド内で再度ファイルパスを作り直しています。

また、アクセスキーやバケットは Rails.application.secrets で秘匿化すると良さそうです。

class RemoteStoragesController < ApplicationController
  def proxy
    s3_bucket = Fog::Storage.new(
      provider: 'AWS',
      aws_access_key_id: 'xxx',
      aws_secret_access_key: 'xxx',
      region: 'ap-northeast-1'
    ).directories.get('example')

    # 拡張子は *path に入らず :format に入るためここで調整している
    path = "#{params[:path]}.#{params[:format]}"
    redirect_to s3_bucket.files.get_https_url(path, 1.minutes.since.to_i)
  end
end

これによって、以下にアクセスすると、

https://rails-sample.com/remote_storages/proxy/foo/bar/piyo.txt

以下ファイルパスの署名付きURLにリダイレクトするということができます。

https://example.s3-ap-northeast-1.amazonaws.com/foo/bar/piyo.txt

おわりに

今回 Rails ルーティングにワイルドカードセグメントというものがあることを初めて知りました。

ただ、ワイルドカードで渡すとどんな文字列も渡せるようになるため、セキュリティを考慮してある程度のパラメータチェックは入れたほうが良いのかなと思います。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ancestryとjQueryで多階層型カテゴリの入力フォームを段階的に表示させてみた。

何をしたか

ショッピングサイトの検索・購入ページなどでよく見かける
「多階層型カテゴリの入力フォームが順に表示される機能」を
ancestryとjQueryを使って実装してみました。
振り返りを兼ねて記事を書いていきます。

下準備

長いので見たい人だけ展開してください

※コードは載せますがここでは特に説明しません。
※scssは必要ないのですが味気ないので入れました。

terminal.
$ rails _5.2.4_ new ancestry_sample --database=mysql --skip-test --skip-turbolinks --skip-bundle
$ gem install ancestry jquery-rails haml-rails
$ bundle install
$ rails g model category
$ rails g model item
$ rails g controller categories
$ rails g controller items
asesst/javascripts/application.js
rails-ujsより上段にjqueryを追加
//= require jquery
//= require rails-ujs
db/migrate/202***********_create_categories.rb
class CreateCategories < ActiveRecord::Migration[5.2]
  def change
    create_table :categories do |t|
      t.string :name, null: false
      t.timestamps      
    end
    add_index :categories, :name
  end
end
db/migrate/202***********_create_items.rb
class CreateItems < ActiveRecord::Migration[5.2]
  def change
    create_table :items do |t|
      t.references :category, null: false, foreign_key: true
      t.timestamps
    end
  end
end
models/category.rb
class Category < ApplicationRecord
  has_many :items
  has_ancestry
end

models/item.rb
class Item < ApplicationRecord
  belongs_to :category
end

controllers/items_controller.rb
class ItemsController < ApplicationController
  def index
    @items = Item.all
  end

  def new
    @item = Item.new
    @categories = []
    @categories.push(Category.new(id: 0,name:"---"))
    @categories.concat(Category.where(ancestry: nil))
  end

  def create
    Item.create(item_params)
    redirect_to items_path
  end

  private
  def item_params
    params.require(:item).permit(:category_id)
  end
end
config/routes.rb
Rails.application.routes.draw do
  root "items#new"
  resources :items ,only: [:index,:new,:create]
end
views/items/new.html.haml
.items
  =form_with(model:@item,local:true) do |f|
    .items__parent
      = select_tag 'parent', options_for_select(@categories.pluck(:name,:id))
    .items__child
    .items__grandchild
    = f.submit "登録する",class:"button"
views/items/index.html.haml
%table
  %tr 
    %td No.
    %td%td%td-@items.each_with_index do |item,i|
    %tr
      %td 
        = i+1
      %td 
        =item.category.parent.parent.name
      %td
        =item.category.parent.name
      %td
        =item.category.name
%button
  =link_to '戻る',new_item_path,class:"button"
db/seed.rb
ary_tops = [{name: "Tシャツ/カットソー(半袖/袖なし)"},{name: "Tシャツ/カットソー(七分/長袖)"},{name: "その他"}]
ary_jacket = [{name: "テーラードジャケット"},{name: "ノーカラージャケット"},{name: "Gジャン/デニムジャケット"},{name: "その他"}]
ary_shoes = [{name: "スニーカー"},{name: "サンダル"},{name: "その他"}]

lady = Category.create(name: "レディース")
lady_tops = lady.children.create(name: "トップス")
lady_tops.children.create(ary_tops)
lady_jacket = lady.children.create(name: "ジャケット/アウター")
lady_jacket.children.create(ary_jacket)
lady_shoes = lady.children.create(name: "靴")
lady_shoes.children.create(ary_shoes)

men = Category.create(name: "メンズ")
men_tops = men.children.create(name: "トップス")
men_tops.children.create(ary_tops)
men_jacket = men.children.create(name: "ジャケット/アウター")
men_jacket.children.create(ary_jacket)
men_shoes = men.children.create(name: "靴")
men_shoes.children.create(ary_shoes)
terminal.
$ rails db:create
$ rails db:migrate
$ rails db:seed
assets/stylesheets/items.scss
*{
  font-family: Arial,游ゴシック体,YuGothic,メイリオ,Meiryo,sans-serif;
  box-sizing: border-box;
}
%__select-form{
  width: 300px;
  height: 48px;
  background-color: #fff;
  border-radius: 4px;
  font-size: 16px;
  border: 1px solid #ccc;
  color: #222;
}
#parent{
  @extend %__select-form;
}
#child{
  @extend %__select-form;
}
#item_category_id{
  @extend %__select-form;
}
.button{
  width: 300px;
  height: 48px;
  background-color: #f5f5f5;
  border-radius: 5px;
  font-size: 17px;
  transition: 0.2s;
  text-decoration:none;
  line-height: 48px;
  color: #222;
}

下準備ここまで。

いざ、実装

では早速やっていきましょう。
※メインはancestryの値の抽出 → ajax通信のため、js内のhtml作成部分には特に触れません。

親入力欄変更 → 子入力欄表示

  • イベント開始点作成(親カテゴリ"parent"を変更した時にイベント開始)
asesst/javascripts/items.js
$(function() {
  $("#parent").on("change",function(){
  }
}
  • ajax通信に必要な値の抽出(selectタグから選択された項目のvalue値を抽出)
asesst/javascripts/items.js
$(function() {
  $("#parent").on("change",function(){
    var int = document.getElementById("parent").value
  };
}
  • コントローラーへのajax通信処理の記述
asesst/javascripts/items.js
$(function() {
  $("#parent").on("change",function(){
    var int = document.getElementById("parent").value
    $.ajax({
      url: "/categories",
      type: 'GET',
      dataType: 'json',
      data: {id: int}
    })
    .done(function() {
    })
    .fail(function() {
    });
  });
})
  • コントローラー内の処理の記述(ancestryの値が選択した親カテゴリのidと同値のレコードを取得)
controllers/items_categories.rb
  def index
    @categories = Category.where(ancestry: params[:id])
    respond_to do |format|
      format.json
    end
  end
  • routeの記述
config/routes.rb
Rails.application.routes.draw do
  root "items#new"
  resources :items ,only: [:index,:new,:create]
  resources :categories ,only: :index
end
  • json.jbulderの作成・記述
views/categories/index.json.jbuilder
json.array! @categories do |category|
  json.id category.id
  json.name category.name
end
  • 返り値と表示処理
asesst/javascripts/items.js
$(function() {
  function buildHTML(result){
    var html =
      `<option value= ${result.id}>${result.name}</option>`
    return html
  }
#省略#
    .done(function(categories) {
      var insertHTML = `<select name="child" id="child">
                        <option value=0>---</option>`;
      $.each(categories, function(i, category) {
        insertHTML += buildHTML(category)
      });
      insertHTML += `</select>`
      $('.items__child').append(insertHTML);
    })
    .fail(function() {
    });
  });
})

子入力欄変更 → 孫入力欄表示

  • イベント開始点作成(子カテゴリ"child"を変更した時にイベント開始)
asesst/javascripts/items.js
$(function() {
  $("#parent").on("change",function(){
    var int = document.getElementById("parent").value
#中略#
  });
  $("#child").on("change",function(){
  });
})
  • コントローラーでバインドするancestryの値「'親id'/'子id'」を取得、およびコントローラーへのajax通信処理を記述
asesst/javascripts/items.js
#省略#
  $("#child").on("change",function(){
    var intParent = document.getElementById("parent").value
    var intChild = document.getElementById("child").value
    var int = intParent + '/' + intChild
    $.ajax({
      url: "/categories",
      type: 'GET',
      dataType: 'json',
      data: {id: int}
    })
    .done(function() {
    })
    .fail(function() {
    });
  });
})

※ controller.rb、route.rb、json.jbuilderは前述のものを使用するため割愛します

  • 返り値と表示処理
asesst/javascripts/items.js
#省略#
    .done(function(categories) {
      var insertHTML = `<select name="item[category_id]" id="item_category_id">
                        <option value=0>---</option>`;
      $.each(categories, function(i, category) {
        insertHTML += buildHTML(category)
      });
      insertHTML += `</select>`
      $('.items__grangchild').append(insertHTML);
    .fail(function() {
    });
  });
})

完成?

完成!

と言いたいところですが、このままだと親や子を変更する度に入力欄が無限に増殖してしまいます。

スクリーンショット 2020-03-14 1.20.02.png

  • 条件式を追加
    • 「"---"を選択した時」 → 下位の要素をremove
    • 「追加する要素が既に存在する時」 → 要素をreplace
    • それ以外 → append
asesst/javascripts/items.js
##省略##
  $("#parent").on("change",function(){
    var int = document.getElementById("parent").value
    if(int == 0){
      $('#child').remove();
      $('#item_category_id').remove();
    }else{
      $.ajax({
        url: "/categories",
        type: 'GET',
        dataType: 'json',
        data: {id: int}
      })
      .done(function(categories) {
        var insertHTML = `<select name="child" id="child">
                          <option value=0>---</option>`;
        $.each(categories, function(i, category) {
          insertHTML += buildHTML(category)
        });
        insertHTML += `</select>`
        if($('#child').length){
          $('#child').replaceWith(insertHTML);
          $('#item_category_id').remove();
        } else {
          $('.items__child').append(insertHTML);
        };
      })
##中略##
  $(document).on("change","#child",function(){
    var intParent = document.getElementById("parent").value
    var intChild = document.getElementById("child").value
    var int = intParent + '/' + intChild
    if(intChild == 0){
      $('#item_category_id').remove();
    } else {
      $.ajax({
        url: "/categories",
        type: 'GET',
        dataType: 'json',
        data: {id: int}
      })
      .done(function(categories) {
        var insertHTML = `<select name="item[category_id]" id="item_category_id">
                          <option value=0>---</option>`;
        $.each(categories, function(i, category) {
          insertHTML += buildHTML(category)
        });
        insertHTML += `</select>`
        if($('#item_category_id').length){
          $('#item_category_id').replaceWith(insertHTML);
        } else {
          $('.items__grandchild').append(insertHTML);
        };
      })
##後略##

条件式により、無限に増殖することもなくなりました。
スクリーンショット 2020-03-14 1.41.41.png

レコードも無事できました。
スクリーンショット 2020-03-14 1.41.12.png
スクリーンショット 2020-03-14 1.41.24.png

というわけで、
完成です!

注意事項

エラー処理は何もしていないので、保存ができない場合が多々ありますが仕様です。
(孫まで入力しないと、form_withで送信するパラメータを拾えないのでレコード登録できません。)

以上です。

参考にさせていただいた記事

多階層カテゴリでancestryを使ったら便利すぎた

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

『メッセージを投稿』できる最低限のRailsアプリを丁寧に作る(これで初心者完全卒業!)

この記事の基本的な方針

メッセージを投稿して一覧表示するだけの簡単なアプリを作ります。
完成するのは以下のたった4画面のアプリです。

【TOP画面(ログイン前)】     【TOP画面(ログイン後)】
a0.png a4.png
【登録画面】
a1.png
【ログイン画面】
a2.png
【投稿画面】
a3.png
これを丁寧に作って、初心者を卒業しましょう。

想定する読み手

既に一度Railsアプリをチュートリアルやスクール等で作ったことがある方を想定しております。
Mac使用で、パソコンの環境構築は完了していることが前提です。

具体的なコーディング手順

完成品GitHub(masterではなくこの記事のタイトルと同名のブランチなので注意して下さい)

①アプリを立ち上げる

まずターミナルを開きcdコマンドを使い、アプリを立ち上げたい場所に移動します。
今回はデスクトップに作ります。

Terminal
$ cd    #ルートディレクトリに移動する
$ cd Desktop   #デスクトップに移動する

そしてrails newします。

Terminal
$ rails _5.2.4.1_ new <名前> -d myspl

します。今回はのバージョンは5.2.4.1にしました。
MySQLをデータベースとして使用しますので、-d mysqlを付けています。
これでアプリが立ち上げられたと思うので、そのアプリのルートディレクトリに移動します。

Terminal
$ cd <名前>

データベースを作ります。

Terminal
$ rails db:create

(中身が空であってもこの時点でデータベースは作っておかないと、ブラウザでアクセスした時にActiveRecord::NoDatabaseErrorになってしまいます。)

そして

Terminal
rails  s

でサーバーを立ち上げて、ブラウザでlocalhost:3000/にアクセスすると
rails new.jpg
これが表示されます。

続いて、コントローラを作ります。

Terminal
$ rails g controller <名前>

名前は自由ですが一番関連のある(データベースの)テーブル名の複数形にするのが一般的です。
今回はTOP画面にmessage一覧を表示するので、

Terminal
$ rails g controller messages index

今回indexを加えていますが、このように名前の後にアクションを並べれば、そのアクション用のhtml.erbファイルが作られ、ルーティングとコントローラファイルに必要なことが追記されます。アクションはもちろん複数並べてよく、コンマ無しで隙間だけ開けて並べていきます。

ここでエディタを起動します。

まずルーティングを設定をします。上でアクション名を並べていればすでに書かれています。

config/routes.rb
get "messages/index"

取り急ぎ、適当にビューを書きます。

app/views/messages/index.html.erb
<div>こんにちは</div>

ここでブラウザでlocalhost:3000/コントローラ名/indexにアクセスすれば、「こんにちは」が見れるはずです。

通常メインのindex画面をルート画面とするので

config/routes.rb
root "messages#index"

を追記しておきます。localhost:3000でもアクセスできるようになります。

②ログイン機能を作る

ログイン機能は自作することもできますが、deviseというgemを使うことが極めて一般的であるのでこれを今回は使用します。

deviseをインストールします。

Gemfile
gem 'devise'

こちらを追記し、ターミナルで

Terminal
$ bundle install

をし、そしてdeviseを使い始めるにはターミナルで

Terminal
rails g devise:install

をする必要があります。これでconfigディレクトリにファイルができたので、サーバの再起動の必要があります。
さらに、

Terminal
rails g devise <モデル名>

をすれば必要ないくつかのファイルが作られます。この時のモデル名はなんでもいいのですが、userとするのが一般的です。

続いて、上のrails g devise user でマイグレーションファイルも作られているので、

Terminal
$ rails db:migrate

します。これでデータベースにユーザー情報用のテーブルが作られます。

deviseはデフォルトではEmailとPasswordで登録する仕様になっています。EmailやPassword以外を含めて登録する場合やビューやコントローラを編集する場合は別の手続きが必要ですが、今回はこのままでいきます。

この時点でURIをブラウザに直接打ち込めば新規登録画面とログイン画面が表示される段階まで来ていますので確認します。URIは rails routesで確認します。
会員登録はlocalhost:3000/users/sign_upですね。結果は以下です。
in.png
ログインのlocalhost:3000/users/sign_inも同様ですね。
ここで適当に新規登録をしてみて、成功すると自動的にログイン状態になり、『こんにちは』のTOP画面に遷移します。これもログインしたらルート画面に遷移するdeviseのデフォルトの機能です。
これでログイン機能完成です。

③TOP画面作成

さてあとは今『こんにちは』になっているTOP画面を整えて、取り急ぎ会員登録画面ログイン画面TOP画面でぐるぐるできるだけの状態を作ります。
TOP画面は、
ログイン状態であれば、そのユーザーのEmailアドレスとログアウトへのリンク、
非ログイン状態であれば、新規登録へのリンクとログインへのリンクが表示されるようにします。
全体を一度に載せてみます。

app/views/コントローラ名/index.html.erb
<% if user_signed_in? %>
  <%= current_user.email %>
  <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %>
<% else %>
  <%= link_to '新規登録', new_user_registration_path %>
  <%= link_to 'ログイン', new_user_session_path %>
<% end %>

まず、このファイルのように拡張子を.erbは、HTMLファイルにRubyのコードを埋め込めるようになります。
<%= %><% %>が重要ですね。<%= %><% %>の違いは「画面上に表示されるか否か」みたいな嘘教えられたことないですか?<%= %><% %>の違いは「HTMLとして出力するか否か」です。きちんと覚えましょう。
if文で条件分岐されてtrueとなった方のみがHTMLとして出力されるので、もちろんif文自体はHTMLとして出力はしませんので<% %>を使います。
後の説明は省きます。
これで以下のような状態ができていると思います。

【TOP画面(ログイン前)】     【TOP画面(ログイン後)】
a0.png a9.png
【登録画面】
a1.png
【ログイン画面】
a2.png

ここまでが難しい場合は過去記事に戻ってみてください。
次に行きます。

④メッセージテーブルを作る

マイグレーションファイルモデルを作るため、

Terminal
$ rails g model message

を打ちます。
マイグレーションファイルはデータベースのテーブルを作るためのファイルで、モデルはデータベースからデータを出し入れするときの設定やルールを書くファイルです。もう大丈夫ですね?

一般的にモデル名は単数形でコントローラ名は複数形です。

ちなみにこれらのファイルはActive Recordというライブラリを継承していて、そのおかげでデータベースに関することをRubyで記述できます。本来はMySQLデータベースとやりとりするにはSQLという言語で書かなければならないので、Active Recordが翻訳をしてくれているということになります。

続いてマイグレーションファイルを書いて、メッセージテーブルを完成させます。
今回はメッセージの内容とそれを誰が投稿したかの情報を保存できるようにしようと思うので、t.string :messaget.references :user, foreign_key: trueを追記し

db/migrate/2020xxxxxxxxxx_create_messages.rb
class CreateMessages < ActiveRecord::Migration[5.2]
  def change
    create_table :messages do |t|
      t.string :message
      t.references :user, foreign_key: true
      t.timestamps
    end
  end
end

となるようにします。
ちなみにこの時点でブラウザからアクセスしようとするとエラーになります。理由はもちろん

Terminal
$ rails db:migrate

していないからですね。これを済ませたら、メッセージテーブルは完成です。
一応ちゃんと出来ているかデータベースを見に行ってみましょう。
mysql.png
私はSequelProを使っていてこんな感じです。

最後にモデルにテーブル同士の関係を書きましょう。これが無くても投稿できなくはないのですが、投稿した内容を引き出して扱う上で便利なので今済ませてしまいましょう。以下を追記します。

app/models/message.rb
belongs_to :user
app/models/user.rb
has_many :messages

⑤メッセージを投稿できるようにする

必要な編集はルーティングビューコントローラ です。
まずルーティング。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root 'messages#index'
  resources :messages, only: [:index, :new, :create]
end

resourcesを用いた記法に変えました。ここは大丈夫ですね?
次はビュー。newのビューファイルがないので、app/views/messages/ディレクトリに作り以下を作ります。

app/views/messages/new.html.erb
<%= form_with(model: @message, local: true) do |f| %>
  <%= f.text_field :message %>
  <%= f.submit "投稿" %>
<% end %>

<%= %><% %>の違いはなんでしたか?
「HTMLとして出力するか否か」です。
<%= f.text_field :message %><%= f.submit "投稿" %>はともかく、<%= form_with(model: @message, local: true) do |f| %>は表示されているとは言い難いですよねぇ?
画面上はこんな感じになるはずです。
form.png

<%= form_with(model: @message, local: true) do |f| %>はHTMLのform要素に対応しています。画面上に表示はされません。

最後はコントローラです。少し詳しく行きます。

app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  before_action :to_root, except: [:index]
  def index
  end
  def new
    @message = Message.new
  end
  def create
    @message = Message.new(message_params)
    @message.save
    redirect_to root_path
  end
  private
  def message_params
    params.require(:message).permit(:message).merge(user_id: current_user.id)
  end
  def to_root
    redirect_to root_path unless user_signed_in?
  end
end

完成品はこれですが、一からこれを構築してみます。
私であればまず

app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def index
  end
  def new
    @message = Message.new
  end
  def create
    binding.pry
    @message = Message.new(??)
    @message.save
  end
end

ここまで書きます。??の値を知りたいですね。
おっとbinding.pryってなんですか?デバック用のコードです。ここでは詳しい説明は省きます。これを使うために

Gemfile
gem 'pry-rails'

を追記して、

Terminal
$ bundle install

します。
この状態で、rails sでサーバを起動し(すでに起動していたら再起動を忘れずに)、ログイン状態にし、ブラウザでhttp://localhost:3000/messages/newにアクセスし、フォームに適当な文字を入れて、投稿ボタンを押してみます。
するとブラウザが待機状態になり、ターミナルで変数の中身を確認できるようになります。
ここでparamsと打ってみます。

Terminal
> params

paramsはページ遷移時のデータがハッシュ形式で格納されているActionController::Parametersクラスのインスタンスです。

Output
=> <ActionController::Parameters {"utf8"=>"✓", "authenticity_token"=>"BZIJ8UtNrNugQOI94y8iWX5dKc2Z2bZaKxKXm8X/tV1cvJxIPlC/MaL7o0WrfTZccD1dr7IPC8Nlq8vKHiFhdg==", "message"=>{"message"=>"こんにちは"}, "commit"=>"投稿", "controller"=>"messages", "action"=>"create"} permitted: false>

こんな感じで値が取れます。これがインスタンスの情報です。<直後がクラス名で、その先がハッシュ形式になっているので例えば"✓"の値を取得したければ、

Terminal
> params["utf8"]

普通にこうします。
ちなみに、

Terminal
> params[:"utf8"]
Terminal
> params[:utf8]

これらでも取得できます。親切に準備してくれたんですね。
さて自分が投稿した"こんにちは"を取得するには、二重ハッシュになっているので

Terminal
> params[:message][:message]

これですね。

あれ?これ<>はなんなの?値を取得するとき気にしなくていいの?
いいみたいですね。すみません、完璧には理解できていません。

ちなみに、試しに空っぽのMessageモデルのインスタンスを作ってみると

Terminal
>  Message.new
Output
=> #<Message:0x00007fcfcde17130 id: nil, message: nil, user_id: nil, created_at: nil, updated_at: nil>

ActionController::Parametersのインスタンスであれば

Terminal
> ActionController::Parameters.new
Output
=> <ActionController::Parameters {} permitted: false>

ふむふむ、最初の#はなんだろう?この0x00007fcfcde17130はなんだろう?
誰か教えてください(笑)

この待機状態から抜け出すには、コントローラ内のbinding.pryを削除して、ターミナルでexitを打ちます。

さて、保存すべきは投稿されたmessageの内容と、投稿した自分のidです。もうすでに両方ともわかっていますね。
以下のようになります。

app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def index
  end
  def new
    @message = Message.new
  end
  def create
    @message = Message.new({message: params[:message][:message], user_id: current_user.id})
    @message.save
  end
end

current_userはgemのdeviseが用意しているものですね。newの引数はハッシュ形式ですが、{}を省略して@message = Message.new(message: params[:message][:message], user_id: current_user.id)でも構いません。
これで一度投稿してみましょう。
goodafternoon.png
問題なさそうですね。投稿はうまくいくけれどそのままの画面で止まってしまうので、createアクションの中の最後にredirect_to root_pathを加えます。

さてこれで一応できましたが、一般的にはストロングパラメータというものを使ってセキュリティを高めるようです。
以下に直します。

app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def index
  end
  def new
    @message = Message.new
  end
  def create
    @message = Message.new(message_params)
    @message.save
    redirect_to root_path
  end
  private
  def message_params
    params.require(:message).permit(:message).merge(user_id: current_user.id)
  end
end

requireとpermitはActionController::Parametersクラスのメソッドで、mergeはHashクラスのメソッドです。permitでparamsから値をとり、他の情報をデータベースに保存するリストに加える場合はmergeを使うようですね。
private以下ではアクションでないメソッドを定義します。その他の詳しい説明は省きます。

あと、ログインしていない人がlocalhost:3000/messages/newをブラウザに直接入力して投稿することは想定していないですし、current_user.idの値がなくておかしなことになるので、ログインしていない人は投稿画面に入れずにroot画面に遷移するようにします。
コントローラのclass内の最後に

app/controllers/messages_controller.rb
  def to_root
    redirect_to root_path unless user_signed_in?
  end

これを置き、最初に

app/controllers/messages_controller.rb
  before_action :to_root, except: [:index]

これを置きます。詳しい説明は省きます。
これでコントローラの完成品にたどり着きました。

ここら辺でTOP画面の条件分岐のログイン中の側に投稿画面のリンクをのせます。PrefixやURIを調べるのはrails routesでしたね。

app/views/messages/index.html.erb
<% if user_signed_in? %>
  <%= current_user.email %>
  <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %>
  <%= link_to '投稿', new_message_path %>
<% else %>
  <%= link_to '新規登録', new_user_registration_path %>
  <%= link_to 'ログイン', new_user_session_path %>
<% end %>

ここで、ログイン状態で投稿できるてTOP画面に戻ることと、ログアウト状態でlocalhost:3000/messages/newを打ってもTOP画面に遷移することをブラウザで確認しましょう。

この状態で、メッセージを投稿する機能が完成しました。
実際に投稿してデータベースで結果を確認してみましょう。大丈夫ですね。

⑥TOP画面にメッセージ一覧を表示する

TOP画面に、メッセージとそれと一緒に投稿された画像全てとそれらの投稿者のEmailを一覧で表示します。これができたら完成とします。

以下を追記します。

app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  〜省略〜
  def index
    @messages = Message.all
  end
  〜省略〜
end
app/views/messages/index.html.erb
<% if user_signed_in? %>
  <%= current_user.email %>
  <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %>
  <%= link_to '投稿', new_message_path %>
  <% @messages.each do |m| %>
    <div><span style="color: red;"><%= m.user.email %></span><%= m.message %></div>
  <% end %>
<% else %>
  <%= link_to '新規登録', new_user_registration_path %>
  <%= link_to 'ログイン', new_user_session_path %>
<% end %>

まずメッセージと投稿者のEmailを表示してみました。今回はメッセージはログインした人のみに見れるようにしました。
簡単ですね。詳細は省きます。

以上で完成です。

この続きにあたる、『メッセージと複数画像の投稿』ができる最低限のRailsアプリを丁寧に作るもよろしければどうぞ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む