- 投稿日:2020-03-14T23:42:05+09:00
Rails + Vue.js + AjaxでCRUDのサンプルプロジェクト [Hello World]
Vue.js初心者が公式サイトで基礎を学んだ後に作るVue.js + AjaxによるCRUD(作成/読み込み/更新/削除)のサンプルプロジェクトです。
動作確認はChrome、FireFox、Microsoft Edge、IE11です。恐らくマックさんのブラウザでも動作するはずです。
DEMO
https://www.petitmonte.com/rails-demo/vue_crud
ソース一式
https://github.com/TakeshiOkamoto/mpp_vue_crud
※学習用の為、ライセンスはパブリックドメイン
- 投稿日:2020-03-14T21:12:48+09:00
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%>
- 投稿日:2020-03-14T19:05:26+09:00
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.erbHTML: <form action="/posts" method="post"> </form> form_tag: <%= form_tag('/posts', method: :post) do %> <% end %>form_tagのdo〜endは〜の部分がフォームであることを示しています。
記載を変換する際は、<%= %>(erbタグ)やdoを忘れないように気をつけましょう。
- 投稿日:2020-03-14T18:52:39+09:00
EC2にapache + rails環境を構築する
参考サイト
es2の設定
・(デプロイ編①)世界一丁寧なAWS解説。EC2を利用して、RailsアプリをAWSにあげるまで
・Linuxグループ作成方法!groupaddで意外とすぐできる
・usermodコマンドについて詳しくまとめました 【Linuxコマンド集】apacheインストール
・Red Hatpassengerインストール
・phusion/passenger - Githubmysqlインストール
・CentOS7.3 に MySQL5.7 をインストールした時のメモ - Qiitaapache,rbenvインストール参考サイト
・EC2にRails5環境を構築する - Qiitagetの設定
・Git
・GitHubでssh接続する手順~公開鍵・秘密鍵の生成から~ - QiitaWARNING: 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 $ dateapacheインストール
・apacheインストール
$ sudo yum -y update $ sudo yum -y install httpd $ sudo systemctl start httpd $ sudo systemctl status httpd $ sudo systemctl enable httpd.service $ httpd -versionmysqlインストール、設定
・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.comrbenvインストール手順
・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 -lRailsアプリをインスタンス上に配置、立ち上げまで
$ 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-moduleapache設定
$ 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でブラウザーからアクセスするとアプリが立ち上がっていると思います。
- 投稿日:2020-03-14T18:35:49+09:00
マイクロポストにコメント機能をつける
はじめに
ご訪問いただきありがとうございます。初めての投稿ですが学んだことを備忘録的にまとめようと思い記事にしてみました。レベル的には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:textdb/migrate/_create_comments.rbclass 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:migrate2.モデルの関連付け、バリデーションの設定
作成されたモデルはこんな感じです。
app/models/comment.rbclass 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.rbclass User < ApplicationRecord has_many :microposts, dependent: :destroy has_many :comments, dependent: :destroyapp/models/micropost.rbclass Micropost < ApplicationRecord belongs_to :user has_many :comments, dependent: :destroyUserもMicropostも多数のコメントを持てるためhas_manyで1対多の関連付けを行います。dependent: :destroyでUser、Micropostが消えた際に関連するコメントも消えるようにしています。
3Comenntsコントローラーの作成、ルーディングの設定
続いてコントローラーを作成します。今回はcreateとdestroyアクションのみ使用します。
rails g controller commentsapp/controllers/comments_controller.rbclass CommentsController < ApplicationController def create end def destroy end endルーディングの設定。
config/routes.rbresources :microposts, only: [:new, :show, :create, :destroy] do resources :comments, only: [:create, :destroy] endcommetsは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#destroy4対応するビューの作成
今回はMicropostの詳細ページからコメントするような形をとります。
コメントを表示するのはMicroposts/:idになるのでまずはコントローラーにコメント表示とコメントフォーム用にインスタンス変数を作成しておきます。app/controllers/microposts_controller.rbdef 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.rbclass 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 endcreateアクション失敗後に再度同じページを表示するため、コメントを取得してます。(このやり方があっているかはわかりません。)コメントフォーム専用のページを設ければこの取得はいらないです。
コメントを作成する際に、ユーザーの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#destroyapp/contorollers/comments_contoroller.rbclass 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終わりに
初めての記事作成で至らぬ点ばかりだったと思います。
何か間違いや、認識違い等があればアドバイスいただければ幸いです。
それではここまで読んんでいただいきありがとうございました。
- 投稿日:2020-03-14T17:51:07+09:00
Mailer viewでヘルパーメソッドを使用する
Mailer viewでヘルパーを使いたい
郵便番号にハイフンをいれてくれるメソッド
application_helper.rbdef format_zipcode_include_hyphen(zipcode) zipcode.include?('-') ? zipcode : zipcode.insert(3,'-') endmailer viewでいきなり使用するとエラーになった。
application_mailer.rb
application_mailer.rbclass ApplicationMailer < ActionMailer::Base add_template_helper(ApplicationHelper) endと記述する
view
mailer.ftml.slimp 〒#{format_zipcode_include_hyphen(zipcode)}でうまくいきました。
- 投稿日:2020-03-14T17:22:47+09:00
Railsにおけるセッション管理とCookieについて
セッション管理とCookieの働きについてあやふやだったのでまとめてみました
HTTP通信
- クライアントとサーバ間はHTTPというルールでやり取りをする
- クライアントからサーバに対して「リクエスト」が送られる
HTTPはステートレス
- ステートレス=記憶力がない
- たとえばyahooにアクセスするときには、クライアントからサーバへ「yahooの情報ちょうだい」とリクエストを送るが、その直後には送ったことすら覚えていない
- yahooの情報がレスポンスで送られてきて画面表示をしたとしても、送られてきたこと自体を覚えていない
- 要求しっぱなし。もらいっぱなし。
![]()
ステートレスだと、Webアプリでログインする時にどんな問題が発生するか
- ログインできても、ログインしたことすら覚えていない
- そのため、次にアクセスした時には、またログインしなければならない
- ショッピングサイトの場合、カートに入れた商品情報が保持されない
そこでCookieの登場
- ログインできた時点で、サーバでセッションIDをCookieに入れてクライアントへ送り返す
コード例:
session[:user_id] = user.id上記の場合は、user_idを元にセッションIDを生成している
session[:email] = user.emailこんなこともできる
- クライアントはCookieを受け取り、ブラウザに保存する
そのセッション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がクライアント〜サーバ間をグルグルと回ることになる
- 投稿日:2020-03-14T17:11:46+09:00
『非同期でのメッセージ投稿』が理解できる最低限のRailsアプリを丁寧に作る(Ajax苦手の自分とお別れしよう)
この記事の基本的な方針
Ajaxはなんだか難しい!は、勘違いです。一歩一歩きちんと進んでいけば、普通のことだと思えてくるでしょう。
ここではAjax非同期通信を理解するためだけの簡易なアプリを一つ丁寧に作成して、Ajax学習の基礎を完了することを目的としています。この記事は、以下の「登録画面」「ログイン画面」「TOP画面」の3画面の簡単なアプリを元に拡張していきます
【TOP画面(ログイン前)】 【TOP画面(ログイン後)】
![]()
【登録画面】
【ログイン画面】
手を動かしながら読みたいようでしたら、以下でこの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.rbclass 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
します。
これを済ませたら、メッセージテーブルは完成です。一応ちゃんと出来ているかデータベースを見に行ってみましょう。
私はSequelProを使っていてこんな感じです。モデルにテーブル同士の関係を書きましょう。これが無くても投稿できなくはないのですが、投稿した内容を引き出して扱う上で便利なので今済ませてしまいましょう。以下を追記します。
app/models/message.rbbelongs_to :userapp/models/user.rbhas_many :messages②メッセージだけを投稿できるようにする
ルーティング、ビュー、コントローラを編集します。今回はindexにフォームを置くのでnewアクションはなしです。
config/routes.rbRails.application.routes.draw do devise_for :users root 'messages#index' resources :messages, only: [:index, :create] endapp/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
でも
が表示されるはずです。この時点ではこの投稿フォームただの飾りなので、データベースに保存できるよう中身を作ります。
コントローラは以下です。app/controllers/messages_controller.rbclass 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これで一度投稿してみましょう。
③投稿を表示
あとはTOP画面に投稿されたものを全て表示させます。以下を追記します。
app/controllers/messages_controller.rbclass MessagesController < ApplicationController 〜省略〜 def index @messages = Message.all @message = Message.new end 〜省略〜 endapp/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 %>投稿するときに画像の上の丸い矢印が一瞬
×
になって投稿が反映されると思いますが、これなしに反映されるのが非同期通信です。④JQueryを導入し、turbolinksを削除する
turbolinksを削除する理由は、JQueryの動きを阻害する可能性があるからです。(リロードすればjsが動作するのに、リンクで移動すると動作しない、等)
Gemfile
にgem '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/javascripts
にmessages.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属性をみればわかります。
ありました。このように参照される値なので、{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"); }); }) });これで一度投稿してみましょう。データベースを見れば投稿は成功しているのがみて取れます。
以下に同期・非同期の両方でのターミナルでの状態を掲載します。このような違いが出てますね。そしてなぜかコントローラの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.rbrespond_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 %>まとめ
本当に最低限です。
これを、整えて十分な状態にするための続きをまた書きます。
フォローしてお待ち下さい。
- 投稿日:2020-03-14T16:26:11+09:00
コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.11 - Test coding -
はじめに
第11回目ですね。
前回はテストを自動化するためにRSpec、Selenium、Capybaraなどを導入しましたね。今日は今まで作ってきたアプリケーションに対してテストコードをコーディングしていきます。
本当はアプリをコーディングする前にテストをコーディングしてRedのフェーズにするべきなのですが、
まぁ最初なのでご愛嬌ということでGreenの状態から始めましょう。前回のソースコード
前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。
どういうふうにテストコード書いてくの?
ここは人によってやりやすいようにでいいと思うのですが、このハンズオンでは基本的には作りたい機能(ユーザーストーリー)ごとにテストファイルを分けて記述していきます。
例えば、今までだと「サインアップ」とか「サインイン」とかそういうやつです。例えば以下のようにテストシナリオを考えてみます。
1. ユーザーとして、ページにダイレクトアクセスしたい
- 未サインインのユーザーが、トップページにダイレクトアクセスしたとき、トップページが表示されること
- 未サインインのユーザーが、サインアップページにダイレクトアクセスしたとき、サインアップページが表示されること
- 未サインインのユーザーが、サインインページにダイレクトアクセスしたとき、サインインページが表示されること
- 未サインインのユーザーが、ユーザー詳細ページにダイレクトアクセスしたとき、ユーザー詳細ページが表示されること
- サインイン済のユーザーが、トップページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること
- サインイン済のユーザーが、サインアップページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること
- サインイン済のユーザーが、サインインページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること
- サインイン済のユーザーが、ユーザー詳細ページにダイレクトアクセスしたとき、ユーザー詳細ページが表示されること
2. ユーザーとして、ヘッダーリンクからページ遷移できること
- 未サインインのユーザーが、トップページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、トップページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、トップページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
- 未サインインのユーザーは、トップページでヘッダーに「Profile」リンクが存在しないこと
- 未サインインのユーザーは、トップページでヘッダーに「Sign out」リンクが存在しないこと
- 未サインインのユーザーが、サインアップページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、サインアップページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、サインアップページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
- 未サインインのユーザーは、サインアップページでヘッダーに「Profile」リンクが存在しないこと
- 未サインインのユーザーは、サインアップページでヘッダーに「Sign out」リンクが存在しないこと
- 未サインインのユーザーが、サインインページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、サインインページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、サインインページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
- 未サインインのユーザーは、サインインページでヘッダーに「Profile」リンクが存在しないこと
- 未サインインのユーザーは、サインインページでヘッダーに「Sign out」リンクが存在しないこと
- 未サインインのユーザーが、ユーザー詳細ページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、ユーザー詳細ページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、ユーザー詳細ページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
- 未サインインのユーザーは、ユーザー詳細ページでヘッダーに「Profile」リンクが存在しないこと
- 未サインインのユーザーは、ユーザー詳細ページでヘッダーに「Sign out」リンクが存在しないこと
- サインイン済のユーザーが、ユーザー詳細ページでヘッダーのロゴをクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
- サインイン済のユーザーは、ユーザー詳細ページでヘッダーに「Home」リンクが存在しないこと
- サインイン済のユーザーは、ユーザー詳細ページでヘッダーに「Sign in」リンクが存在しないこと
- サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Profile」リンクをクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
- サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Sign out」リンクが存在すること
3. ユーザーとして、サインアップしたい
- 未サインインのユーザーが、トップページで「Sign up now!」ボタンを選択したとき、サインアップページに遷移すること
- サインアップページで「お名前」を入力できること
- サインアップページで「メールアドレス」を入力できること
- サインアップページで「パスワード」を入力できること
- サインアップページで「パスワード」はマスク化されること
- サインアップページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスにチェックをいれたとき、「パスワード」が表示されること
- サインアップページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスのチェックを外したとき、「パスワード」がマスク化されること
- サインアップページで「お名前」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「お名前」未入力のエラーメッセージが表示されること
- サインアップページで「お名前」を51文字以上入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「お名前」文字数超過のエラーメッセージが表示されること
- サインアップページで「メールアドレス」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」未入力のエラーメッセージが表示されること
- サインアップページで「メールアドレス」を256文字以上入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」文字数超過のエラーメッセージが表示されること
- サインアップページで「メールアドレス」を誤ったフォーマットで入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」フォーマットチェックエラーのエラーメッセージが表示されること
- サインアップページで「メールアドレス」がすでに登録済みのメールアドレスを入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」重複のエラーメッセージが表示されること
- サインアップページで「パスワード」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「パスワード」文字数不足のエラーメッセージが表示されること
- サインアップページで「パスワード」を5文字以下で入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「パスワード」文字数不足のエラーメッセージが表示されること
- サインアップページで「お名前」「メールアドレス」「パスワード」を正しく入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは成功し、そのユーザーのユーザー詳細ページにサインイン済状態で遷移すること
- サインアップに成功したユーザーは、遷移後のユーザー詳細ページで自分の入力した「お名前」を確認できること
- サインアップに成功したユーザーは、遷移後のユーザー詳細ページで自分の入力した「メールアドレス」を確認できること
- サインアップに成功したユーザーは、遷移後のユーザー詳細ページでウェルカムメッセージを確認できること
- サインアップに成功したユーザーは、遷移後のユーザー詳細ページをリロードしたとき、ウェルカムメッセージを確認できなくなること
- サインアップページで「登録済みの方はこちら」リンクを選択したとき、サインインページに遷移すること
4. ユーザーとして、サインインしたい
- サインインページで「メールアドレス」を入力できること
- サインインページで「パスワード」を入力できること
- サインインページで「パスワード」はマスク化されること
- サインインページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスにチェックをいれたとき、「パスワード」が表示されること
- サインインページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスのチェックを外したとき、「パスワード」がマスク化されること
- サインインページで「メールアドレス」を入力していないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
- サインインページで「メールアドレス」として登録されていないメールアドレスを入力したユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
- サインインページで「メールアドレス」は正しいが「パスワード」を入力していないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
- サインインページで「メールアドレス」は正しいが「パスワード」が正しくないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
- サインインページで「メールアドレス」「パスワード」に正しい値を入力したユーザーが、「Sign in」ボタンをクリックしたとき、サインイン済状態でそのユーザーのユーザー詳細ページに遷移すること
- サインインに成功したユーザーは、遷移後のユーザー詳細ページでサインイン成功メッセージを確認できること
- サインインに成功したユーザーは、遷移後のユーザー詳細ページをリロードしたとき、サインイン成功メッセージを確認できなくなること
- サインインページで「登録がまだの方はこちら」リンクを選択したとき、サインアップページに遷移すること
5. ユーザーとして、サインアウトしたい
- サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Sign out」リンクをクリックしたとき、未サインイン状態になりトップページに遷移すること
6. ユーザーとして、他のユーザーの情報を閲覧したい
- ユーザーが、存在するユーザーのユーザー詳細ページにアクセスしようとしたとき、そのユーザーの「お名前」「メールアドレス」を確認できること
- ユーザーが、存在しないユーザーのユーザー詳細ページにアクセスしようとしたとき、エラーが発生すること
ざっとあげただけでもこれだけのテストシナリオがあります。
こんなにコード書かなきゃいけないのかよ!と思うかもしれませんが、コードを書かないと少しのリファクタリングの度にこれら全てのテストを手動で行わなければ安心してデプロイできないという修羅の道を選ぶことになります。
今日でテストコードへのハードルを爆下げして気軽にリファクタできるエンジニアをめざしましょう!テストコードを書いていこう
ここから実際にテストコードを書いていきます。
上でナンバリングで章立てしてましたね。それごとにスペックファイルを作って管理します。1. ユーザーとして、ページにダイレクトアクセスしたい
ファイル名は
01_direct_access_spec.rb
にしておきましょう。$ mkdir spec/system/ $ touch spec/system/01_direct_access_spec.rb01_direct_access_spec.rbfeature "ユーザーとして、ページにダイレクトアクセスしたい", 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.create
やUser.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_in
はinput
に文字を入力する操作を実行します。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.rbfeature "ユーザーとして、ヘッダーリンクからページ遷移できること", 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.rbfeature "ユーザーとして、サインアップしたい", 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
を使っています。これはinput
にvalue
属性を取得しています。
value
属性にはinput type="text"
などの場合にはテキストボックスにデフォルトで入力しておきたい文字列を入力しておいたりしますが、Capybaraではvalue
属性を取得することで今入力されている文字列を取得することができます。[:type]
find("#user_password")[:type]のように使っています。Capybaraでは
value
とtext
は要素と.
でつなぐことで取得できるのですが、それ以外の属性は[:attribute_name]
の形式で取得します。[:type]
だとtype
属性を取得してきていることになりますね。
今回はpassword
のtype
属性をjavascriptでtext
とpassword
を切り替えているので、これでチェックができます。text
はマスク化なし、password
はマスク化ありはHTML5の仕様なので、今回のテストではtype
属性が正しく指定されているかを検証しました。check
check
はチェックボックスにチェックする操作です。
今回はcheck :visible_passwordの形式で
id
属性がvisible_password
のチェックボックスにチェックを入れています。uncheck
uncheck
はcheck
の反対でチェックボックスからチェックを外す操作です。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.rbfeature "ユーザーとして、サインインしたい", 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 end5. ユーザーとして、サインアウトしたい
次はサインアウトについてですね。
# touch spec/system/05_sign_out_spec.rbそしてコーディング。これも今までのコードの組み合わせで表現可能ですね。
spec/system/05_sign_out_spec.rbfeature "ユーザーとして、サインアウトしたい", 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.rbfeature "ユーザーとして、他のユーザーの情報を閲覧したい", 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
の後に期待する例外を記述します。
今回は@user2
のid
に+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
- 【Rails】こわくない!TDD/BDD・テスト自動化はじめの一歩ハンズオン! - Qiita
- 使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita
- Class: Capybara::Node::Element — Documentation for jnicklas/capybara (master)
- 【FeatureSpec】404 / 500ページを表示させる - Qiita
Other Hands-on Links
- 投稿日:2020-03-14T16:08:50+09:00
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() { }); }; }); })はい、もーなんか既に分かる人には分かりますね。
原因
原因はこれでした。
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)
今度のターミナルエラーはこれ。
Started GET "/categories?id=1" for ::1 at 2020-03-14 15:36:36 +0900
ActionController::RoutingError (No route matches [GET] "/categories"):原因
config/routes.rbRails.application.routes.draw do root "items#new" resources :items ,only: [:index,:new,:create] endただのroutingの設定忘れでした。
config/routes.rbRails.application.routes.draw do root "items#new" resources :items ,only: [:index,:new,:create] resources :categories ,only: :index end修正します。
第三の404 (Not Found)
ターミナルエラーはこれ。
Started GET "/categories?id=1" for ::1 at 2020-03-14 15:45:21 +0900
ActionController::RoutingError (uninitialized constant CategoriesController):原因
categories_controller.rbが無い・・・だと?
ちゃんとあるじゃ・・・ある・・・あ?
/controllers/category_controller.rbclass 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.rbclass CategoriesController < ApplicationController def index @categories = Category.where(ancestry: params[:id]) respond_to do |format| format.json end end end結果
今回の教訓
①エラーが出たらターミナルをよく見よう。
②routingはちゃんとやっておこう。
③controllerは複数形で作ろう。今回は以上です。
- 投稿日:2020-03-14T15:53:08+09:00
営業マンが独学で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アプリを作ることができる
・スクールに通わない道もある面接にお呼びください
- 投稿日:2020-03-14T15:53:08+09:00
営業マンが独学で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アプリを作ることができる
・スクールに通わない道もある面接にお呼びください
- 投稿日:2020-03-14T15:52:18+09:00
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.410.4になってます。
因みに、これをアンインストールするには
make
した場所で、$ make uninstallとすると消える。
参照
How To Completely Uninstall PostgreSQL
Installing PostgreSQL Client v10 on AWS Amazon Linux (EC2) AMI
make installしたソフトウェアをアンインストールする
- 投稿日:2020-03-14T15:04:18+09:00
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.rbputs 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に共有できればと思います。
ご覧いただきありがとうございました。
- 投稿日:2020-03-14T15:04:18+09:00
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.rbputs 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に共有できればと思います。
ご覧いただきありがとうございました。
- 投稿日:2020-03-14T14:06:29+09:00
『2ページ遷移して会員登録』できる最低限のRailsアプリを丁寧に作る(deviseをウィザード形式に拡張)
この記事の基本的な方針
deviseで実装したログイン機能は特にカスタマイズしなければ、1ページでEmailとパスワードの2項目を入力して登録完了です。
これを2ページに拡張し、1ページ目にEmailとパスワードに加えニックネームを、2ページ目でケータイ番号の入力を求めます。この記事は、以下の「登録画面」「ログイン画面」「TOP画面」の3画面の簡単なアプリを元に拡張していきます。
【TOP画面(ログイン前)】 【TOP画面(ログイン後)】
![]()
【登録画面】
【ログイン画面】
手を動かしながら読みたいようでしたら、以下でこの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ページ目が新たに作られます。
想定する読み手
既に一度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.rbclass 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.rbclass 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.rbclass 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.rb
にvalidates :cellphone ,presence: true
を加えます。バリデーションです。これは簡単ですね。
続いて、アソシエーションです。cellphaneモデルとuserモデルを同時に見てください。app/models/cellphone.rbclass Cellphone < ApplicationRecord validates :cellphone ,presence: true belongs_to :user, optional: true endapp/models/user.rbclass 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?
OutputUser Exists (0.8ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = BINARY 'aaa@aaa' LIMIT 1 ↳ (pry):3 => truetrueって出ました。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.rbRails.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これで完成です。
まだケータイ番号用に桁数を制限などはしていません。余裕があったら挑戦してみてください。
- 投稿日:2020-03-14T11:49:36+09:00
既存の開発環境に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 Pro1 自分のアプリのディレクトリ直下にDockerfileを作成する
DockerfileFROM 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_HOME2 docker-compose.ymlを作成する
docker-compose.ymlversion: '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: - db3 database.ymlを修正する
database.ymldefault: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password socket: /var/lib/mysql/mysql.sock host: db4 dockerコンテナを起動
terminal$ docker-compose build #コンテナを立ち上げる(立ち上げるのに少し時間かかります) $ docker-compose up #コンテナを起動するコンテナを起動して、ターミナルにこんなログが出てきたらうまくいってます。
5 データベースを構築する
terminal$ docker-compose run web rails db:create #コンテナ上にDBを作成する $ docker-compose run web rails db:migrate # コンテナ上のDBにマイグレーションファイルを反映させる6 ブラウザでlocalhost:3000にアクセスする
メモ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については学習を始めてから、まだ数週間で慣れない点も多いですが、使いこなせるように勉強をしていきます。
- 投稿日:2020-03-14T10:48:28+09:00
【Rails】 関連モデル先の条件で検索する方法(内部結合)
- 投稿日:2020-03-14T10:02:45+09:00
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日前です。
since
やuntil
などもありますが、これは継続を表したいパターンで明らかに上手くハマる、みたいなケースを除いて使う必要は無いかなと思います。パターンは以下のとおりです。
起点\表す時点 過去 未来 現在 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 へのメソッド呼び出しを行う
- 投稿日:2020-03-14T08:19:12+09:00
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 ルーティングにワイルドカードセグメントというものがあることを初めて知りました。
ただ、ワイルドカードで渡すとどんな文字列も渡せるようになるため、セキュリティを考慮してある程度のパラメータチェックは入れたほうが良いのかなと思います。
参考
- 投稿日:2020-03-14T02:27:31+09:00
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 itemsasesst/javascripts/application.jsrails-ujsより上段にjqueryを追加 //= require jquery //= require rails-ujsdb/migrate/202***********_create_categories.rbclass 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 enddb/migrate/202***********_create_items.rbclass CreateItems < ActiveRecord::Migration[5.2] def change create_table :items do |t| t.references :category, null: false, foreign_key: true t.timestamps end end endmodels/category.rbclass Category < ApplicationRecord has_many :items has_ancestry endmodels/item.rbclass Item < ApplicationRecord belongs_to :category endcontrollers/items_controller.rbclass 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 endconfig/routes.rbRails.application.routes.draw do root "items#new" resources :items ,only: [:index,:new,:create] endviews/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.rbary_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:seedassets/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.rbdef index @categories = Category.where(ancestry: params[:id]) respond_to do |format| format.json end end
- routeの記述
config/routes.rbRails.application.routes.draw do root "items#new" resources :items ,only: [:index,:new,:create] resources :categories ,only: :index end
- json.jbulderの作成・記述
views/categories/index.json.jbuilderjson.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() { }); }); })完成?
完成!
と言いたいところですが、このままだと親や子を変更する度に入力欄が無限に増殖してしまいます。
- 条件式を追加
- 「"---"を選択した時」 → 下位の要素を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); }; }) ##後略##というわけで、
完成です!注意事項
・エラー処理は何もしていないので、保存ができない場合が多々ありますが仕様です。
(孫まで入力しないと、form_withで送信するパラメータを拾えないのでレコード登録できません。)以上です。
参考にさせていただいた記事
- 投稿日:2020-03-14T01:21:26+09:00
『メッセージを投稿』できる最低限のRailsアプリを丁寧に作る(これで初心者完全卒業!)
この記事の基本的な方針
メッセージを投稿して一覧表示するだけの簡単なアプリを作ります。
完成するのは以下のたった4画面のアプリです。【TOP画面(ログイン前)】 【TOP画面(ログイン後)】
![]()
【登録画面】
【ログイン画面】
【投稿画面】
これを丁寧に作って、初心者を卒業しましょう。想定する読み手
既に一度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
になってしまいます。)そして
Terminalrails sでサーバーを立ち上げて、ブラウザで
localhost:3000/
にアクセスすると
これが表示されます。続いて、コントローラを作ります。
Terminal$ rails g controller <名前>
名前は自由ですが一番関連のある(データベースの)テーブル名の複数形にするのが一般的です。
今回はTOP画面にmessage一覧を表示するので、Terminal$ rails g controller messages index
今回
index
を加えていますが、このように名前の後にアクションを並べれば、そのアクション用のhtml.erbファイルが作られ、ルーティングとコントローラファイルに必要なことが追記されます。アクションはもちろん複数並べてよく、コンマ無しで隙間だけ開けて並べていきます。ここでエディタを起動します。
まずルーティングを設定をします。上でアクション名を並べていればすでに書かれています。
config/routes.rbget "messages/index"取り急ぎ、適当にビューを書きます。
app/views/messages/index.html.erb<div>こんにちは</div>ここでブラウザで
localhost:3000/コントローラ名/index
にアクセスすれば、「こんにちは」が見れるはずです。通常メインのindex画面をルート画面とするので
config/routes.rbroot "messages#index"を追記しておきます。
localhost:3000
でもアクセスできるようになります。②ログイン機能を作る
ログイン機能は自作することもできますが、deviseというgemを使うことが極めて一般的であるのでこれを今回は使用します。
deviseをインストールします。
Gemfilegem 'devise'こちらを追記し、ターミナルで
Terminal$ bundle installをし、そしてdeviseを使い始めるにはターミナルで
Terminalrails g devise:installをする必要があります。これでconfigディレクトリにファイルができたので、サーバの再起動の必要があります。
さらに、Terminalrails 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
ですね。結果は以下です。
ログインの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画面(ログイン後)】
![]()
【登録画面】
【ログイン画面】
ここまでが難しい場合は過去記事に戻ってみてください。
次に行きます。④メッセージテーブルを作る
マイグレーションファイル
とモデル
を作るため、Terminal$ rails g model message
を打ちます。
マイグレーションファイル
はデータベースのテーブルを作るためのファイルで、モデル
はデータベースからデータを出し入れするときの設定やルールを書くファイルです。もう大丈夫ですね?一般的にモデル名は単数形でコントローラ名は複数形です。
ちなみにこれらのファイルは
Active Record
というライブラリを継承していて、そのおかげでデータベースに関することをRubyで記述できます。本来はMySQLデータベースとやりとりするにはSQLという言語で書かなければならないので、Active Record
が翻訳をしてくれているということになります。続いてマイグレーションファイルを書いて、メッセージテーブルを完成させます。
今回はメッセージの内容とそれを誰が投稿したかの情報を保存できるようにしようと思うので、t.string :message
とt.references :user, foreign_key: true
を追記しdb/migrate/2020xxxxxxxxxx_create_messages.rbclass 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
していないからですね。これを済ませたら、メッセージテーブルは完成です。
一応ちゃんと出来ているかデータベースを見に行ってみましょう。
私はSequelProを使っていてこんな感じです。最後にモデルにテーブル同士の関係を書きましょう。これが無くても投稿できなくはないのですが、投稿した内容を引き出して扱う上で便利なので今済ませてしまいましょう。以下を追記します。
app/models/message.rbbelongs_to :userapp/models/user.rbhas_many :messages⑤メッセージを投稿できるようにする
必要な編集は
ルーティング
、ビュー
、コントローラ
です。
まずルーティング。config/routes.rbRails.application.routes.draw do devise_for :users root 'messages#index' resources :messages, only: [:index, :new, :create] endresourcesを用いた記法に変えました。ここは大丈夫ですね?
次はビュー。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_with(model: @message, local: true) do |f| %>
はHTMLのform要素に対応しています。画面上に表示はされません。最後はコントローラです。少し詳しく行きます。
app/controllers/messages_controller.rbclass 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.rbclass MessagesController < ApplicationController def index end def new @message = Message.new end def create binding.pry @message = Message.new(??) @message.save end endここまで書きます。
??
の値を知りたいですね。
おっとbinding.pry
ってなんですか?デバック用のコードです。ここでは詳しい説明は省きます。これを使うためにGemfilegem '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.rbclass 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)
でも構いません。
これで一度投稿してみましょう。
問題なさそうですね。投稿はうまくいくけれどそのままの画面で止まってしまうので、createアクションの中の最後にredirect_to root_path
を加えます。さてこれで一応できましたが、一般的にはストロングパラメータというものを使ってセキュリティを高めるようです。
以下に直します。app/controllers/messages_controller.rbclass 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 endrequireとpermitはActionController::Parametersクラスのメソッドで、mergeはHashクラスのメソッドです。permitでparamsから値をとり、他の情報をデータベースに保存するリストに加える場合はmergeを使うようですね。
private以下ではアクションでないメソッドを定義します。その他の詳しい説明は省きます。あと、ログインしていない人が
localhost:3000/messages/new
をブラウザに直接入力して投稿することは想定していないですし、current_user.id
の値がなくておかしなことになるので、ログインしていない人は投稿画面に入れずにroot画面に遷移するようにします。
コントローラのclass内の最後にapp/controllers/messages_controller.rbdef to_root redirect_to root_path unless user_signed_in? endこれを置き、最初に
app/controllers/messages_controller.rbbefore_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.rbclass MessagesController < ApplicationController 〜省略〜 def index @messages = Message.all end 〜省略〜 endapp/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アプリを丁寧に作るもよろしければどうぞ。