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

【テックエキスパート】Rails勉強会良かった点と今後のリクエスト

◆理由・良かった点
(夜間組は普段コミュケーションが取りづらいので)

・進捗シート上の数字だけでなく、実際の同期メンバーの状況が分かる

・上記により、モチベーションアップや、情報交換が出来る

・教室へ行くのが億劫になってしまった場合に、いつもと違う気分で向かえる

・FocusDay以上に、Focus出来る

・単純にたのしい

◆要望

・勉強会などのイベントを、もう少し頻度を増やして惜しい (出来れば2週間に1回など)

・1時間よりも長めにしたい

・今日の形式以外にも、変わった勉強会や、プレゼン方式?など、
色々なイベントを増やして欲しい (気分が変わり、楽しいという気持ちになれる)

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

取り出した情報(インスタンス変数)をhaml記法を用いてビューで表示させたいです。

初投稿です。初心者なので理解不足点、お見苦しい文面お許し下さい。

コントローラーからモデル経由で取り出したDBのデータを表示させたいのですがうまくいきません。
やりたい事はhaml記法でビューに取り出したデータを一覧表示させたいです。
試したことはデバックを行いデータ@pruducts@imagesは中身があることはわかりました。
hamlの記法においてendがないのでdo抜けやインデントミスが原因だとは思うのですが見つけることができませんでした。回答いただけましたら幸いです。よろしくお願いします。

products_controller.rb

class ProductsController < ApplicationController

  def index
    @products = Product.includes(:images)
    @images = Image.includes(:product)
  end

  def show
  end

  def new
  end

end

content.html.haml

.header-kari
  仮です!!!!!!
.slider
  .slider__content
    = image_tag 'GuitarWolf2019_a.jpg',width:"300px"
  .slider__content
    = image_tag 'gahag-0097732117.jpg',width:"300px"
  .slider__content
    = image_tag 'J19y54-P_400x400.jpg',width:"300px"

.categoryname-sapce
  .categoryname-sapce__title
    人気のカテゴリ
  .categoryname-sapce__center-space
    .categoryname-sapce__center-space__name1
      レディース
    .categoryname-sapce__center-space__name2
      メンズ
    .categoryname-sapce__center-space__name3
      おもちゃ・ホビー・グッズ
    .categoryname-sapce__center-space__name4
      家電・スマホ・カメラ
.center
  .center__space-a
    .center__space-a__new-item1
      .center__space-a__new-item1__title1
        レディース新着アイテム
      .center__space-a__new-item1__link1
        = link_to "もっとみる"
      .center__space-a__new-item1__image-space
        %ul.space
          %li.space__box
            - @product.each do |product|
              - product.images.each do |image|
            -# = image_tag @products.url, class:'space__box__size'
          -# %li.space__box
          -#   = image_tag @products.url, class:'space__box__size'
          -# %li.space__box
          -#   = image_tag @image.url, class:'space__box__size'
          -# %li.space__box
          -#   = image_tag @image.url, class:'space__box__size'
          -# %li.space__box
          -#   5
          -# %li.space__box
          -#   6
          -# %li.space__box
          -#   7
          -# %li.space__box
          -#   8
          -# %li.space__box
          -#   9
          -# %li.space__box
          -#   10

  .center__space-b
    .center__space-b__new-item2
      .center__space-b__new-item2__title2
        メンズ新着アイテム
      .center__space-b__new-item2__link2
        = link_to "もっとみる" , class:'links'
      .center__space-b__new-item2__image-space
        .center__space-b__new-item2__image-space__image
          ここは写真です
        .center__space-b__new-item2__image-space__imagetest
          testimage

  .center__space-c
    .center__space-c__new-item3
      .center__space-c__new-item3__title3
        メンズ新着アイテム
      .center__space-c__new-item3__link3
        = link_to "もっとみる" , class:'links'
      .center__space-c__new-item3__image-space
        .center__space-c__new-item3__image-space__image
          ここは写真です
        .center__space-c__new-item3__image-space__imagetest
          testimage
  .center__space-d
    .center__space-d__new-item4
      .center__space-d__new-item4__title4
        家電・スマホ・カメラ新着アイテム
      .center__space-d__new-item4__link4
        = link_to "もっとみる" , class:'links'
      .center__space-d__new-item4__image-space
        .center__space-d__new-item4__image-space__image
          ここは写真です
        .center__space-d__new-item4__image-space__imagetest
          testimage

  .center__space-e
    .center__space-e__new-item5
      .center__space-e__new-item5__title5
        おもちゃ・ホビー・グッズ新着アイテム
      .center__space-e__new-item5__link5
        = link_to "もっとみる" , class:'links'
      .center__space-e__new-item5__image-space
        .center__space-e__new-item5__image-space__image
          ここは写真です
        .center__space-e__new-item5__image-space__imagetest
          testimage

.categoryname-sapce
  .categoryname-sapce__title
    人気のブランド
  .categoryname-sapce__center-space
    .categoryname-sapce__center-space__name5
      シャネル
    .categoryname-sapce__center-space__name6
      ルイビトン
    .categoryname-sapce__center-space__name7
      シュプリーム
    .categoryname-sapce__center-space__name8
      ナイキ
.center
  .center__space-a
    .center__space-a__new-item1
      .center__space-a__new-item1__title1
        シャネル新着アイテム
      .center__space-a__new-item1__link1
        = link_to "もっとみる"
      .center__space-a__new-item1__image-space
        .center__space-a__new-item1__image-space__image
          ここは写真です
        .center__space-a__new-item1__image-space__imagetest
          testimage
  .center__space-b
    .center__space-b__new-item2
      .center__space-b__new-item2__title2
        ルイビトン新着アイテム
      .center__space-b__new-item2__link2
        = link_to "もっとみる" , class:'links'
      .center__space-b__new-item2__image-space
        .center__space-b__new-item2__image-space__image
          ここは写真です
        .center__space-b__new-item2__image-space__imagetest
          testimage

  .center__space-c
    .center__space-c__new-item3
      .center__space-c__new-item3__title3
        シュプリーム新着アイテム
      .center__space-c__new-item3__link3
        = link_to "もっとみる" , class:'links'
      .center__space-c__new-item3__image-space
        .center__space-c__new-item3__image-space__image
          ここは写真です
        .center__space-c__new-item3__image-space__imagetest
          testimage
  .center__space-d
    .center__space-d__new-item4
      .center__space-d__new-item4__title4
        ナイキ新着アイテム
      .center__space-d__new-item4__link4
        = link_to "もっとみる" , class:'links'
      .center__space-d__new-item4__image-space
        .center__space-d__new-item4__image-space__image
          ここは写真です
        .center__space-d__new-item4__image-space__imagetest
          testimage

  .center__space-e
    .center__space-e__new-item5
      .center__space-e__new-item5__title5
        おもちゃ・ホビー・グッズ新着アイテム
      .center__space-e__new-item5__link5
        = link_to "もっとみる" , class:'links'
      .center__space-e__new-item5__image-space
        .center__space-e__new-item5__image-space__image
          ここは写真です
        .center__space-e__new-item5__image-space__imagetest
          testimage

脂肪.png

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

You don't have bcrypt installed in your application. と言われた

railsチュートリアル第6章の最後の最後のrails testでエラってハマりまくったので備忘録。

環境

  • Rails 5.1.6
  • ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-darwin18]

問題発生

ユーザー作成も認証もできたし一区切り!と、ローカルでのrails testはok
しかしマージしてpushしたあと再びrails testしたところ怒られました

You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install:??

bcryptはinstallしてるしGemfileにもGemfile.lockにもあるのになんでだ・・・?
ググると同じようなエラーでハマったという方がいらっしゃって
rails server再起動とかuninstallしてから再インストールやってみたけど解決せず・・

なんとか解決

  • Gemfileのbcryptの部分をコメントアウトする
  • bundle installする
  • コメントアウト外す
  • 再びbundle install

たまたま見つけたこちらの方の手順でいけました:sob:
rails consoleでコマンド実行時のbcryptに関するエラーとその対処法

根本的な原因がまじでわかんないのでもちょい調べる。

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

【Rails】Colorpickersliderの使い方

開発環境

Rails 5.2.4.1
ruby 2.4.0

前提

devise導入済み
userにcolorカラムを追加
Screen Shot 0002-01-19 at 22.05.41.png

参考URL

色の指定が自由自在!カラーピッカーを出現させるjQueryプラグイン「Bootstrap Color Picker Sliders」
Bootstrap Color Picker Sliders

まとめた経緯

自社内サービスでガントチャートを作成するに当たり、
自分の色をつけることでわかりやすくしようとしました。

使い方

ソースをダウンロードする↓

Githubから
ファイルをダウンロードする

ライブラリを読み込む

jQuery, Bootstrap, tinyColorに依存するので、それらを先に読み込む
(ここは省略します)

表示のしかた

index.haml
...
= f.text_field :color, id: 'hsvflat', autofocus: true, autocomplete: 'color'

jsの初期化

index.haml
<script>
  $("#hsvflat").ColorPickerSliders({
    color: "#F44336",
    placement: 'right',
    sliders: false,
    swatches: ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800', '#FF5722', '#795548', '#9E9E9E', '#607D8B', '#000000', '#FFFFFF'],
    hsvpanel: false
  });
</script>

こんな感じになるよ

Screen Shot 0002-01-19 at 22.17.06.png

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

エンジニアのポートフォリオを作ってみた

以前からポートフォリオサイトを作ってみたかったので今回作ってみました。

https://sssttt-maker.github.io/portfolio/
image.png

github.ioで公開できる最近まで知りませんでした。

デザイン

デザインは、僕の大好きなブランド会社のTOKYO BASEさんのホームページを参考にさせていただきました。
http://www.tokyobase.co.jp/

パララックスサイトみたいなのがかっこいいなと思ったので導入。
セクションが次々に動いていてとても気持ちいいです。

cssで

position: sticky;

というのをsection指定にして入れるだけで簡単に再現できちゃいます。

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

configure IIS with ruby on rails app on windows server

As the title looks strange, RubyOnRails application deploy on Windows Server??? really???

Yes, today I would like to share my experience, how I managed to deploy a Ruby application on the window server(IIS).

Most of the Ruby applications are deploying on a Linux system(cents, ubuntu, AMI) & as the performance perspective, which is also very good and the developer also prefers to develop on Linux systems, because all gems are easily installed on Linux machine in compare to install on the Windows machine.

Let's back to the topic,
In the year 2018, we released Ruby application on the Windows server and It's almost 1.5 years, it's running perfectly without having any big issue.
Regarding the issue, I will discuss it later.

Sharing the System architecture with you all. If you find helpful then please share and leave your comments.
Screen Shot 2020-01-17 at 21.46.10.png

At 1st we started development on the Linux machine, but suddenly we get to knew that we have to release it on the Windows server(IIS). At that time we discussed all the pros and cons with the client and within a team, but it's a God wish to get in a new ride. We accepted this challenge.

Before going deep into it,
I researched how much it is practical to deploy the RubyonRails app on production env(Window Server). I found some articles over the internet but looks like deploy on development env.

Sharing the application details.
・Language: Ruby
・Framework: Rails
・Database: SQL Server 2014
・Platform: Windows Server(2012)
・Application Server: Puma

Now it's time to get it into the windows server.

Step 1: Installation of the Ruby and Rails framework.
This step is quite easy because I already did the same in the past. so 1st step is clear.

Step 2: Run our application on Windows.
As you all know that rails provide the puma server as default, so I used puma and there are some dependency issues but resolved it successfully. And finally, our application is on running mode.

Step 3: IIS Installation.
You can find many articles to Install IIS. As we need to follow some steps to install IIS. So this step is also clear without having any problem.

Step 4: Deploy app on IIS.
Open IIS Manager, then Go to the Site, Right-click on Default Web Site and Add New Application.
As the below image described.
2019-10-09 11_02_17-API-SVR57 - TeamViewer.jpg

Step 5: Configration web.config file.
This step was the most difficult. WHY???
Because 1st you have to create a web.config file with all the basic settings for the Ruby app.
2nd the web.config file location(where should I keep this file, inside the ruby application folders or outside).
3rd is, how to run a puma server in IIS.

I tried many settings, did many changes in the IIS and configuration files.
but no luck.

and finally, the day has come, when I got successfully run a Ruby application in IIS.
yeah....cheers.

1st below is the web.config file for your reference.
2nd, please copy your web.config file under your mail directory. (please check the below image.)
3rd, please check the web.config file below.

Setup and configuration have been completed.
2019-10-09 11_00_54-API-SVR57 - TeamViewer.jpg

Step 6: How to do set up an environment(run an app on development or production mode?).
In apache or Nginx, it is easy to define the environment. but in IIS it was not an easy task.
So the next hurdler was: run our application on development and as well as production env.
here is the web.config for the development & production env.
■ Development Env:

    <?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <handlers>
            <add name="httpplatformhandler" path="*" verb="*" modules="httpPlatformHandler" resourceType="Unspecified" />            
        </handlers>
        <httpPlatform stdoutLogEnabled="true" processesPerApplication="1" stdoutLogFile="log\rails.log" startupTimeLimit="20" processPath="C:\Ruby24-x64\bin\ruby.exe" arguments="-S puma --env development --dir C:\inetpub\wwwroot\live\<your-project-folder-name> -p %HTTP_PLATFORM_PORT% ">
            <environmentVariables>              
              <environmentVariable name="RAILS_ENV" value="development" />        
            </environmentVariables>            
        </httpPlatform>        
    </system.webServer>    
</configuration>

■ Production Env:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <handlers>
            <add name="httpplatformhandler" path="*" verb="*" modules="httpPlatformHandler" resourceType="Unspecified" />            
        </handlers>
        <httpPlatform stdoutLogEnabled="true" processesPerApplication="1" stdoutLogFile="log\rails.log" startupTimeLimit="20" processPath="C:\Ruby24-x64\bin\ruby.exe" arguments="-S puma --env production --dir C:\inetpub\wwwroot\live\<your-project-folder-name> -p %HTTP_PLATFORM_PORT% ">
            <environmentVariables>              
              <environmentVariable name="RAILS_ENV" value="production" />  
              <environmentVariable name="SECRET_KEY_BASE" value="<generate-your-secret-key>" />         
            </environmentVariables>            
        </httpPlatform>        
    </system.webServer>    
</configuration>

Till date, it is running perfectly.
But this was a very interesting and new combination(ruby on rails on IIS).

If you have the same scenario then please go through this article. I hope it will be helpful.

■Problem
In the last, I want to share one problem with you guys,
sometimes the IIS server stopped suddenly, so at that time we need to restart it. Looking for the solution...

If you find anything wrong within this article then please let me know.

Thank you for your valuable​ time.

Enjoy Coding.:smiley::smiley:

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

Rails+WebpackerにVue.jsとReactの両方を入れる

環境:Rails 6.0、Webpacker 4.2、Vue.js 2.6、React 16.12

Webpackerを使ってVue.jsとReactの両方を動かすことに成功したので、メモしておきます。

コピー元の作成

まず、設定ファイルのコピー元とするだけのアプリケーションを作成します。すでにVueを入れたアプリケーションがある場合は、Reactで作ります。

% rails new reactapp --webpack=react

Reactを入れたアプリケーションがある場合は、Vueを指定します。

% rails new vueapp --webpack=vue

yarn add

Vueで作ってあるアプリケーションには、Reactのモジュールをインストールします。prop-typesはWebpackerがデフォルトで入れるものですが、必須ではありません。

% yarn add @babel/preset-react babel-plugin-transform-react-remove-prop-types prop-types react react-dom

Reactで作ってある場合は、Vueのモジュールをインストールします。

% yarn add vue vue-loader vue-template-compiler vue-turbolinks

babel.config.js

ルートにあるbabel.config.jsは、Reactで生成したものを使います。つまり、すでにReactならそのままにし、VueならReact用のbabel.config.jsを上書きします。

config/webpacker.yml

config/webpacker.ymlは、元からあるものを使い、Reactを加える場合は.jsxを、Vueを加える場合は.vueを追加します。

config/webpacker.yml
  extensions:
    - .vue
    - .jsx

config/webpacker/

ReactにVueを加える場合は、Vueで生成したものからconfig/webpacker/loaders/vue.jsをコピーします。

また、config/webpacker/environment.jsはVueのもので上書きします。

両方動かしてみる

次のような感じで両方を動かすapplication.jsを書いて動けば成功です。

app/javascript/packs/application.js
require("@rails/ujs").start();
require("turbolinks").start();

import React from 'react';
import ReactDOM from 'react-dom';

import Vue from 'vue';
import TurbolinksAdapter from 'vue-turbolinks';

import VueApp from '../vueapp';
import ReactApp from '../reactapp';

Vue.use(TurbolinksAdapter);

document.addEventListener('turbolinks:load', () => {
  if($('#vue-app').length) {
    new Vue(VueApp).$mount('#vue-app');
  }

  if($('#react-app').length) {
    ReactDOM.render(React.createElement(ReactApp), $('#react-app')[0]);
  }
});

実際にこんなアプリケーションを作ることはないと思いますが、現実的な使い方として考えられるのは、Railsアプリケーションの中でVueを使う部分とReactを使う部分を分けるケースです。その場合は、app/javascript/packsの下にVue用とReact用のxxx.jsを作り、レイアウトテンプレートを複数作ってjavascript_pack_tagを切り替える、ということになるでしょう。

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

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #17.5 MySQL導入編

こんな人におすすめ

  • プログラミング初心者でポートフォリオの作り方が分からない
  • Rails Tutorialをやってみたが理解することが難しい

前回:#17 VPC環境構築編
次回:準備中

今回の流れ

  1. SQLite3からMySQLに変更する

この記事は、動画を観た時間を記録するアプリのポートフォリオです。
今回はRailsアプリのDBをSQLite3からAWSによるRDSのMySQLに変更します。
RDSはすでにAWSで作成しているものとします(詳しくは#17をご覧ください)。

SQLite3からRDSのMySQLに変更する

RailsアプリのDBをSQLite3からRDSのMySQLに変更します。

  • Gemfileを編集する
  • エンドポイントを確認する
  • database.ymlを編集する
  • RDS(MySQL)に接続する
  • DBを更新する

Gemfileを編集する

Gemfileから使っているDBを削除し、MySQLを入れます。

Gemfile
+ gem 'mysql2'

group :development, :test do
- gem 'sqlite3'
end

group :production do
- gem 'pg'
end
shell
$ bundle install

エンドポイントを確認する

RDSのエンドポイントはDBを設定する際に必要なので、確認します。

AWSにログイン → AWSマネジメントコンソール『RDS』
画面左のダッシュボード『データベース』 → 『自分のアプリ名』 → 下部タブ『接続とセキュリティ』のエンドポイントをコピー

database.ymlを編集する

DBの設定を編集します。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  reconnect: false
  database: lantern(自分のアプリ名)
  pool: 5
  username: ①任意の名前
  password: ②任意のパスワード
  socket: /var/lib/mysql/mysql.sock
  host: コピーしたエンドポイント

development:
  <<: *default
  database: lantern_devlopment(自分のアプリ名)

test:
  <<: *default
  database: lantern_test(自分のアプリ名)

production:
  <<: *default
  database: lantern_production(自分のアプリ名)
$ sudo yum install mysql-devel
$ bundle install
$ sudo /etc/init.d/mysqld restart

RDS(MySQL)に接続する

RDS(MySQL)に接続します。

shell
$ mysql -h コピーしたエンドポイント -P 3306 -u ①任意の名前 -p
Enter password:②任意のパスワード

その後はユーザーを作成し、ルート権限を与えます。

mysql
mysql> create user '①任意の名前'@'コピーしたエンドポイント' identified by '②任意のパスワード';
mysql> grant all on `%`.* to '①任意の名前'@'コピーしたエンドポイント' identified by '②任意のパスワード';

参考になりました↓
MySqlのソケットエラーを解決する
RailsのDBを(初めから| |後から)MySQLに変更する
RDSのMySQLでGRANT文が通らない

DBを更新する

後はいつも通り、DBを更新します。

shell
$ rails db:reset
$ rails db:migrate
$ rails db:seed

以上でMySQLの移行が完了しました。

補足:MySQLが繋がらない場合

VPC内にRDSを設置した場合、外からRDSにアクセスができません。
そのため、EC2からではなくcloud9から接続するには、一時的にセキュリティを切っておくなどの工夫が必要です。

AWSマネジメントコンソール『EC2』 → 画面左のダッシュボード『セキュリティグループ』 → 作成したセキュリティグループを選択 → 下部タブ『インバウンド』 → 『編集』

各ルールを一時的に削除する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

high_voltageで利用規約等の静的ページを作る

high_voltageで静的ページ(プライバシーポリシー、利用規約など)作成

routesとcontrollerの記述不要
headerやfooterを適用したい場合

gem high_voltage

routes.rb
/pages/*id 

を自動生成してくれる。rails routesで確認しよう
routes.rbには記述しないでいい

つまづいたところ

https://github.com/thoughtbot/high_voltage

Disabling routes
The default routes can be completely removed by setting the routes to false:

# config/initializers/high_voltage.rb
HighVoltage.configure do |config|
  config.routes = false
end

※high_voltageのgithub参照

config.routes = false
はhigh_voltageのルートを使えなくするという意味。ここを読まずにコントローラーを作成したりルーティングを記述したりして複雑にしてしまっていた。

high_voltageのgithubやREAD.MEを参考にしてよく読む。

レイアウト指定

initializer.rb
config.layout = 'customer' 
#customer.html.slimがheader footerが書かれているlayout配下のfile

で適用したいレイアウトを指定

views/pages内に

confirm.html.slim
=link_to 'プライバシーポリシー', page_path('privacypolicy')
confirm.html.slim
=link_to '利用規約', page_path('terms')

と記述する

リンクごとにpages/privacyやpages/termsが表示されるようになります。

コピペじゃなくてなぜ動いているかを考えよう(自戒)

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

RailsにおけるAjaxの実装(JavaScriptとjQueryのコード比較)

1. はじめに

RailsでAjaxを実装するには、jQueryを使うのが便利です。
しかし、ブラックボックスが多すぎて、何をやっているのか、いまいちよく分かりません。

そこで、jQueryで記述したAjax処理を、改めてJavaScriptのみを用いて記述し直してみました。
以下、jQuery及びJavaScriptのコードを比較しつつ、使用したメソッドなどについて記録しておきます。

対象とするHTTPメソッドは、POSTメソッドとDELETEメソッドです。
なお、turbolinksは無効にしています。

誤りなどあれば、ご指摘をいただけると幸いです。

実行した環境

  • Rails 5.2.4.1
  • Ruby 2.5.1
  • jQuery 1.12.4
  • jquery-rails 4.3.5
  • (※ turbolinksは無効にしています)

コードの記載にあたっては、次のような簡易ツールを作って、挙動を確認しています。
スクリーンショット 2020-01-18 15.43.03.png
ビューファイルは、次のように記載しています。

index.html.erb
<h4>メモアプリ(サンプル)</h4>
<div class="form">
  <% if user_signed_in? %>
    <%= form_with(model: @note, class: "note_form", id: "note_input", local: true) do |form| %>
      <%= form.text_area :body, class: "note_form-text" %>
      <%= form.submit "メモを登録", class: "note_form-btn" %>
    <% end %>
  <% end %>
</div>
<div class="notes">
  <% @notes.each do |note| %>
    <div class="note" id="note<%= note.id %>">
      <span class="note_name">
        投稿者:<%= note.user.name %>
      </span>
      <% if user_signed_in? && note.user_id == current_user.id %>
        <%= link_to "削除", note_path(note.id) ,class: "note_delete", method: :delete %>
      <% end %>
      <%= simple_format note.body, class: "note_body"%>
    </div>
  <% end %>
</div>

2. 投稿(POSTメソッド)についてのAjaxのコード

まずは、POSTメソッド(投稿)についてのAjax処理です。

2-1. jQueryでAjaxを記載(POSTメソッド)

まずは、一般的な、jQueryを使用したサンプルコードです。
データの受け渡しの状況を確認するため、FormDataは使用していません。

note.js
$(function() {
  // 追加するHTMLデータを生成する関数
  function createHTML(note) {
    let html = `<div class="note" id="note${note.id}">
                  <span class="note_name">投稿者:${note.user_name}</span>
                  <a class="note_delete" rel="nofollow" data-method="delete" href="/notes/${note.id}">削除</a>
                  <p class="note_body">${note.body}</p>
                </div>`
    return html;
  }
  // メモ投稿(POSTメソッド)の処理
  $("#note_input").on("submit", function(e) {
    e.preventDefault();  // デフォルトのイベント(HTMLデータ送信など)を無効にする
    let inputText = $(".note_form-text").val();  // textareaの入力値を取得
    let url = $(this).attr("action");  // action属性のurlを抽出
    $.ajax({
      url: url,  // リクエストを送信するURLを指定
      type: "POST",  // HTTPメソッドを指定(デフォルトはGET)
      data: {  // 送信するデータをハッシュ形式で指定
        "note[body]": inputText
      },
      dataType: "json"  // レスポンスデータをjson形式と指定する
    })
    .done(function(data) {
      let html = createHTML(data);  // 受信したデータ(data)を元に追加するURLを生成(createHTML関数は冒頭で定義)
      $(".notes").append(html);  // 生成したHTMLをappendメソッドでドキュメントに追加
      $(".note_form-text").val("");  // textareaを空にする
    })
    .fail(function() {
      alert("error!");  // 通信に失敗した場合はアラートを表示
    })
    .always(function() {
      $(".note_form-btn").prop("disabled", false);  // submitボタンのdisableを解除
      $(".note_form-btn").removeAttr("data-disable-with");  // submitボタンのdisableを解除(Rails5.0以降はこちらも必要)
    });
  });
});

コントローラーは、次のように記載しています。

notes_controller.rb
class NotesController < ApplicationController
  def index
    @note = Note.new
    @notes = Note.includes(:user)
  end

  def create
    @note = Note.new(note_params)
    if @note.save
      respond_to do |format|
        format.html { redirect_to root_path }
        format.json { render json: {
            body: @note.body,
            user_name: @note.user.name,
            user_id: @note.user_id,
            id: @note.id
          }
        }
      end
    end
  end

  def destroy
    @note = Note.find(params[:id])
    if @note.destroy
      respond_to do |format|
        format.html { redirect_to root_path }
        format.json { render json: { id: params[:id] } }
      end
    end
  end

  private
  def note_params
    params.require(:note).permit(:body).merge(user_id: current_user.id)
  end
end

2-1-1. jQueryにおけるAjax通信の基本的な記載項目

必要ないかもしれませんが、備忘としてjQueryのAjaxについても、ざっと概要のみ紹介しておきます。
不要な方は読み飛ばしてください。

2-1-1-1. $.ajax({})

jQueryでAjax通信をする場合に、送信データとして記載する内容は次のとおりです。

項目 内容
url リクエストを送信するURLを指定(formタグのaction属性にあるURLを指定します)
type HTTPメソッドを指定(デフォルトはGET)
data 送信するデータをハッシュ形式で指定
dataType サーバから返信されるデータの形式を指定

●参考サイト(本記事の全般で参考にさせていただいています)
jQuery逆引きリファレンス:一般的なAjax通信を実装するには?
JavaScript 日本語リファレンス jQuery $.ajax()

2-1-1-2. .done(function(data){})

.doneの部分には、通信が成功した場合の処理を記載します。
引数のdataには、受信したデータが格納されています。

2-1-1-3. .fail(function(){})

.failの部分には、通信が失敗した場合の処理を記載します。

2-1-1-4. .always(function(){})

.alwaysの部分には、通信の成功、失敗に関わらず行う処理を記載します。

ここには、「submitボタンが2回目以降押せなくなる」というRailsの仕様を解除するためのコードを記載しています。

sample.js
.always(function() {
  $(".note_form-btn").prop("disabled", false);  // submitボタンのdisableを解除
  $(".note_form-btn").removeAttr("data-disable-with");  // submitボタンのdisableを解除(Rails5.0以降はこちらも必要)
})

submitボタンのdisableを解除するためには、上記の1つ目の処理のように$(セレクタ名).prop("disabled", false);と記載します。

ただし、Railsの新しいバージョン(Rails5.0以降)では、上記のコードを記載しただけでは、submitボタンの無効が解除されません。
その理由は、ボタンの2度押しを回避するために、"data-disable-with"という属性が自動で追加される仕様となっているためです。

これについては、上記の2つ目のように、"data-disable-with"属性自体を消去するコードを追加することで(参照記事:[Rails5] submitタグでAjaxを使うと2回目以降に無効になる)submitボタンがロックされなくなります。

●参考サイト
Rails で JavaScript を使用する / 3.4 入力を自動で無効にする

2-1-2. セキュリティトークンについて

jQueryでAjaxを記述する場合は、本来、セキュリティトークンは意識しなくとも良いのですが、JavaScriptでコードを記載する場合に必要になりますので先に触れておきます。

Railsにおいて、POSTメソッド等でサーバにリクエストを送信する場合は、サーバからクライアントあてに発行されているセキュリティトークン(ワンタイムパスワードのようなもの)を送信データと共に送らなければ、リクエストは受け付けられない仕様になっています。

しかし、今回のサンプルコードでは、特にセキュリティトークンの送信について記述していません。

下のようにログを確認しても、パラメータとしては、Parameters: {"note"=>{"body"=>"こんにちは!"}}というように、1つのみのデータしか送られていません。

log/development.log
Started POST "/notes" for ::1 at 2020-01-19 12:30:52 +0900
Processing by NotesController#create as JSON
  Parameters: {"note"=>{"body"=>"こんにちは!"}}

このように、明示的にセキュリティートークンの送信を指定しなくても、リクエストは正常に処理されています。

なぜこのようにリクエストが正常に成立するかと言うと、jQueryで処理する場合は、サーバにリクエストを送信する際のリクエストヘッダに、自動的にセキュリティトークンが付加されているからでした。
実際のリクエストヘッダは、次のようになっています。上から7行目のX-CSRF-Token:のところがセキュリティトークンに当たります。

RequestHeaders
POST /notes HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Content-Length: 69
Accept: application/json, text/javascript, */*; q=0.01
Origin: http://localhost:3000
X-CSRF-Token: gHVpLnJtIFjVzl5VsSESArnw7+sNU67AenEoa29eALi3s9EPl+O5VbM8TnE1QgrA1PbS4Avhdg9atdz2rDcJhg==
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
(以下略)

リクエストヘッダの確認方法は、「HTTPレスポンスヘッダ・リクエストヘッダ情報をウェブブラウザで表示・確認する方法」でご確認ください。

●参考サイト
Rails で JavaScript を使用する / 6 AjaxのCSRF(Cross-Site Request Forgery)トークン

2-1-3. (参考)FormDataを使用した場合のコード

参考のため、FormDataを使用した場合の記載も掲載しておきます。
省略した部分は、先に記載している「FormDataを使用しない場合」と同様です。

note.js
$(function() {
  // (省略)
  $("#note_input").on("submit", function(e) {
    e.preventDefault();
    let formData = new FormData(this);  // FormDataを作成
    let url = $(this).attr("action");
    $.ajax({
      url: url,
      type: "POST",
      data: formData,  //FormDataをそのまま渡せば良い(必要な"note[body]"と"authenticity_token"を含む)
      dataType: "json",
      processData: false,  //FormDataを使用した場合に必要となる
      contentType: false  //FormDataを使用した場合に必要となる
    })
    .done(function(data) {
      // (以下省略)
    });
  });
});

FormDataを使用した場合に、どのようなデータが送信されているかを確認しておきます。

下記のログに表示されているパラメータには、3つのデータが含まれています。
これは、formタグの中に記載された内容を、そのまま送信しているようです。

2つ目にある"authenticity_token"は、formタグ内に含まれているセキュリティトークンです。
中身の文字列はこの次に掲載しているリクエストヘッダと全く同じです。

log/development.log
Started POST "/notes" for ::1 at 2020-01-19 13:27:54 +0900
Processing by NotesController#create as JSON
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"FcqVGUaR69OzydUk9hwJRleLmX0z6auwkgd+NSy1RXwiDC04ox9y3tU7xQByfxGEOo2kdjVbc3+yw4qo79xMQg==", "note"=>{"body"=>"こんばんは!"}}

リクエストヘッダでも、7行目のところにX-CSRF-Token:とあり、セキュリティトークンが付加されています。

RequestHeaders
POST /notes HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Content-Length: 446
Accept: application/json, text/javascript, */*; q=0.01
Origin: http://localhost:3000
X-CSRF-Token: FcqVGUaR69OzydUk9hwJRleLmX0z6auwkgd+NSy1RXwiDC04ox9y3tU7xQByfxGEOo2kdjVbc3+yw4qo79xMQg==
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryPiYVoZWTHjJdIdbK
(以下略)

FormDataでは、以上のようにパラメータ及びリクエストヘッダの両方にセキュリティトークンが付加されています。
実際の挙動を確認しましたところ、どちらか一方にセキュリティトークンがあれば、リクエストは成功するようです。

2-2. JavaScriptでAjaxを記載(POSTメソッド)

前置きが長すぎましたが、ここからが本題です。
JavaScriptでPOSTメソッドを実装すると次のようなコードとなります。変に長いですが。

note.js
window.addEventListener("load", function() {
  let token = document.getElementsByName("csrf-token")[0].content; //セキュリティトークンの取得
  // 追加するHTMLデータを生成する関数
  function createHTML(note) {
    // 必要となるタグ及びテキストノードを生成
    let divElm = document.createElement("div"); 
    let spanElm = document.createElement("span");
    let aElm = document.createElement("a");
    let pElm = document.createElement("p");
    let nameText = document.createTextNode("投稿者:" + note.user_name);
    let deleteText = document.createTextNode(" 削除");
    let bodyText = document.createTextNode(note.body);
    // 各タグに属性・属性値を付加
    divElm.setAttribute("class", "note");
    divElm.setAttribute("id", "note" + note.id);
    spanElm.setAttribute("class", "note_name");
    aElm.setAttribute("class", "note_delete");
    aElm.setAttribute("rel", "nofollow");
    aElm.setAttribute("data-method", "delete");
    aElm.setAttribute("href", "/notes/" + note.id);
    // aElm.addEventListener("click", function(e) { deleteHTMLEvent(e, aElm) }, false); //ここは削除メソッド実装時に使用
    pElm.setAttribute("class", "note_body");
    // ノードの結合(各子要素にテキストを追加)
    spanElm.appendChild(nameText);
    aElm.appendChild(deleteText);
    pElm.appendChild(bodyText);
    // ノードの結合(親要素に子要素を追加)
    divElm.appendChild(spanElm);
    divElm.appendChild(aElm);
    divElm.appendChild(pElm);
    return divElm;
  };
  // メモ投稿(POSTメソッド)の処理
  document.getElementById("note_input").addEventListener("submit", function(e) {
    e.preventDefault();  // デフォルトのイベント(HTMLデータ送信など)を無効にする
    //送信データの生成
    let inputText = document.getElementsByClassName("note_form-text")[0].value;  // textareaの入力値を取得
    let url = document.getElementById("note_input").getAttribute("action") + ".json";  // 末尾に[.json]を追加して送信データがjson形式であることを指定
    let hashData = {  // 送信するデータをハッシュ形式で指定
      note: {body: inputText}  // 入力テキストを送信
      // authenticity_token: token  // セキュリティトークンの送信(ここから送信することも可能)
    };
    let data = JSON.stringify(hashData); // 送信用のjson形式に変換
    // Ajax通信を実行
    let xmlHR = new XMLHttpRequest();  // XMLHttpRequestオブジェクトの作成
    xmlHR.open("POST", url, true);  // open(HTTPメソッド, URL, 非同期通信[true:default]か同期通信[false]か)
    xmlHR.responseType = "json";  // レスポンスデータをjson形式と指定
    xmlHR.setRequestHeader("Content-Type", "application/json");  // リクエストヘッダーを追加(HTTP通信でJSONを送る際のルール)
    xmlHR.setRequestHeader("X-CSRF-Token", token);  // リクエストヘッダーを追加(セキュリティトークンの追加)
    xmlHR.send(data);  // sendメソッドでサーバに送信
    // 受信したデータの処理
    xmlHR.onreadystatechange = function() {
      if (xmlHR.readyState === 4) {  // readyStateが4になればデータの読込み完了
        if (xmlHR.status === 200) {  // statusが200の場合はリクエストが成功
          let note = xmlHR.response;  // 受信したjsonデータを変数noteに格納
          let html = createHTML(note);  // 受信データを元にHTMLを作成
          document.getElementsByClassName("notes")[0].appendChild(html);  // 作成したHTMLをドキュメントに追加
          document.getElementsByClassName("note_form-text")[0].value = "";  // テキストエリアを空白に戻す
        } else {  // statusが200以外の場合はリクエストが適切でなかったとしてエラー表示
          alert("error");
        }
        document.getElementsByClassName("note_form-btn")[0].disabled = false;  // submitボタンのdisableを解除
        document.getElementsByClassName("note_form-btn")[0].removeAttribute("data-disable-with");  // submitボタンのdisableを解除(Rails5.0以降はこちらも必要)
      }
    };
  }, false);
});

●参考サイト
基本的なことは、全般的に次の記事を参考にさせていただきました。
JavascriptのAjaxについての基本まとめ

2-2-1. ノード(Elementオブジェクト)の取得等

最初に、jQueryとJavaScriptの記述の違いとして、基本的なところを記録しておきます。

上記の、JavaScriptのサンプルコードでは、ノード(Elementオブジェクト)の取得等において、次のようなメソッドを使用しています。
対比が分かりやすいように、表の右側には、対応するjQueryの記述を入れました。

項番 内容 JavaScript jQueryt
1 ID名から取得 document.getElementById("note-id") $("#note-id")
2 class名から取得 document.getElementsByClassName("note_form-text")[0] $(".note_form-text")
3 name名から取得 document.getElementsByName("csrf-token")[0] $("*[name=csrf-token]")
4 属性値を取得(getAttributeメソッド) document.getElementById("note_input").getAttribute("action") $("#note_input").attr("action")
5 ページ全体の読込みができたことの確認 window.addEventListener("load", function() {}) $(function() {})

表中の項番1から3は、要素(Elementオブジェクト)の取得で使用するメソッドです。

class名、name名を指定してElementオブジェクトを取得する場合、jQueryでは最初に見つかったElementオブジェクトを返しますが、JavaScriptでは該当する全ての要素を返すため、要素を指定する番号[0]を付すことが必要です。

一方、id名を指定してElementオブジェクトを取得する場合は、結果は1つのみ(idは原則として1つしか使用されない)ですので、要素の番号指定は不要です。
idによる指定では、getElementByIdというように、[Element]と単数形になっていることからも、取得するノード数が1つであることが分かります。

2-2-2. HTMLデータの生成

ページに追加するHTML作成において使用しているメソッドについて、jQueryと比較しつつ簡単にまとめておきます。

例として、投稿者名とコメントを表示するだけの簡単なHTMLを書いてみます。
まず、jQueryでは、次のように、バッククオートでHTML文を囲むことで、簡単にHTMLの生成ができます。

sample.js
let html = `<div class="comment">
              <p class="comment_name">投稿者:山田</p>
              こんにちは!
            </div>`

これと同じ内容を、JavaScriptで記述すると、次のようになります。

sample.js
    let divElm = document.createElement("div");  // divタグを生成: <div></div>
    divElm.setAttribute("class", "comment");  // class名を追加: <div class="comment"></div>

    let pElm = document.createElement("p");  // pタグを生成: <p></p>
    pElm.setAttribute("class", "omment_name");  // class名を追加: <p class="comment_name"></p>
    pElm.appendChild(document.createTextNode("投稿者:山田"));  // テキストノードを追加: <p class="comment_name">投稿者:山田</p>

    let commentText = document.createTextNode("こんにちは!");  // テキストノードを生成: "こんにちは!"
    divElm.appendChild(pElm);  // divタグ(divElm)の子要素にpタグ(pElm)を追加
    divElm.appendChild(commentText); // divタグ(divElm)の子要素にテキストノード("こんにちは!")を追加

    let html = divElm;

JavaScriptにおけるHTML生成のためのメソッドは、主に次のとおりです。
これらを組み合わせて、HTMLを生成していくということになります(もっと簡単な方法があれば良いのですが)。

項番 項目 構文 具体例
1 要素(タグ)の生成 document.createElement(タグ名) document.createElement("div")
2 テキストノードの生成 document.createTextNode(テキスト文) document.createTextNode("こんにちは!")
3 属性の追加 要素ノード.setAttribute(属性名, 属性値) divElm.setAttribute("class", "comment")
4 子要素として追加 親ノード.appendChild(子ノード) divElm.appendChild(pElm)

●参考サイト
【JavaScript入門】appendと何が違う?appendChild徹底解説

2-2-3. サーバへのリクエスト送信

sample.js
let xmlHR = new XMLHttpRequest();  // XMLHttpRequestオブジェクトの作成
xmlHR.open("POST", url, true);  // open(HTTPメソッド, URL, 非同期通信[true:default]か同期通信[false]か)
xmlHR.responseType = "json";  // レスポンスデータをjson形式と指定
xmlHR.setRequestHeader("Content-Type", "application/json");  // リクエストヘッダーを追加(HTTP通信でJSONを送る際のルール)
xmlHR.setRequestHeader("X-CSRF-Token", token);  // リクエストヘッダーを追加(セキュリティトークンの追加)
xmlHR.send(data);  // sendメソッドでサーバに送信

サーバにリクエストを送信するために使用しているオブジェクト及びメソッドは、次のとおりです。

項番 項目 内容
1 XMLHttpRequestオブジェクト サーバとの通信を行うためのオブジェクト(API)。このオブジェクトにより、ページ全体の更新をすることなくサーバとの送受信を行うことができる。
2 XMLHttpRequest.open()メソッド XMLHttpRequestオブジェクトのメソッド。リクエストの作成に使用する。
3 XMLHttpRequest.responseTypeメソッド XMLHttpRequestオブジェクトのメソッド。サーバから返信されるデータの形式を指定。
4 XMLHttpRequest.setRequestHeaderメソッド XMLHttpRequestオブジェクトのメソッド。リクエストヘッダーを追加する。
5 XMLHttpRequest.send()メソッド XMLHttpRequestオブジェクトのメソッド。引数に指定したデータをサーバに送信する。

2-2-3-1. XMLHttpRequestオブジェクト

まず、1つめのXMLHttpRequestオブジェクトですが、これについては、次の説明が直感的に分かりやすいです。

XMLHttpRequestはブラウザ上でサーバーとHTTP通信を行うためのAPIです。名前にXMLが付いていますがXMLに限ったものではなく,HTTPリクエストを投げてテキスト形式かDOMノードでレスポンスを受け取る機能を持っています。(これでできる! クロスブラウザJavaScript入門

このXMLHttpRequestオブジェクトを介して、サーバへのリクエストを作成してデータのやり取りを行うことになります。

●参考サイト
MDN Web Docs / XMLHttpRequest

2-2-3-2. XMLHttpRequest.open()メソッド

構文:XMLHttpRequest.open(HTTPメソッド, URL, 非同期通信か同期通信か)
このopen()メソッドは、リクエストを作成する場合に使用します。
第1引数でHTTPメソッド、第2引数でURL、第3引数で非同期通信[true]か同期通信[false]かを指定します。第3引数はデフォルトがtrueなので、一般的には省略されているようです。

更に、第4引数(ユーザ名)、第5引数(パスワード)もありますので、詳しくは「MDN Web Docs / XMLHttpRequest.open()」を参照してください。

なお、JSON形式で送信する場合、URLの指定において、末尾に.JSONを付すことが必要です。これにより、送信データがjson形式であることが指定されます。

2-2-3-3. XMLHttpRequest.responseTypeメソッド

responseTypeメソッドでは、サーバから返信されるデータの形式を指定します。
ここでは、返信されるデータ形式として"json"を指定していますが、そのほかの形式として、"arraybuffer""blob""document""text"などの形式の指定ができます(詳細は「MDN Web Docs - XMLHttpRequest.responseType」を参照してください)。

2-2-3-4. XMLHttpRequest.setRequestHeaderメソッド

これは、HTTP通信におけるリクエストヘッダを追加するメソッドです(参考「MDN Web Docs / XMLHttpRequest.setRequestHeader()」)。

sample.js
xmlHR.setRequestHeader("Content-Type", "application/json");  // リクエストヘッダーを追加(HTTP通信でJSONを送る際のルール)
xmlHR.setRequestHeader("X-CSRF-Token", token);  // リクエストヘッダーを追加(セキュリティトークンの追加)

JSON形式のリクエストを送信する場合には、上記1行目のように、Content-Typeヘッダに"application/json"(又は"text/json")というMIMEタイプの指定が必要となります(引用サイト:JSON リクエストとレスポンス)。

2行目のコードは、セキュリティトークンをリクエストヘッダに追加する処理です。
このセキュリティトークンの詳細は、次の項「2-2-4. セキュリティトークンの送信」に書いています。

●参考サイト
XMLHttpRequestでJSONをPOST
ajaxでpostするときに必要なリクエストヘッダ

2-2-3-5. XMLHttpRequest.send()メソッド

send()メソッドは、リクエストをサーバに送信するメソッドです。送信するデータを引数に指定することができます(参考「MDN Web Docs / XMLHttpRequest.send()」)。
関係部分を抜粋すると、次のようになっています。

sample.js
let hashData = {  // 送信するデータをハッシュ形式で指定
  note: {body: inputText}  // 入力テキストを送信
};
let data = JSON.stringify(hashData); // 送信用のjson形式に変換
xmlHR.send(data);  // sendメソッドでサーバにリクエストを送信

上記のコードでは、まず、ハッシュオブジェクトとしてデータを作成し、これを、JSON.stringifyメソッドを使用して、JSON文字列に変換しています(参考「MDN Web Docs / JSON.stringify()」)。
これにより、引数に指定したデータをJSON形式として送信することができます。

2-2-4. セキュリティトークンの送信

JavaScriptでAjaxを実装する場合には、セキュリティトークン(サーバから発行されるワンタイムパスワードのようなもの)を意識する必要があります。

自動的にセキュリティトークンが送信されるjQueryの場合とは異なり(前出の「2-1-2. セキュリティトークンについて」を参照)、JavaScriptでAjaxを記述する場合には、サーバへのHTTPリクエストにセキュリティトークンを付加する処理が必要となります。
この処理を書かなければ、POSTメソッドやDELETEメソッドによるリクエストを、サーバ側で受け付けることができません。

サンプルコードから、セキュリティトークンに関する部分を抜粋すると、次のようになっています。

sample.js
let token = document.getElementsByName("csrf-token")[0].content; //セキュリティトークンの取得
// (中略)
let xmlHR = new XMLHttpRequest();  // XMLHttpRequestオブジェクトの作成
// (中略)
xmlHR.setRequestHeader("X-CSRF-Token", token);  // リクエストヘッダーを追加(セキュリティトークンの追加)

上記の最初の行の処理で、ページのHTMLヘッダからセキュリティトークンの情報を抽出し、最後の行で、リクエストヘッダにセキュリティトークンを追加する処理を行っています。

なお、セキュリティトークンは、ページのHTMLを見れば確認できます。

(HTMLのヘッダ部分に表示されているセキュリティトークン)

sample.html
<head>
  <title>DemoApp</title>
  <meta name="csrf-param" content="authenticity_token">
  <meta name="csrf-token" content="RqHWJuRG/kobdPDTVdQEaJAl7hwpmfptmuJbamK/+5JxZ24HAchnR32G4PfRtxyq/SPTFy8rIqK6Jq/3odbyrA==">
  <!-- 略 -->
</head>

ヘッダ部分の3行目にある<meta name="csrf-token" content="RqHWJuRG/kobdPDTVdQEaJAl7hwpmfptmuJbamK/+5JxZ24HAchnR32G4PfRtxyq/SPTFy8rIqK6Jq/3odbyrA==">がセキュリティトークンです。
この、contentの属性値を拾ってサーバに送信していると言うことになります。

また、Formタグ(下記)の中にも、name="authenticity_token"の属性valueに、セキュリティートークンが格納されています。こちらからデータを拾って送信しても同様の結果を得ることができます。

sample.html
<form id="note_input" class="note_form" action="/notes" accept-charset="UTF-8" method="post">
  <input name="utf8" type="hidden" value="✓">
  <input type="hidden" name="authenticity_token" value="RqHWJuRG/kobdPDTVdQEaJAl7hwpmfptmuJbamK/+5JxZ24HAchnR32G4PfRtxyq/SPTFy8rIqK6Jq/3odbyrA==">
  <textarea class="note_form-text" name="note[body]" id="note_body"></textarea>
  <input type="submit" name="commit" value="メモを登録" class="note_form-btn" data-disable-with="メモを登録">
</form>

詳細は、Rails で JavaScript を使用する / 6 AjaxのCSRF(Cross-Site Request Forgery)トークンを参照してください。

2-2-5. サーバからのレスポンスの受信

2-2-5-1. レスポンス処理の構造

JavaScriptにおいては、次のように記述することで、リクエストの成否に合わせて処理が実行されます。

(JavaScriptにおけるレスポンスの処理)

sample.js
xmlHR.onreadystatechange = function() {
  if (xmlHR.readyState === 4) {  // readyStateが4になればデータの読込み完了
    if (xmlHR.status === 200) {  // statusが200の場合はリクエストが成功
      // (1) リクエストが成功した場合に行う処理
    } else {  // statusが200以外の場合はリクエストが適切でなかったとしてエラー表示
      // (2) リクエストが成功しなかった場合に行う処理
    }
    // (3) リクエストの成功・失敗に関わらず行う処理
  }
};

下記は、jQueryでのコード記載です。JavaScriptにおける(1)から(3)の処理とほぼ同じ形で対応しています(細かい違いはあるかもしれませんが)。

(jQueryにおけるレスポンスの処理)

sample.js
.done(function(data) {
  // (1) リクエストが成功した場合に行う処理
})
.fail(function() {
  // (2) リクエストが成功しなかった場合に行う処理
})
.always(function() {
  // (3) リクエストの成功・失敗に関わらず行う処理
});

2-2-5-2. レスポンスデータの取得について

JavaScriptのレスポンスデータは、次のように取得することができます。
(JavaScriptにおけるレスポンスデータの取得)

sample.js
if (xmlHR.status === 200) {  // statusが200の場合はリクエストが成功
  let note = xmlHR.response;  // XMLHttpRequest.responseメソッドからレスポンスデータを取得できる
  // (略)
}

対比として、jQueryにおけるレスポンスデータの取得も以下に記載しておきます。
(jQueryにおけるレスポンスデータの取得)

sample.js
.done(function(data) {
  let note = data;  // 引数のdataからレスポンスデータを取得できる
  // (略)
})

2-2-5-3. readyStateプロパティとstatusプロパティについて

XMLHttpRequest.readyStateプロパティ

これは、XMLHttpRequestオブジェクトのプロパティとなります。
readyStateプロパティは、XMLHttpRequestのインスタンスの状態を0から4の数値で返します。
数値と状態の対比は次のとおりです(引用サイト「MDN Web Docs / XMLHttpRequest.readyState」→こちらを見ていただいた方が正確です)。

戻り値 状態 内容
0 UNSENT XMLHttpRequestのインスタンス作成済み
1 OPENED open()メソッド呼び出し済み
2 HEADERS_RECEIVED send()メソッド呼び出し済み
3 LOADING レスポンスデータの読み込み中
4 DONE 読み込み完了

サンプルコードでは、if (xmlHR.readyState === 4)という条件を満たした場合に、レスポンスデータの処理等を行うという構造になっています。

XMLHttpRequest.statusプロパティ

このXMLHttpRequest.statusプロパティには、サーバから送信される「HTTPステータスコード」が格納されています(引用サイト「MDN Web Docs / XMLHttpRequest.status」)。
このHTTPステータスコードにより、HTTPリクエストが正常に終了したかどうかが分かります。
下記は今回の記事作成中に見かけたHTTPステータスコードです(ほんの一例です)。

コード 意味 説明
200 OK リクエストが成功した
400 Bad Request 無効なリクエストが送信されたなど
404 Not Found リクエストされたリソースが見つからなかった
422 Unprocessable Entity リクエストは正しいがサーバで処理できない

例えば、セキュリティトークンが送信されていない場合、422のエラーが生じます。
その他、コードの一覧は「MDN Web Docs / HTTP レスポンスステータスコード」などで確認できます。

サンプルコードでは、if (xmlHR.status === 200)という条件を満たした場合にはリクエスト成功時の処理を記述し、条件を満たさなかった場合にはリクエスト失敗時の処理を記述するという構造になっています。

3. 削除(DELETEメソッド)についてのAjaxのコード

次に、投稿内容を削除するDELETEメソッドについてのAjax通信です。

3-1. jQueryでAjaxを記載(DELETEメソッド)

まず、jQueryでの実装例です。
新たに削除(DELETE)メソッドを追加した部分は、後半の20行分となります。

note.js
$(function() {
  // 追加するHTMLデータを生成する関数
  function createHTML(note) {
    let html = `<div class="note" id="note${note.id}">
                  <span class="note_name">投稿者:${note.user_name}</span>
                  <a class="note_delete" rel="nofollow" data-method="delete" href="/notes/${note.id}">削除</a>
                  <p class="note_body">${note.body}</p>
                </div>`
    return html;
  }
  // メモ投稿(POSTメソッド)の処理
  $("#note_input").on("submit", function(e) {
    e.preventDefault();  // デフォルトのイベント(HTMLデータ送信など)を無効にする
    let inputText = $(".note_form-text").val();  // textareaの入力値を取得
    let url = $(this).attr("action");  // action属性のurlを抽出
    $.ajax({
      url: url,  // リクエストを送信するURLを指定
      type: "POST",  // HTTPメソッドを指定(デフォルトはGET)
      data: {  // 送信するデータをハッシュ形式で指定
        "note[body]": inputText
      },
      dataType: "json"  // レスポンスデータをjson形式と指定する
    })
    .done(function(data) {
      let html = createHTML(data);  // 受信したデータ(data)を元に追加するURLを生成(createHTML関数は冒頭で定義)
      $(".notes").append(html);  // 生成したHTMLをappendメソッドでドキュメントに追加
      $(".note_form-text").val("");  // textareaを空にする
    })
    .fail(function() {
      alert("error!");  // 通信に失敗した場合はアラートを表示
    })
    .always(function() {
      $(".note_form-btn").prop("disabled", false);  // submitボタンのdisableを解除
      $(".note_form-btn").removeAttr("data-disable-with");  // submitボタンのdisableを解除(Rails5.0以降はこちらも必要)
    });
  });
  // メモ削除(DELETEメソッド)の処理
  $(".notes").on("click", ".note_delete", function(e) {
    e.preventDefault();  // デフォルトのイベント(リンクURLへの遷移処理など)を無効にする
    e.stopPropagation();  // 現在のイベントのさらなる伝播(DELETEメソッドの実行)を止める
    let url = $(this).attr("href");
    $.ajax({
      url: url,
      type: "POST",  // 原則に従って"DELETE"メソッドを使用しない
      data: {
        _method: "delete",  // ここで"DELETE"メソッドを使用することを指定
      },
      dataType: "json"
    })
    .done(function(data) {
      $("#note" + data.id).remove();  // レスポンスデータのIDを元に投稿を削除
    })
    .fail(function(XMLHttpRequest) {
      alert(XMLHttpRequest.status);
    });
  });
});

基本的な形式は、POSTメソッドと変わりません。
異なる部分を中心に、以下、説明を書いていきます。

3-1-1. 動的に追加した要素をクリックする(jQuery)

前提として、jQuery(JavaScript)のファイル読み込み処理について確認しておきます。

まず、jQuery(JavaScript)のコードを記載したファイルの読み込みですが、これは、最初のページ読み込み時にのみ行われます。
次のような、クリックなどのイベント発火による処理も、クリックされるたびにファイルが読み込まれているわけではなく、最初の1回のみ読み込まれて、それをブラウザ内に記憶しておき、都度ブラウザ内の記憶を呼び出して実行しているということになります。

sample.js
$("#note_input").on("submit", function(e) {
  // イベント発火時に実行する内容
})

このようにイベントに基づき発火するメソッド(関数)は、「イベントリスナー(イベント実行リストのようなもの)」という名目でブラウザ内に登録されています。

以上のことから、ページを読み込んだ後(JavaScriptのファイルの読み込みが終わった後)に、動的に新たに追加された要素は、イベントリスナーとして登録されないことになるため、クリックしても反応しないということになってしまいます。

jQueryでは、この問題を、簡単に解決できるようになっています。
サンプルコード上で、その処理を実現している部分は、次の部分となります。

sample.js
$(".notes").on("click", ".note_delete", function(e) {
  // イベント発火時の処理内容を記載
});

これを構文として書き表すと次のようになります。
$(親要素のセレクタ).on(イベントの種類, 子要素のセレクタ, 関数等のオブジェクト)

この構文において、クリック(他のイベントも同じ)によるイベント発火は、見かけ上、子要素へのクリックに基づき実行されます。
ところが、実際は、親要素へのクリックにより、イベント発火が行われているという仕組みになっています。

そのため、イベント発火の元となる親要素は、最初(ページ読み込み時)に存在している必要がありますが、子要素は、後から追加された動的な要素であっても構わないという仕組みになっています(と理解しています)。

具体的にこのjQueryのメソッドどのような構造なのかについては、下記のサイトを参考にしていただければと思います。

●参考サイト
[jQuery] on() で後から追加した要素にもイベントを定義したい
jQuery write less, do more. / .on()

3-1-2. preventDefault()メソッド及びstopPropagation()メソッド

次に、デフォルトの処理等をキャンセルするpreventDefault()メソッド及びstopPropagation()メソッドについてです。
サンプルコードでは、次のように記載しています。

sample.js
$(".notes").on("click", ".note_delete", function(e) {
  e.preventDefault();  // デフォルトのイベント(リンクURLへの遷移処理)を無効にする
  e.stopPropagation();  // 現在のイベントのさらなる伝播(DELETEメソッドの実行)を止める
  // (略)
});

これらの記述により、HTMLに基づくデフォルトのイベント処理をキャンセルすることで、無用の処理を発生させず、Ajaxでの処理との重複などを避けることができます。
以下、個別に見てみます。

3-1-2-1. Event.preventDefault()メソッド

preventDefault()メソッドは、デフォルトのイベント処理をキャンセルして実行しないようにするメソッドです(参照:MDN Web Docs / Event.preventDefault())。

サンプルコードでは、次のaタグにおけるリンク機能が、preventDefault()メソッドによりキャンセルされています。

sample.html
<a class="note_delete" rel="nofollow" data-method="delete" href="/notes/5">削除</a>

3-1-2-2. event.stopPropagation()メソッド

stopPropagation()メソッドは、現在のイベントの更なる伝播をキャンセルするメソッドです(参照:MDN Web Docs / event.stopPropagation)。

サンプルコードでは、上記aタグの属性data-method="delete"によるdeleteメソッドの実行が、stopPropagation()メソッドによりキャンセルされています。

なお、全ての削除処理をAjax通信のみで行うのであれば、data-method="delete"の部分を消してしまうことでも同様の結果が得られると思います(stopPropagation()メソッドがなくてもエラーなく処理が実行できることになります)。

3-1-3. HTTPにおけるDELETEメソッドについて

3-1-3-1. POSTメソッドによる削除

サンプルコードでは、削除(DELETE)の処理であるにも関わらず、次のように、typeとして"POST"メソッドを指定しています。

sample.js
$.ajax({
  url: url,
  type: "POST",  // 原則に従って"DELETE"メソッドを使用しない
  data: {
    _method: "delete",  // ここで"DELETE"メソッドを使用することを指定
  },
  dataType: "json"
})

POSTメソッドを使用して、削除機能を実装する理由は、「jQueryの日本語リファレンス / $.ajax()」にある次の説明のとおりで、DELETEメソッドが全てのブラウザでサポートされている保証がないからです。

キー:type
型:String 初期値:'GET'
リクエストのタイプ("POST"または"GET")を指定します。
注意: PUTやDELETEのような、他のHTTPリクエストメソッドも、ここで指定することが可能ですが、 全てのブラウザでサポートされている保証がありません。

具体的な、コードの記載については、次の記事などを参考とさせていただきました。
[Laravel / jQuery] 非同期(Ajax)でレコードを削除したい

3-1-3-2. DELETEメソッドによる削除

どこまでの環境で動作するかは確認できていませんが、次のようにDELETEメソッドによる記載をしても、Ajaxによる削除を行うことが可能です。

sample.js
$.ajax({
  url: url,
  type: "DELETE",  // 原則に従って"DELETE"メソッドを使用しない
  dataType: "json"
})

3-2. JavaScriptでAjaxを記載(DELETEメソッド)

次に、JavaScriptでの削除(DELETEメソッド)機能の実装例です。

note.js
window.addEventListener("load", function() {
  let token = document.getElementsByName("csrf-token")[0].content; //セキュリティトークンの取得
  // 追加するHTMLデータを生成する関数
  function createHTML(note) {
    // 必要となるタグ及びテキストノードを生成
    let divElm = document.createElement("div"); 
    let spanElm = document.createElement("span");
    let aElm = document.createElement("a");
    let pElm = document.createElement("p");
    let nameText = document.createTextNode("投稿者:" + note.user_name);
    let deleteText = document.createTextNode(" 削除");
    let bodyText = document.createTextNode(note.body);
    // 各タグに属性・属性値を付加
    divElm.setAttribute("class", "note");
    divElm.setAttribute("id", "note" + note.id);
    spanElm.setAttribute("class", "note_name");
    aElm.setAttribute("class", "note_delete");
    aElm.setAttribute("rel", "nofollow");
    aElm.setAttribute("data-method", "delete");
    aElm.setAttribute("href", "/notes/" + note.id);
    aElm.addEventListener("click", function(e) { deleteHTMLEvent(e, aElm) }, false); //ここが大事!
    pElm.setAttribute("class", "note_body");
    // ノードの結合(各子要素にテキストを追加)
    spanElm.appendChild(nameText);
    aElm.appendChild(deleteText);
    pElm.appendChild(bodyText);
    // ノードの結合(親要素に子要素を追加)
    divElm.appendChild(spanElm);
    divElm.appendChild(aElm);
    divElm.appendChild(pElm);
    return divElm;
  };

  // メモ投稿(POSTメソッド)の処理
  let addHTMLEvent = function(e) {
    e.preventDefault();  // デフォルトのイベント(HTMLデータ送信など)を無効にする
    //送信データの生成
    let inputText = document.getElementsByClassName("note_form-text")[0].value;  // textareaの入力値を取得
    let url = document.getElementById("note_input").getAttribute("action") + ".json";  // 末尾に[.json]を追加して送信データがjson形式であることを指定
    let hashData = {  // 送信するデータをハッシュ形式で指定
      note: {body: inputText}  // 入力テキストを送信
      // authenticity_token: token  // セキュリティトークンの送信(ここから送信することも可能)
    };
    let data = JSON.stringify(hashData); // 送信用のjson形式に変換
    // Ajax通信を実行
    let xmlHR = new XMLHttpRequest();  // XMLHttpRequestオブジェクトの作成
    xmlHR.open("POST", url, true);  // open(HTTPメソッド, URL, 非同期通信[true:default]か同期通信[false]か)
    xmlHR.responseType = "json";  // レスポンスデータをjson形式と指定
    xmlHR.setRequestHeader("Content-Type", "application/json");  // リクエストヘッダーを追加(HTTP通信でJSONを送る際のルール)
    xmlHR.setRequestHeader("X-CSRF-Token", token);  // リクエストヘッダーを追加(セキュリティトークンの追加)
    xmlHR.send(data);  // sendメソッドでサーバに送信
    // 受信したデータの処理
    xmlHR.onreadystatechange = function() {
      if (xmlHR.readyState === 4) {  // readyStateが4になればデータの読込み完了
        if (xmlHR.status === 200) {  // statusが200の場合はリクエストが成功
          let note = xmlHR.response;  // 受信したjsonデータを変数noteに格納
          let html = createHTML(note);  // 受信データを元にHTMLを作成
          document.getElementsByClassName("notes")[0].appendChild(html);  // 作成したHTMLをドキュメントに追加
          document.getElementsByClassName("note_form-text")[0].value = "";  // テキストエリアを空白に戻す
        } else {  // statusが200以外の場合はリクエストが適切でなかったとしてエラー表示
          alert("error");
        }
        document.getElementsByClassName("note_form-btn")[0].disabled = false;  // submitボタンのdisableを解除
        document.getElementsByClassName("note_form-btn")[0].removeAttribute("data-disable-with");  // submitボタンのdisableを解除(Rails5.0以降はこちらも必要)
      }
    };
  };
  document.getElementById("note_input").addEventListener("submit", addHTMLEvent, false);

  // 削除イベントの処理
  let deleteHTMLEvent = function(e, noteDelete) {
    e.preventDefault();
    e.stopPropagation();
    let url = noteDelete.getAttribute("href") + ".json"; //末尾に .json を追加することで送信データがjson形式であることを指定する
    // Ajax通信を実行
    let xmlHR = new XMLHttpRequest(); //XMLHttpRequestの作成
    xmlHR.open("DELETE", url, true); //open(HTTPメソッド, URL, 非同期通信[true:default]か同期通信[false]か)
    xmlHR.responseType = "json"; //レスポンスデータを json形式と指定する
    xmlHR.setRequestHeader("Content-Type", "application/json");  // リクエストヘッダーを追加(HTTP通信でJSONを送る際のルール)
    xmlHR.setRequestHeader("X-CSRF-Token", token);  // リクエストヘッダーを追加(セキュリティトークンの追加)
    xmlHR.send(); //サーバに送信
    // 受信したデータの処理
    xmlHR.onreadystatechange = function() {
      if (xmlHR.readyState === 4) {
        if (xmlHR.status === 200) {
          let note = xmlHR.response;
          document.getElementById("note" + note.id).remove();
        } else {
          alert(xmlHR.status);
        }
      }
    };
  };
  let noteDeletes = document.getElementsByClassName("note_delete")
  Array.prototype.forEach.call(noteDeletes, function(noteDelete) {
    noteDelete.addEventListener("click", function(e) { deleteHTMLEvent(e, noteDelete) }, false);
  });
});

POSTメソッドのサンプルコードに、末尾の27行を追加した上で、その他に若干の修正を行っています。

3-2-1. AjaxによるDELETEメソッドの送信(JavaScript)

Ajax通信を行うために、XMLHttpRequestオブジェクトを使ってサーバと通信するという点は、基本的にPOSTメソッドの場合と変わりません。

該当部分のコードは次のとおりです。

sample.js
let xmlHR = new XMLHttpRequest(); //XMLHttpRequestの作成
xmlHR.open("DELETE", url, true); //open(HTTPメソッド, URL, 非同期通信[true:default]か同期通信[false]か)
xmlHR.responseType = "json"; //レスポンスデータを json形式と指定する
xmlHR.setRequestHeader("Content-Type", "application/json");  // リクエストヘッダーを追加(HTTP通信でJSONを送る際のルール)
xmlHR.setRequestHeader("X-CSRF-Token", token);  // リクエストヘッダーを追加(セキュリティトークンの追加)
xmlHR.send(); //サーバに送信

異なると言えば、send()メソッドの送信時に、引数としてデータを指定していないということくらいでしょうか。

削除時のログは、次のようになっています。

log/development.log
Started DELETE "/notes/591.json" for ::1 at 2020-01-19 15:53:48 +0900
Processing by NotesController#destroy as JSON
  Parameters: {"id"=>"591", "note"=>{}}

send()メソッドの引数として"id"は特に指定していませんが、パラメータとしてしっかり送られています。
URLの情報に"id"が含まれているので、当然と言えば当然だと思います。

なお、このJavaScriptにおいても、POSTメソッドを使用した削除機能(DELETEメソッド)の実装を試みましたが、設定が上手く行かず、現時点では実装できていません。

3-2-2. 複数の要素にイベントリスナーを適用する方法(JavaScript)

POSTメソッドの実装時に、イベントリスナーを適用するノード(要素、タグ)は、formタグの中のsubmitボタンのクリックイベントの1箇所だけでした。
しかし、DELETEメソッドでは、表示されている全ての投稿の削除ボタンにイベントリスナーを設定する必要があります。

投稿の全てに、イベントリスナーを設定するには、次のように、Array.prototype.forEach.callメソッドなどを用いて該当する全ての要素にイベントリスナーを登録することになります。
この方法は、「【JavaScript】イベントリスナを複数要素にまとめて登録する方法」で解説されている内容を使用させていただきました。

sample.js
let noteDeletes = document.getElementsByClassName("note_delete")
Array.prototype.forEach.call(noteDeletes, function(noteDelete) {
  noteDelete.addEventListener("click", function(e) { deleteHTMLEvent(e, noteDelete) }, false);
});

これは、for文で単純にループさせることでも可能です。

なお、コールバック関数に引数があるため、function(e) { deleteHTMLEvent(e, noteDelete) },という特殊な書き方になっています。
これについては、「Javascriptでイベントハンドラのコールバック関数に引数を渡す」の記事等を参考にさせていただきました。詳細はリンク先を参照してください。

●参考サイト
配列ライクなオブジェクトをforEachするときのイディオム
JavaScriptでコールバック関数にあらかじめ引数を渡したい!

3-2-3. 動的に追加された要素へのイベントリスナーの適用(JavaScript)

動的に追加された要素(本サンプルではメモ投稿を表示するノード)に、後からイベントリスナーを登録するには、多少の工夫が必要となります。

大事なところは、createHTML関数の中に追加した次の1行です(上から21行目)。

sample.js
aElm.addEventListener("click", function(e) { deleteHTMLEvent(e, aElm) }, false); //ここが大事!

新規投稿のHTMLが生成がされる度に、上記の1行でaタグに新しいイベントリスナーを追加するようにしています。

ここでコールバック関数として呼び出されているのはfunction(e) { deleteHTMLEvent(e, aElm) }の部分です(コールバック関数に引数を渡す方法については、「Javascriptでイベントハンドラのコールバック関数に引数を渡す」を参照)。

呼びだされるコールバック関数は、最初にページをロードする際に読み込む関数と同じもので、具体的には、次の部分となります。

sample.js
let deleteHTMLEvent = function(e, noteDelete) {
  e.preventDefault();
  e.stopPropagation();
  let url = noteDelete.getAttribute("href") + ".json"; //末尾に .json を追加することで送信データがjson形式であることを指定する
  // Ajax通信を実行
  let xmlHR = new XMLHttpRequest(); //XMLHttpRequestの作成
  xmlHR.open("DELETE", url, true); //open(HTTPメソッド, URL, 非同期通信[true:default]か同期通信[false]か)
  xmlHR.responseType = "json"; //レスポンスデータを json形式と指定する
  xmlHR.setRequestHeader("Content-Type", "application/json");  // リクエストヘッダーを追加(HTTP通信でJSONを送る際のルール)
  xmlHR.setRequestHeader("X-CSRF-Token", token);  // リクエストヘッダーを追加(セキュリティトークンの追加)
  xmlHR.send(); //サーバに送信
  // 受信したデータの処理
  xmlHR.onreadystatechange = function() {
    if (xmlHR.readyState === 4) {
      if (xmlHR.status === 200) {
        let note = xmlHR.response;
        document.getElementById("note" + note.id).remove();
      } else {
        alert(xmlHR.status);
      }
    }
  };
};

新規の投稿がページに追加される度に、上記の関数が呼び出されて、イベントリスナーとして追加登録されているということになります。

●参考サイト
[JavaScript] イベント処理を動的に追加する
動的に追加した要素にaddEventListnerを設定する方法

4. おわりに

まとめておきたかった内容は、以上のところです。

挙動は確認しているので、動作はするはずですが、もっと良い書き方や、正しい書き方があるのだろうと思います。
お気付きのことがあれば、ご指摘等をいただけると幸いです。

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

Hamlでのlink toの書き方【Rails】

Hamlにおける link to の書き方がいつも迷ってしまうのでメモ。

Hamlでのlink to の書き方

/profiles のページに飛ばしたい場合
以下どちらかの書き方がある。

sample.html.haml
#以下のどちらでもよい
= link_to "/profiles" do #①
= link_to profiles_path do #②

/profiles/new のページに飛ばしたい場合
以下どちらかの書き方がある。

sample.html.haml
#以下のどちらでもよい
= link_to "/profiles/new" do #①
= link_to new_profile_path do #②

②の書き方のpathはどこから取得するのか

ターミナルで $rails routes で出てくる。

例)この Prefix の部分

terminal
$ rails routes
                              Prefix Verb   URI Pattern                                                                              Controller#Action
                    new_user_session GET    /users/sign_in(.:format)                                                                 devise/sessions#new
                        user_session POST   /users/sign_in(.:format)                                                                 devise/sessions#create
                destroy_user_session DELETE /users/sign_out(.:format)                                                                devise/sessions#destroy
                   new_user_password GET    /users/password/new(.:format)                                                            devise/passwords#new
                  edit_user_password GET    /users/password/edit(.:format)                                                           devise/passwords#edit
                       user_password PATCH  /users/password(.:format)                                                                devise/passwords#update
                                     PUT    /users/password(.:format)                                                                devise/passwords#update
                                     POST   /users/password(.:format)                                                                devise/passwords#create
            cancel_user_registration GET    /users/cancel(.:format)                                                                  devise/registrations#cancel
               new_user_registration GET    /users/sign_up(.:format)                                                                 devise/registrations#new
              edit_user_registration GET    /users/edit(.:format)                                                                    devise/registrations#edit
                   user_registration PATCH  /users(.:format)                                                                         devise/registrations#update
                                     PUT    /users(.:format)                                                                         devise/registrations#update
                                     DELETE /users(.:format)                                                                         devise/registrations#destroy
                                     POST   /users(.:format)                                                                         devise/registrations#create
                                root GET    /                                                                                        home#index
                             mypages GET    /mypages(.:format)                                                                       mypages#index
                               items POST   /items(.:format)                                                                         items#create
                            new_item GET    /items/new(.:format)                                                                     items#new
                                item GET    /items/:id(.:format)                                                                     items#show
                            profiles GET    /profiles(.:format)                                                                      profiles#index
                         new_profile GET    /profiles/new(.:format)                                                                  profiles#new
                        edit_profile GET    /profiles/:id/edit(.:format)                                                             profiles#edit
                                     GET    /items(.:format)                                                                         items#index
                                     POST   /items(.:format)                                                                         items#create
                                     GET    /items/new(.:format)                                                                     items#new
                                     GET    /items/:id(.:format)                                                                     items#show
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアル 第13章 ユーザーのマイクロポスト - マイクロポストの画像投稿

概要

マイクロポストの投稿に関する基本的な機能は、第13章ここまでの学習で一通り実装が完了しました。続いては「画像つきマイクロポストを投稿できるようにする」という項です。

「画像をアップロードしてマイクロポストに関連付ける」という機能を実装するためには、以下のリソースが必要となります。

  • 画像をアップロードするためのフォーム
  • 投稿された画像そのもの

画像をアップロードするためのフォームには、「Upload Image」というボタンからアクセスすることとします。Railsチュートリアル本文では、図 13.18に、「Upload Image」ボタンと画像つきマイクロポストを含むモックアップが示されています。

まずは開発環境にて、画像アップロード機能のβ版を実装していくこととします。

基本的な画像アップロード

長くなりましたので、別記事で解説します。

演習 - 基本的な画像アップロード

1. 画像付きのマイクロポストを投稿してみましょう。もしかして、大きすぎる画像が表示されてしまいましたか? (心配しないでください、次の13.4.3でこの問題を直します)。

実際に画像付きのマイクロポストを投稿した結果です。

スクリーンショット 2020-01-07 7.50.28.png

サーバー側における、マイクロポスト投稿時のPOSTリクエストに対して記録されたログは以下の通りです。

Started POST "/microposts" for ...略
Processing by MicropostsController#create as HTML
  Parameters: {...略, "micropost"=>{"content"=>"LGTM!", "picture"=>#<ActionDispatch::Http::UploadedFile:0x00005592590299f0 @tempfile=#<Tempfile:/tmp/RackMultipart20200106-15302-4935p1.png>, @original_filename="lgtm1.png", @content_type="image/png", @headers="Content-Disposition: form-data; name=\"micropost[picture]\"; filename=\"lgtm1.png\"\r\nContent-Type: image/png\r\n">}, "commit"=>"Post"}
  User Load (2.5ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  SQL (20.4ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at", "picture") VALUES (?, ?, ?, ?, ?)  [["content", "LGTM!"], ["user_id", 1], ["created_at", "2020-01-06 22:48:18.966477"], ["updated_at", "2020-01-06 22:48:18.966477"], ["picture", "lgtm1.png"]]
   (10.2ms)  commit transaction
Redirected to http://localhost:8080/
Completed 302 Found in 278ms (ActiveRecord: 36.3ms)

目新しいのは、「POSTリクエストに含まれるpictureパラメータ」と「SQLのINSERT文に含まれるpicture属性」です。以下、わかりやすいように改行を入れつつ、「POSTリクエストのパラメータの中身」と「SQLのINSERT文の中身」それぞれ見てみましょう。

POSTリクエストのパラメータの中身
"micropost"=>{
  "content"=>"LGTM!",
  "picture"=>#<ActionDispatch::Http::UploadedFile:0x00005592590299f0
    @tempfile=#<Tempfile:/tmp/RackMultipart20200106-15302-4935p1.png>, 
    @original_filename="lgtm1.png",
    @content_type="image/png",
    @headers="Content-Disposition: form-data;
      name=\"micropost[picture]\"; filename=\"lgtm1.png\"\r\nContent-Type:image/png\r\n">
}
SQLのINSERT文の中身
INSERT INTO "microposts"
  ("content", "user_id", "created_at", "updated_at", "picture")
  VALUES (?, ?, ?, ?, ?)  [
    ["content", "LGTM!"],
    ["user_id", 1],
    ["created_at", "2020-01-06 22:48:18.966477"],
    ["updated_at", "2020-01-06 22:48:18.966477"],
    ["picture", "lgtm1.png"]
  ]

2. リスト 13.63に示すテンプレートを参考に、13.4で実装した画像アップローダーをテストしてください。

テストの準備として、まずはサンプル画像をfixtureディレクトリに追加してください (コマンド例: cp app/assets/images/rails.png test/fixtures/)。

リスト 13.63で追加したテストでは、Homeページにあるファイルアップロードと、投稿に成功した時に画像が表示されているかどうかをチェックしています。なお、テスト内にあるfixture_file_uploadというメソッドは、fixtureで定義されたファイルをアップロードする特別なメソッドです。

ヒント: picture属性が有効かどうかを確かめるときは、11.3.3で紹介したassignsメソッドを使ってください。このメソッドを使うと、投稿に成功した後にcreateアクション内のマイクロポストにアクセスするようになります。

変更対象のファイルはtest/integration/microposts_interface_test.rbです。変更内容は以下のようになります。

test/integration/microposts_interface_test.rb
  require 'test_helper'

  class MicropostsInterfaceTest < ActionDispatch::IntegrationTest
    def setup
      @user = users(:rhakurei)
      @other_user = users(:skomeiji)
    end

    test "micropost interface" do
      log_in_as(@user)
      get root_path
      assert_select 'img.gravatar'
      assert_select 'h1', @user.name
      assert_select 'span', /#{@user.microposts.count}/
      assert_select 'form[action="/microposts"]'
      assert_select 'textarea'
      assert_select 'div.pagination'
+     assert_select 'input[type="file"]'
      # 無効な送信
      assert_no_difference 'Micropost.count' do
        post microposts_path, params: { micropost: { content: "" } }
      end
      assert_select 'div#error_explanation'
      # 有効な送信
      content = "This micropost really ties the room together"
      picture = fixture_file_upload('test/fixtures/lgtm3.png', 'image/png')
      assert_difference 'Micropost.count', 1 do
-       post microposts_path, params: { micropost: { content: content } }
+       post microposts_path, params: { micropost: { content: content, picture: picture } }
      end
+     assert assigns(:micropost).picture?
      assert_redirected_to root_url
      follow_redirect!
      assert_match content, response.body
      # 投稿を削除する
      assert_select 'a', text: 'delete'
      first_micropost = @user.microposts.paginate(page: 1).first
      assert_difference 'Micropost.count', -1 do
        delete micropost_path(first_micropost)
      end
      # 違うユーザーのプロフィールにアクセス(削除リンクがないことの確認)
      get user_path(users(:mkirisame))
      assert_select 'a', text: 'delete', count: 0
    end

    ...略
  end

現時点で、上記テストは問題なく成功します。

# rails test test/integration/microposts_interface_test.rb
NOTE: Gem::Specification#rubyforge_project= is deprecated with no replacement. It will be removed on or after 2019-12-01.
Gem::Specification#rubyforge_project= called from /usr/local/bundle/specifications/i18n-0.9.5.gemspec:17.
NOTE: Gem::Specification#rubyforge_project= is deprecated with no replacement. It will be removed on or after 2019-12-01.
Gem::Specification#rubyforge_project= called from /usr/local/bundle/specifications/i18n-0.9.5.gemspec:17.
Running via Spring preloader in process 15350
Started with run options --seed 7126

  2/2: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.57526s
2 tests, 23 assertions, 0 failures, 0 errors, 0 skips

どのような場合に上記テストが失敗するのか

例えば、以下のようにapp/controllers/microposts_controller.rbを書き換えるとどうなるでしょうか。「MicropostsコントローラーのStrong Parametersで、Webからの変更を受理する属性にpictureが含まれていない」場合です。

app/controllers/microposts_controller.rb
  class MicropostsController < ApplicationController
    ...略

    private

      def micropost_params
-       params.require(:micropost).permit(:content, :picture)
+       params.require(:micropost).permit(:content)
      end

      ...略
  end

以下のようなメッセージを出力してテストが失敗するようになります。

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 15402
Started with run options --seed 13967

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 4.093530500002089]
 test_micropost_interface#MicropostsInterfaceTest (4.09s)
        Expected false to be truthy.
        test/integration/microposts_interface_test.rb:30:in `block in <class:MicropostsInterfaceTest>'

  2/2: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.10118s
2 tests, 17 assertions, 1 failures, 0 errors, 0 skips

このテストの不具合

実は、このテストには1つの不具合があります。その内容解説と解消については、別記事で解説します。

画像の検証

長くなりましたので、別記事で解説します。

演習 - 画像の検証

1. 5MB以上の画像ファイルを送信しようとした場合、どうなりますか?

5MB以上の画像ファイルを選択した時点で、まず警告メッセージが出ます。

スクリーンショット 2020-01-09 20.41.08.png

それでも送信を強行しようとすると、以下のようなエラーメッセージが出て、マイクロポストの送信が強制的に中止されます。

スクリーンショット 2020-01-09 20.45.33.png

「Picture should be less than 5MB」というメッセージは、確かにバリデーションで定義したとおりですね。「Picture」というのは、errors.addの第1引数として与えたシンボル名を元に、Railsによって自動補完されたものです。

5MB以上の画像ファイルを送信しようとした場合にRailsサーバーが返すログの内容

このとき、当該マイクロポストのPOSTリクエストに対してRailsサーバーが返すログの内容は以下のようになります。

Started POST "/microposts" for ...略
Processing by MicropostsController#create as HTML
  Parameters: {...略, "micropost"=>{"content"=>"25MB picture uploading", "picture"=>#<ActionDispatch::Http::UploadedFile:0x00007fd15dac96e0 @tempfile=#<Tempfile:/tmp/RackMultipart20200109-15302-g9g2gb.jpg>, @original_filename="48965744982_478d5a648c_o.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"micropost[picture]\"; filename=\"48965744982_478d5a648c_o.jpg\"\r\nContent-Type: image/jpeg\r\n">}, "commit"=>"Post"}
  User Load (2.7ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
   (0.1ms)  begin transaction
   (0.1ms)  rollback transaction
  Rendering static_pages/home.html.erb within layouts/application
   (3.6ms)  SELECT COUNT(*) FROM "microposts" WHERE "microposts"."user_id" = ?  [["user_id", 1]]
  Rendered shared/_user_info.html.erb (8.3ms)
  Rendered shared/_error_messages.html.erb (1.1ms)
  ...略
Completed 200 OK in 3238ms (Views: 507.8ms | ActiveRecord: 24.8ms)
rollback transaction

上記のように、RDBへの保存が取り消されています。

Rendered shared/_error_messages.html.erb (1.1ms)

上記のように、エラーメッセージのパーシャルが描画されています。

2. 無効な拡張子のファイルを送信しようとした場合、どうなりますか?

標準の状態では、input要素のaccept属性にない形式のファイルは、Webブラウザでアップロードするファイルを選択しようとした際にグレーアウトして選択できないようになっています。

スクリーンショット 2020-01-10 7.42.03.png

続いて、Webブラウザのインスペクター機能を用いて、input要素のaccept属性を無理やり削除してみます。

- <input accept="image/jpeg,image/gif,image/png" type="file" name="micropost[picture]" id="micropost_picture">
+ <input type="file" name="micropost[picture]" id="micropost_picture">

input要素のaccept属性を削除すると、以下のように、Webブラウザで無効な形式のファイルを選択することが可能になります。

スクリーンショット 2020-01-10 7.48.24.png

そのままマイクロポストを投稿してみます。

スクリーンショット 2020-01-10 7.56.41.png

エラーメッセージが出て、マイクロポストの送信が強制的に中止されました。

「Picture You are not allowed to upload "pu" files, allowed types: jpg, jpeg, gif, png」というエラーメッセージも自動生成されます。

無効な拡張子のファイルを送信しようとした場合にRailsサーバーが返すログの内容

このとき、当該マイクロポストのPOSTリクエストに対してRailsサーバーが返すログの内容は以下のようになります。

Started POST "/microposts" for ...略
Processing by MicropostsController#create as HTML
  Parameters: {...略, "micropost"=>{"content"=>"invalid format file", "picture"=>#<ActionDispatch::Http::UploadedFile:0x00007fd16c06a398 @tempfile=#<Tempfile:/tmp/RackMultipart20200109-15302-164lc9d.pu>, @original_filename="Class.pu", @content_type="application/octet-stream", @headers="Content-Disposition: form-data; name=\"micropost[picture]\"; filename=\"Class.pu\"\r\nContent-Type: application/octet-stream\r\n">}, "commit"=>"Post"}
  User Load (2.9ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
   (0.1ms)  begin transaction
   (0.1ms)  rollback transaction
  Rendering static_pages/home.html.erb within layouts/application
   (3.3ms)  SELECT COUNT(*) FROM "microposts" WHERE "microposts"."user_id" = ?  [["user_id", 1]]
  Rendered shared/_user_info.html.erb (6.5ms)
  Rendered shared/_error_messages.html.erb (0.7ms)
  Rendered shared/_micropost_form.html.erb (20.7ms)
  Rendered shared/_feed.html.erb (0.5ms)
  Rendered shared/_home_logged_in.erb (85.2ms)
  Rendered static_pages/home.html.erb within layouts/application (103.8ms)
  Rendered layouts/_rails_default.erb (289.3ms)
  Rendered layouts/_shim.html.erb (0.3ms)
  Rendered layouts/_header.html.erb (1.0ms)
  Rendered layouts/_footer.html.erb (0.5ms)
Completed 200 OK in 572ms (Views: 529.8ms | ActiveRecord: 6.4ms)
rollback transaction

上記のように、RDBへの保存が取り消されています。

Rendered shared/_error_messages.html.erb (0.7ms)

上記のように、エラーメッセージのパーシャルが描画されています。

画像のリサイズ

長くなりましたので、別記事で解説します。

演習 - 画像のリサイズ

1. 解像度の高い画像をアップロードし、リサイズされているかどうか確認してみましょう。画像が長方形だった場合、リサイズはうまく行われているでしょうか?

ここまでの実装を終えた時点で、改めて大きなサイズの画像をアップロードしてみましょう。

スクリーンショット 2020-01-10 18.40.32.png

いい感じにリサイズされた上でアップロードされるようになりました。長方形の画像でも、アスペクト比がおかしくなるようなことはないですね。

なお、Railsサーバーのログには、画像のリサイズに関するログは残りませんでした。

2. 既にリスト 13.63のテストを追加していた場合、この時点でテストスイートを走らせるとエラーメッセージが表示されるようになるはずです。このエラーを取り除いてみましょう。

ヒント: リスト 13.68にある設定ファイルを修正し、テスト時はCarrierWaveに画像のリサイズをさせないようにしてみましょう。

下記のようなassignsを使ったController Specのテストの場合、画像のリサイズ処理を実装すると、テストが失敗するようになります。

test/integration/microposts_interface_test.rb
require 'test_helper'

class MicropostsInterfaceTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:rhakurei)
    @other_user = users(:skomeiji)
  end

  test "micropost interface" do
    # ...略
    # 有効な送信
    content = "This micropost really ties the room together"
    picture = fixture_file_upload('test/fixtures/lgtm3.png', 'image/png')
    assert_difference 'Micropost.count', 1 do
      post microposts_path, params: { micropost: { content: content, picture: picture } }
    end
    assert assigns(:picture).picture?
    # ...略
  end
end

具体的には、以下のようなエラーが発生するようになります。

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 15789
Started with run options --seed 59918

ERROR["test_micropost_interface", MicropostsInterfaceTest, 4.157690599997295]
 test_micropost_interface#MicropostsInterfaceTest (4.16s)
NoMethodError:         NoMethodError: undefined method `picture?' for nil:NilClass
            test/integration/microposts_interface_test.rb:30:in `block in <class:MicropostsInterfaceTest>'

  2/2: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.43253s
2 tests, 16 assertions, 0 failures, 1 errors, 0 skips

assigns絡みでエラーが発生しているのはわかるのですが、以下の内容のconfig/initializers/skip_image_resizing.rbを作成しても、エラーが解消できませんでした。

config/initializers/skip_image_resizing.rb
if Rails.env.test?
  CarrierWave.configure do |config|
    config.enable_processing = false
  end
end

なお、以下のようなRequest Specのテストを行っている場合、上記のエラーは発生しません。

test/integration/microposts_interface_test.rb
require 'test_helper'

class MicropostsInterfaceTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:rhakurei)
    @other_user = users(:skomeiji)
  end

  test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select 'img.gravatar'
    assert_select 'h1', @user.name
    assert_select 'span', /#{@user.microposts.count}/
    assert_select 'form[action="/microposts"]'
    assert_select 'textarea'
    assert_select 'div.pagination'
    assert_select 'input[type="file"]'
    # 無効な送信
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "" } }
    end
    assert_select 'div#error_explanation'
    # 有効な送信
    content = "This micropost really ties the room together"
    picture = fixture_file_upload('test/fixtures/lgtm3.png', 'image/png')
    assert_difference 'Micropost.count', 1 do
      post microposts_path, params: { micropost: { content: content, picture: picture } }
    end
    assert_redirected_to root_url
    follow_redirect!
    assert_match content, response.body
    assert_select "img[src*='#{picture.original_filename}']"
    # ...略
  end

本番環境での画像アップロード

開発環境における画像アップロード機能のβ版は、これにて完成となりました。今度は本番環境に画像アップロード機能を実装していきます。長くなりましたので、別記事で解説します。

演習 - 本番環境での画像アップロード

1. 本番環境で解像度の高い画像をアップロードし、適切にリサイズされているか確認してみましょう。長方形の画像であっても、適切にリサイズされていますか?

スクリーンショット 2020-01-18 22.37.22.png

スクリーンショット 2020-01-18 22.39.52.png

適切に画像はリサイズされているようです。

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

【Rails】ActionMailer + AWS SES

はじめに

Rails アプリケーションからユーザーへメールを飛ばす方法としてAWS SESというサービスがあるので、こちらの使い方を説明させていただきます。
設定方法なので知っていればすぐにできるんですが、知らなければやり方を見つけるだけでかなり時間がかかってしまうと思います。
時間短縮の一助になれば嬉しいです。

関連リンク

Gmailを用いたActionMailerの使い方についても下記に載せておくので、必要であれば参考にしてください。。

アプリケーション作成

以下の項目については、上記の"Gmailを用いた ActionMailer の使い方"と同様となるため、そちらをご参照ください。

  • Mailer作成コマンド(rails g mailer ~)
  • Mailer
  • Controller
  • View

ドメイン関連

下記リンク先をご参照ください。

IAMユーザーの作成

下記リンク先をご参照ください。

config / yaml設定

config/development.rb
  # ActionMailer Setting with AWS SES
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.perform_caching = true
  config.action_mailer.default_url_options = { host: Rails.application.secrets.host }
  ActionMailer::Base.smtp_settings = {
      :address =>        Rails.application.secrets.address,
      :port =>           587,
      :domain =>         Rails.application.secrets.domain,
      :authentication => :login,
      :user_name =>      Rails.application.secrets.access_key_id,
      :password =>       Rails.application.secrets.secret_access_key
  }
config/production.rb
  # ActionMailer Setting with AWS SES
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.perform_caching = true
  config.action_mailer.default_url_options = { host: Rails.application.secrets.host }
  ActionMailer::Base.smtp_settings = {
      :address =>        Rails.application.secrets.address,
      :port =>           587,
      :domain =>         Rails.application.secrets.domain,
      :authentication => :login,
      :user_name =>      Rails.application.secrets.access_key_id,
      :password =>       Rails.application.secrets.secret_access_key
  }
secret.yml
development:
  host:              ec2-*****.ap-northeast-1.compute.amazonaws.com  # EC2インスタンス の パブリック DNS (IPv4) を入力する。
  access_key_id:     A******************A                            # 上記で取得した値
  secret_access_key: B******************************************B    # 上記で取得した値
  address:           email-smtp.us-east-1.amazonaws.com              # 自身が仕様しているリージョン
  domain:            c*****.com                                      # 上記で取得した値

production:
  host:              <%= ENV['AWS_HOST'] %>               # ElasticBeanstalk を使う場合、環境変数へ入力する。
  access_key_id:     <%= ENV['AWS_ACCESS_KEY_ID'] %>      # ElasticBeanstalk を使う場合、環境変数へ入力する。
  secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>  # ElasticBeanstalk を使う場合、環境変数へ入力する。
  address:           <%= ENV['AWS_ADDRESS'] %>            # ElasticBeanstalk を使う場合、環境変数へ入力する。
  domain:            <%= ENV['AWS_DOMAIN'] %>             # ElasticBeanstalk を使う場合、環境変数へ入力する。

まとめ

今回、ドメインを取得するところもAWSのサービスを使って説明をさせていただきましたが、ドメイン取得については他のサービスを使ったほうがコストが低いケースもありますので、あくまで一例とさせていただきます。
個人的には色々なサービスを使うよりもAWSでまとめて管理できるほうが管理コストも含めると安いのではないかなーと思ったりします。
お好きな方法で取り組んでいただけたらなと思います。

参考

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

Railsチュートリアル 第13章 ユーザーのマイクロポスト - 本番環境での画像アップロード

本番環境での画像アップロードに必要な技術

本番環境に画像をアップロードするためには、以下のような技術が必要となります。

  • AWS IAMにおけるグループおよびユーザーの設定
  • AWS S3バケットの新規作成およびアクセス権設定
  • 本番環境に適用するRails設定の記述
  • Herokuの環境変数設定

特に未経験の人にとっては、相応に高度で総合力が試されるパートではないかと思われます。

本番環境での画像アップロードに必要な設定

現在までに実装した画像アップローダーは、ローカルのファイルシステムに画像を保存するようになっているため、本番環境での使用には適していません。「ローカルのファイルシステムに画像を保存する」という動作は、app/uploaders/picture_uploader.rb内の以下の記述に由来します。

app/uploaders/picture_uploader.rb(抜粋)
# Choose what kind of storage to use for this uploader:
storage :file
# storage :fog

「本番環境では、ローカルのファイルシステムとは別のクラウドストレージに画像を保存する」という設定にしたい場合、以下のように、「本番環境ではfog gemを使用する。それ以外ではローカルのファイルシステムに画像を保存する。」という記述を行います。

  class PictureUploader < CarrierWave::Uploader::Base
    # ...略

    # Choose what kind of storage to use for this uploader:
-   storage :file
-   # storage :fog
+   if Rails.env.production?
+     storage :fog
+   else
+     storage :file
+   end

    # ...略
  end

Rails.env.production?は、本番環境であればtrue、それ以外の環境(開発環境・テスト環境等)ではfalseを返します1

Amazon Web Servicesの設定

  • マイクロポストの画像を保存するためのAWS S3バケットを新規に作成する
  • AWSのリソースに対してプログラムによるアクセスが可能なユーザーを新規に作成する
    • 当該ユーザーは、AWSマネジメントコンソールにはアクセスしない
    • 別途当該S3バケットへのアクセス権を持つグループを割り当てる
  • 上記ユーザーに割り当てるためのグループを新規に作成する
    • 当該S3バケットへのフルアクセスを許可する
    • S3バケットの新規作成・削除は明示的に拒否する

以上の条件を前提として、以下の情報を参考にして設定しました。

特に、一番下に書かれた情報については見落としがちかと思います。

なお、Secretキーの内容を閲覧できるのは、Accessキーの新規作成時のみです。Accessキーに対応するSecretキーがわからない場合は、Accessキーそのものを新規作成する必要があります。

IAMにて「AWSマネジメントコンソールにはアクセスせず、プログラムによるアクセスのみを許可する」というユーザーを作成するには

GUIで行う場合、ユーザー作成の最初の画面で「プログラムによるアクセス」のみにチェックを入れます。「AWS マネジメントコンソールへのアクセス」にはチェックを入れません。

2020-01-19 142328.png

S3バケットへのパブリックアクセスを部分的に許可する

今回開発中のアプリケーションにおいて、S3バケットへの画像の保存を正常に行うには、以下2つの設定を行う必要があります。

  • S3バケットの「アクセス権限 - ブロックパブリックアクセス (バケット設定)」の設定を変更する
  • S3バケットの「アクセス権限 - バケットポリシー」の設定を変更する

S3バケットの「アクセス権限 - ブロックパブリックアクセス (バケット設定)」の設定を変更する

  • 「新規のパブリックバケットポリシーまたはアクセスポイントポリシーを介して付与されたバケットとオブジェクトへのパブリックアクセスをブロックする」のチェックボックスは「オン」
  • 上記以外のチェックボックスはオフ

2020-01-19 143557.png

S3バケットの「アクセス権限 - バケットポリシー」の設定を変更する

「アクセス権限 - バケットポリシー」のJSONエディタに、以下のJSONを入力していきます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "statement1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::[12桁のアカウントID]:user/[今回作成したユーザー名]"
            },
            "Action": "*",
            "Resource": "arn:aws:s3:::[バケット名]/*"
        }
    ]
}

CarrierWaveを通してS3を使うようにする

S3アカウントの作成と設定が終わったら、CarrierWaveに与える追加の設定を記述していきます。その内容は以下のようになります。

config/initializers/carrier_wave.rb
if Rails.env.production?
  CarrierWave.configure do |config|
    config.fog_credentials = {
      # Amazon S3用の設定
      :provider              => 'AWS',
      :region                => ENV['S3_REGION'],
      :aws_access_key_id     => ENV['S3_ACCESS_KEY'],
      :aws_secret_access_key => ENV['S3_SECRET_KEY']
    }
    config.fog_directory = ENV['S3_BUCKET']
  end
end

ポイントは以下です。

  • S3バケットへのアクセスに必要な情報は、Herokuの環境変数によって与えるようにしている
    • 「機密情報はソースコードにベタ書きしてはいけない」というのは大原則である
    • Herokuの環境変数は、手動で設定する必要がある
  • CarrierWave.configureの用法は、以前の演習で作成したconfig/initializers/skip_image_resizing.rbと同様である
    • 対象環境はtestではなくproduction
    • config.fog_credentialsの設定内容は「ハッシュの配列」として与える

なお、「GitHubのパブリックリポジトリにAWSのアクセスキーをアップロードしてしまった場合に何が起こるか」については、以下のようなリソースに詳しく書かれています。本当に怖いですね。

Herokuの環境変数に、S3バケットにアクセスするために必要な情報を追加する

Herokuの環境変数を設定するコマンドは、heroku config:setとなります。以下のように使用します。

heroku config:set [環境変数名]="[値]"

S3バケットにアクセスするために必要な情報のうち、セキュリティ確保の観点からソースコードに記述しない項目は以下の4つです。これらをHerokuの環境変数として与える必要があります。

  • S3バケットのAccessキー
  • S3バケットのSecretキー
  • S3バケットのID
  • S3バケットが属するリージョンのコード
    • 東京リージョンであればap-northeast-1となります
>>> heroku config:set S3_ACCESS_KEY="Accessキー"
>>> heroku config:set S3_SECRET_KEY="Secretキー"
>>> heroku config:set S3_BUCKET="バケットID"
>>> heroku config:set S3_REGION="リージョン"

画像を保存するディレクトリを、Gitによる管理対象から除外する

画像をはじめとするバイナリファイルは、一般に、Gitほかバージョン管理システムによるバージョン管理の対象とはなりません。そのため、バイナリファイルを保存するディレクトリは、バージョン管理の対象から除外する必要があります。

「Git管理下にあるディレクトリに格納されている特定リソースのみを、Gitによる管理対象から除外したい」という場合、一般に、「当該リソースについて、Gitリポジトリのルートディレクトリからのパスを.gitignoreファイルに記述する」という方法により実現できます。

画像を保存するディレクトリは、Gitリポジトリのルートディレクトリを/とすると、/public/uploadsとなります。

結果、.gitignoreに追加する内容は以下のようになります。

.gitignore
  # See https://help.github.com/articles/ignoring-files for more about ignoring files.
  #
  # If you find yourself ignoring temporary files generated by your text editor
  # or operating system, you probably want to add a global ignore instead:
  #   git config --global core.excludesfile '~/.gitignore_global'

  # ...略
+
+ # Ignore updated images
+ /public/uploads

ここまでの変更をHerokuにデプロイする

ソースコードに対する変更は、当然ながらデプロイしなければHeroku(に限らず本番環境全般)に反映されません。

masterブランチの変更内容をHerokuにプッシュする

まずは、masterブランチの内容をHerokuにpushしていきます。

>>> git push heroku

masterブランチの内容をHerokuにpushすると、裏で自動的にBundlerが動きます。pgfog、ならびにそれらが必要とするgemが次々にインストールされる様子がコンソール出力からも見て取れます。

データベースをリセットする

続いて、Heroku上のRDBの内容を一度完全にリセットします。

>>> heroku pg:reset DATABASE

なお、この操作を行うにあたっては、追加の確認が求められます。「データベースのリセット」というのは、影響範囲が極めて大きい破壊的変更であるためです。

>>> heroku pg:reset DATABASE
 ▸    WARNING: Destructive action
 ▸    postgresql-cylindrical-32893 will lose all of its data
 ▸    
 ▸    To proceed, type [アプリ名] or re-run this command with --confirm [アプリ名]

> [アプリ名]
Resetting postgresql-cylindrical-32893... done

データベースの構造と内容の再構築

まずはデータベースの構造の再構築からです。Herokuにおけるデータベースの構造の再構築は、heroku run rails db:migrateコマンドで行います。

>>> heroku run rails db:migrate
Running rails db:migrate on ⬢ [アプリ名]... up, run.8443 (Free)
...略
== 20190928080951 CreateUsers: migrating ======================================
== 20190928080951 CreateUsers: migrated (0.0093s) =============================
== 20191010034159 AddIndexToUsersEmail: migrating =============================
== 20191010034159 AddIndexToUsersEmail: migrated (0.0070s) ====================
== 20191013040411 AddPasswordDigestToUsers: migrating =========================
== 20191013040411 AddPasswordDigestToUsers: migrated (0.0024s) ================
== 20191104221611 AddRememberDigestToUsers: migrating =========================
== 20191104221611 AddRememberDigestToUsers: migrated (0.0028s) ================
== 20191128032931 AddAdminToUsers: migrating ==================================
== 20191128032931 AddAdminToUsers: migrated (0.0099s) =========================
== 20191202093532 AddActivationToUsers: migrating =============================
== 20191202093532 AddActivationToUsers: migrated (0.0089s) ====================
== 20191211225559 AddResetToUsers: migrating ==================================
== 20191211225559 AddResetToUsers: migrated (0.0048s) =========================
== 20191218224953 CreateMicroposts: migrating =================================
== 20191218224953 CreateMicroposts: migrated (0.0330s) ========================
== 20200105225338 AddPictureToMicroposts: migrated (0.0019s) ==================
...略

マイグレーションの進捗に、サンプルアプリケーションに積み上げてきた機能とその実装履歴が見えます。積み上げてきたものの歴史を感じられていいですよね。

続いてはデータベースの内容の再構築です。こちらはheroku run rails db:seedコマンドで行います。

AWSの設定情報が正しく与えられていれば、画像つきのマイクロポストを正常に投稿できます。その際、Herokuのログには以下のような記録が残ります。

2020-01-18T13:31:34.647241+00:00 app[web.1]: I, [2020-01-18T13:31:34.647131 #4]  INFO -- : [5d55adcb-c098-456a-8352-89e9afd326a6] Started POST "/microposts" for 1.33.232.123 at 2020-01-18 13:31:34 +0000
2020-01-18T13:31:34.648257+00:00 app[web.1]: I, [2020-01-18T13:31:34.648156 #4]  INFO -- : [5d55adcb-c098-456a-8352-89e9afd326a6] Processing by MicropostsController#create as HTML
2020-01-18T13:31:34.648445+00:00 app[web.1]: I, [2020-01-18T13:31:34.648361 #4]  INFO -- : [5d55adcb-c098-456a-8352-89e9afd326a6]   Parameters: {"utf8"=>"✓", "authenticity_token"=>"MILJ4TEbgpIItHsVQbg+f09zcrOzVHCT8rTACTQesXuw/smIusnvBoM9eP3LgfNFC8nK3X72awHdZPIwpYhM5A==", "micropost"=>{"content"=>"LGTM!", "picture"=>#<ActionDispatch::Http::UploadedFile:0x00007f94603397e8 @tempfile=#<Tempfile:/tmp/RackMultipart20200118-4-jt3qxq.png>, @original_filename="lgtm3.png", @content_type="image/png", @headers="Content-Disposition: form-data; name=\"micropost[picture]\"; filename=\"lgtm3.png\"\r\nContent-Type: image/png\r\n">}, "commit"=>"Post"}
2020-01-18T13:31:34.652670+00:00 app[web.1]: D, [2020-01-18T13:31:34.652572 #4] DEBUG -- : [5d55adcb-c098-456a-8352-89e9afd326a6]   User Load (1.9ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
2020-01-18T13:31:35.092221+00:00 app[web.1]: D, [2020-01-18T13:31:35.092040 #4] DEBUG -- : [5d55adcb-c098-456a-8352-89e9afd326a6]    (11.6ms)  BEGIN
2020-01-18T13:31:35.095644+00:00 app[web.1]: D, [2020-01-18T13:31:35.095539 #4] DEBUG -- : [5d55adcb-c098-456a-8352-89e9afd326a6]   SQL (1.2ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at", "picture") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["content", "LGTM!"], ["user_id", 1], ["created_at", "2020-01-18 13:31:35.093179"], ["updated_at", "2020-01-18 13:31:35.093179"], ["picture", "lgtm3.png"]]
2020-01-18T13:31:36.478723+00:00 heroku[router]: at=info method=POST path="/microposts" host=warm-woodland-62915.herokuapp.com request_id=5d55adcb-c098-456a-8352-89e9afd326a6 fwd="1.33.232.123" dyno=web.1 connect=0ms service=3010ms status=302 bytes=1017 protocol=https
2020-01-18T13:31:36.473495+00:00 app[web.1]: D, [2020-01-18T13:31:36.473384 #4] DEBUG -- : [5d55adcb-c098-456a-8352-89e9afd326a6]    (1.7ms)  COMMIT
2020-01-18T13:31:36.474219+00:00 app[web.1]: I, [2020-01-18T13:31:36.473995 #4]  INFO -- : [5d55adcb-c098-456a-8352-89e9afd326a6] Redirected to https://warm-woodland-62915.herokuapp.com/
2020-01-18T13:31:36.474222+00:00 app[web.1]: I, [2020-01-18T13:31:36.474157 #4]  INFO -- : [5d55adcb-c098-456a-8352-89e9afd326a6] Completed 302 Found in 1826ms (ActiveRecord: 16.4ms)

AWS S3を使っているからといって、Heroku側のログには特段変わった内容のログが記録されるわけではないといえそうです。


  1. 「テスト環境ではtrue、それ以外の環境ではfalseを返す」という動作をするRails.env.test?は、Railsチュートリアルの第13章中、「演習 - 画像のリサイズ」で用いましたね。 

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

ページ遷移先でリロードしないと非同期通信(ajax)できない

はじめに

某プログラミングスクールの課題で、Railsを使ってECサイトを作成しています。

エラー発生時の状況

トップページにて、link_to でページに移動するとリロードしないと非同期通信できない

エラーの仮説

turbolinksが邪魔してそう...

turbolinksってなんだっけ?

turbolinksとは、ページ遷移をAjaxに置き換え、JavaScriptやCSSのパースを省略することで高速化するgemで、Rails 4からはデフォルトで使用されるようになります。

原因

data-turbolinkが働いて遷移したページではイベントが発火しない事があるらしい.

対策

= link_to new_item_path,data: {"turbolinks" => false}, class: "seller_btn" do

これ→

{"turbolinks" => false},

data-turbolinkをオフにする

遷移元のリンクタグにdata属性を追加

参考記事

Rails6でjqueryアニメーションライブラリanimsitionの使用 | 躓いたことなど...
https://qiita.com/lookatachic/items/cc3accb542fca0eaf43a

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

https(SSL)通信の環境下でjavascriptが動かなくなる場合の原因と解決方法 ( 本番環境(AWS)でjavascriptを読み込む方法 )

はじめに

某プログラミングスクールの課題で、Railsを使ってECサイトを作成しています。

発生時の状況

商品出品画面のカテゴリー選択時、javascriptを使ってサブカテゴリーを入力できる機能を実装
本番環境にデプロイすると、カテゴリー選択時にサブカテゴリーが出現せず入力できませんでした。

エラーの仮説

・javascriptが本番環境で読み込めてない

エラーの原因

本番環境つまり、httpsによるSSL暗号通信下では、読み込んでない。

対策

1,jQuery本体をダウンロードして自分のサーバーで動かす方法。

2,URLをhttpsに変更する方法。

application.html
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>

3,プロトコルを指定しない方法。

application.html
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>

3つ目の方法が一番汎用性が高いのでおすすめです。この記述方法はjavascriptに限らず、CSSや画像のパスの指定にも使えるので、覚えておくと便利だそうです!!


参考記事

https://www.webernote.net/webcreate/https-javascript.html


最後に

自分のメモ用と、アウトプットとして記事に投稿させていただきました。

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

Railsチュートリアル 第13章 ユーザーのマイクロポスト - 画像のリサイズ

画像の自動リサイズ処理が必要な理由

ファイルサイズに対するバリデーションは、Railsチュートリアル 第13章 ユーザーのマイクロポスト - 画像の検証で実装できました。しかしながら、画像サイズ(縦横の長さ)に対する制限は、現状では存在しません。そのため、大きすぎる画像サイズ(横2048ピクセル等)のファイルがアップロードされると、以下のようにレイアウトが壊れてしまいます。

スクリーンショット 2020-01-10 8.15.32.png

ImageMagickのセットアップ

Railsチュートリアルを進めていっている開発環境においては、ImageMagickは必ずしもインストールされているとはいえません。また、ImageMagickそのものはgemというわけではないので、Bundlerによる管理の対象とはならず、個々の環境で別途インストールする必要があります。homebrewとかdnfとかでインストールするやつですね。

Dockerのrubyイメージは、Debianをベースに構成されています。そのため、ImageMagickをインストールするためのコマンドは以下となります。

# apt install imagemagick -y

yumにおけるパッケージ名(Railsチュートリアル本文に記述されていたものです)とは異なり、imagemagickは全て小文字となります。

実際に画像のリサイズを行うように画像アップローダーを修正する

今回は、「画像の縦横どちらかが400pxを超えていた場合、適切なサイズに縮小する。但し、画像を引き伸ばすことはしない。」という内容の実装とします。

MiniMagick gemについて

RubyでImageMagickを使うためのgemはMiniMagickといいます。gemそのもののインストールは、CarrierWave gemのインストールと同時に済ませています。

Gemfile
source 'https://rubygems.org'

# ...略
gem 'carrierwave',             '1.2.2'
gem 'mini_magick',             '4.7.0'  # <-- 既にインストール済み
# ...略

group :production do
  gem 'pg',  '0.20.0'
  gem 'fog', '1.42'
end

# ...略

CarrierWave::MiniMagickモジュールについて

CarrierWaveには、MiniMagickを使うための機能が含まれています。但し、当該機能を使うためにはCarrierWave::MiniMagickというモジュールをincludeする必要があります。

include CarrierWave::MiniMagick

画像のリサイズ処理の実体

「画像の縦横どちらかが400pxを超えていた場合、適切なサイズに縮小する。但し、画像を引き伸ばすことはしない。」という内容の実装の場合、その実体は以下のようになります。

process resize_to_limit: [400, 400]

processメソッドに、resize_to_limitというシンボルをキーとするハッシュを与えます。ハッシュの値には、[縦ピクセル数, 横ピクセル数、{オプション}]という配列を与えます。

Pictureアップローダーの内容を変更する

上記を総合すると、app/uploaders/picture_uploader.rbの変更内容は以下のようになります。

app/uploaders/picture_uploader.rb
  class PictureUploader < CarrierWave::Uploader::Base
    # Include RMagick or MiniMagick support:
    # include CarrierWave::RMagick
-   # include CarrierWave::MiniMagick
+   include CarrierWave::MiniMagick
    process resize_to_limit: [400, 400]

    # Choose what kind of storage to use for this uploader:
    storage :file
    # storage :fog

    ...略

    # Add a white list of extensions which are allowed to be uploaded.
    # For images you might use something like this:
    def extension_whitelist
      %w(jpg jpeg gif png)
    end

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

Railsチュートリアル 第13章 ユーザーのマイクロポスト - 画像の検証

アップローダーにおけるバリデーションの実装

画像フォーマットのバリデーション

現状では、画像として無効なファイルをアップロードできてしまう

Railsチュートリアル 第13章 ユーザーのマイクロポスト - 基本的な画像アップロードで実装した画像のアップローダーでは、画像として無効なファイルをアップロードすることができてしまいます。

Started POST "/microposts" for 172.17.0.1 at 2020-01-08 09:50:59 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by MicropostsController#create as HTML
  Parameters: {...略, "micropost"=>{"content"=>"upload invalid format file", "picture"=>#<ActionDispatch::Http::UploadedFile:0x00007fd16c2513a0 @tempfile=#<Tempfile:/tmp/RackMultipart20200108-15302-1ati1vk.drawio>, @original_filename="Untitled Diagram.drawio", @content_type="application/octet-stream", @headers="Content-Disposition: form-data; name=\"micropost[picture]\"; filename=\"Untitled Diagram.drawio\"\r\nContent-Type: application/octet-stream\r\n">}, "commit"=>"Post"}
  User Load (6.8ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  SQL (22.5ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at", "picture") VALUES (?, ?, ?, ?, ?)  [["content", "upload invalid format file"], ["user_id", 1], ["created_at", "2020-01-08 09:51:00.104135"], ["updated_at", "2020-01-08 09:51:00.104135"], ["picture", "Untitled_Diagram.drawio"]]
   (17.5ms)  commit transaction
Redirected to http://localhost:8080/
Completed 302 Found in 194ms (ActiveRecord: 46.9ms)

「Untitled Diagram.drawio」というのは、明らかに画像ファイルではありません。しかし、それでも上記のようにマイクロポストの投稿は正常に完了してしまうのです。

このときのフィードの内容は以下のようになります。

スクリーンショット 2020-01-08 18.53.32.png

「Untitled diagram」という文字になっている部分が、画像として無効なファイルを描画しようとした成れの果てです。

CarrierWaveの機能により、特定の拡張子のファイルしかアップロードできないようにする

実は、CarrierWaveによって作られたPictureアップローダーには、「特定の拡張子のファイルしかアップロードできないようにする」という機能があります。app/uploaders/picture_uploader.rbのソースコード中にも、当該機能を使うためのコードがコメントアウトされた状態で存在します。

app/uploaders/picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base
  # ...略

  # Add a white list of extensions which are allowed to be uploaded.
  # For images you might use something like this:
  # def extension_whitelist
  #   %w(jpg jpeg gif png)
  # end

  # ...略
end

当該コードを以下のように変更すれば、指定した拡張子のファイルしかアップロードできないようになります。

app/uploaders/picture_uploader.rb
  class PictureUploader < CarrierWave::Uploader::Base
    ...略

    # Add a white list of extensions which are allowed to be uploaded.
    # For images you might use something like this:
-   # def extension_whitelist
-   #   %w(jpg jpeg gif png)
-   # end
+   def extension_whitelist
+     %w(jpg jpeg gif png)
+   end

    ...略
  end

上記の例の場合、.jpg/.jpeg/.gif/.png以上4つの拡張子をもつファイルのみがアップロード可能になります。

モデルにおけるバリデーションの実装

画像のファイルサイズに対するバリデーション

ファイルサイズに対するバリデーションは、独自に実装する必要がある

Railsチュートリアル本文には、「ファイルサイズに対するバリデーションはRails標準のバリデーション1としては用意されておらず、独自に定義する必要がある」とあります。まずはファイルサイズに対するバリデーションの実体そのものを定義する必要があります。

今回のサンプルアプリケーションでは、画像はマイクロポストに関連付けされます。そのため、画像のファイルサイズに対するバリデーションもMicropostモデルに実装されるのが自然です。

ファイルサイズに対するバリデーションの実装

メソッド名はpicture_sizeとします。定義する箇所は、app/models/micropost.rbprivateメソッド以降の部分となります。

def picture_size
  if picture.size > 5.megabytes
    errors.add(:picture, "should be less than 5MB")
  end
end

picture_sizeメソッドは、「アップロードされた画像のファイルサイズが5MBを超える場合、バリデーションを失敗させ、errorsコレクションにカスタマイズされたエラーメッセージを追加する」という動作をします。

errorsコレクションの使い方の詳細については、バリデーションエラーに対応する - Active Record バリデーション - Railsガイドを参照ください。

ファイルサイズに対してバリデーションを行うようにする

上述picutre_sizeバリデーションをapp/models/micropost.rb内でvalidateメソッドによって呼び出すようにすれば、ファイルサイズに対してバリデーションが行われるようになります。

validatesではありません。validateです。私は一度ここを間違えました。)

validate :picture_size

独自に定義したバリデーションに対してvalidateメソッドで呼び出しを行う場合、validateメソッドの第1引数には、「当該バリデーション名のシンボル」を与えます。

ファイルサイズのバリデーションの実装に必要となる、Micropostモデルに対する変更の全体像

上記を踏まえ、app/models/micropost.rb全体の変更内容は以下のようになります。

app/models/micropost.rb
  class Micropost < ApplicationRecord
    belongs_to :user
    default_scope -> { order(created_at: :desc) }
    mount_uploader :picture, PictureUploader
    validates :user_id, presence: true
    validates :content, presence: true, length: { maximum: 140 }
+   validate  :picture_size
+
+   private
+
+     # アップロードされた画像のサイズをバリデーションする
+     def picture_size
+       if picture.size > 5.megabytes
+         errors.add(:picture, "should be less than 5MB")
+       end
+     end
  end

ビューに対するバリデーションの実装

ファイルアップロード用のフォーム部品に、受理するファイル形式を追加する

ファイルアップロード用のフォーム部品で特定のファイル形式のみを受理するようにするためには、file_fieldタグにacceptオプションを与えて使います。

<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>

受理するファイル形式はMIMEタイプで記述します。カンマ区切りで複数記述することができます。

大きすぎるファイルサイズに対して警告を出すようにする

Javascript(正確にはjQuery)を用いて実装してみます。

$('#micropost_picture').bind('change', function() {
  var size_in_megabytes = this.files[0].size/1024/1024;
  if (size_in_megabytes > 5) {
    alert('Macimum file size is 5MB. Please choose a smaller file.');
  }
});

上記Javascriptコードの処理内容は以下のようになります。

  • CSS idのmicropost_pictureを含む要素を見つけ出す
    • マイクロポスト投稿フォームの画像アップロード用パーツを指す
  • 上記要素に変化があった場合、このJavascriptが動作する
  • ファイルサイズが5MB以上となった場合、警告メッセージを出す

マイクロポスト投稿フォームの実装の変更

ここまでに書かれたことを総合すると、app/views/shared/_micropost_form.html.erbの変更の全体像は以下のようになります。

app/views/shared/_micropost_form.html.erb
  <%= form_for(@micropost) do |f| %>
    <%= render 'shared/error_messages', object: f.object %>
    <div class="field">
      <%= f.text_area :content, placeholder: "Compose new micropost..." %>
    </div>
    <%= f.submit "Post", class: "btn btn-primary" %>
    <span class="picture">
-     <%= f.file_field :picture %>
+     <%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
    </span>
  <% end %>
+
+ <script type="text/javascript">
+   $('#micropost_picture').bind('change', function() {
+     var size_in_megabytes = this.files[0].size/1024/1024;
+     if (size_in_megabytes > 5) {
+       alert('Macimum file size is 5MB. Please choose a smaller file.');
+     }
+   });
+ </script>

ビューに対するバリデーションの限界

もっとも、警告を無視して大きすぎるアップロードを強行することは可能です。そのようなアップロードの強行を防ぐような実装も可能ですが、それでも限界はあります。例えば、以下のような操作をされた場合、ビュー側のバリデーションでは対策のしようがありません。

  • Webブラウザのインスペクター機能でJavaScriptをいじる
  • フォームを使わずに、curlなどで直接POSTリクエストを実行する

このように、ビューに対するバリデーションには限界があります。ビューに対するバリデーションの限界を超えるためには、モデルやアップローダー段階でのバリデーションが必要となるのです。


  1. presencelength等が該当します。 

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

#Rails migration / Remove unique index / Mysql2::Error: Cannot drop index 'index_xxx_on_yyy_id': needed in a foreign key constraint

I dont know its best way or wrong way
but it seems to work

model Annotation = DB table schema info

Wanna delete UNIQUE index

# == Schema Information
#
# Table name: books
# user_id                          :bigint           not null
#
# Indexes
#
- # index_books_on_user_id (user_id)
+ # index_books_on_user_id (user_id) UNIQUE
#
#
# Foreign Keys
#  fk_rails_...  (user_id => sims.id)
#

Model

class Book < ApplicationRecord
  belongs_to :user
end

Fail Migration

class RemoveUniqueIndexUserIdFromBoook < ActiveRecord::Migration[5.2]
  def change
    # Mysql2::Error: Cannot drop index 'index_books_on_user_id': needed in a foreign key constraint
    remove_index :books, :user_id
  end
end

Succeed Migration

class RemoveUniqueIndexUserIdFromBoook < ActiveRecord::Migration[5.2]
  def change
    # Anti migration error
    # Mysql2::Error: Cannot drop index 'index_books_on_user_id': needed in a foreign key constraint
    remove_foreign_key :books, name: "fk_rails_80340687ab"
    remove_index :books, :user_id
    add_index :books, :user_id
    add_foreign_key :books, :users
  end
end

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2950

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

Uncaught TypeError: Cannot read property 'length' of undefined のエラー解決例

1.エラーの状況とそのエラーに関係したファイル

<エラー文>
スクリーンショット 2020-01-19 5.15.53.png

<エラーに関連したファイル>

app/asset/javascripts/users.js
$(function() {
  function addUser(user) {
    let html = `
      <div class="chat-group-user clearfix">
        <p class="chat-group-user__name">${user.name}</p>
        <div class="user-search-add chat-group-user__btn chat-group-user__btn--add" data-user-id="${user.id}" data-user-name="${user.name}">追加</div>
      </div>
    `;
    $("#user-search-result").append(html);
  }

  function addNoUser() {
    let html = `
      <div class="chat-group-user clearfix">
        <p class="chat-group-user__name">ユーザーが見つかりません</p>
      </div>
    `;
    $("#user-search-result").append(html);
  }
  function addDeleteUser(name, id) {
    let html = `
    <div class="chat-group-user clearfix" id="${id}">
      <p class="chat-group-user__name">${name}</p>
      <div class="user-search-remove chat-group-user__btn chat-group-user__btn--remove js-remove-btn" data-user-id="${id}" data-user-name="${name}">削除</div>
    </div>`;
    $(".js-add-user").append(html);
  }
  function addMember(userId) {
    let html = `<input value="${userId}" name="group[user_ids][]" type="hidden" id="group_user_ids_${userId}" />`;
    $(`#${userId}`).append(html);
  }
  $("#user-search-field").on("keyup", function() {
    let input = $("#user-search-field").val();
    $.ajax({
      type: "GET",
      url: "/users",
      data: { keyword: input },
      dataType: "json"
    })
      .done(function(users) {
        $("#user-search-result").empty();

        if (users.length !== 0) {         #エラーの原因となった行(43行目)
          users.forEach(function(user) {
            addUser(user);
          });
        } else if (input.length == 0) {
          return false;
        } else {
          addNoUser();
        }
      })
      .fail(function() {
        alert("通信エラーです。ユーザーが表示できません。");
      });
  });
  $(document).on("click", ".chat-group-user__btn--add", function() {
    const userName = $(this).attr("data-user-name");
    const userId = $(this).attr("data-user-id");
    $(this)
      .parent()
      .remove();
    addDeleteUser(userName, userId);
    addMember(userId);
  });
  $(document).on("click", ".chat-group-user__btn--remove", function() {
    $(this)
      .parent()
      .remove();
  });
});
app/controllers//users_controller.rb
class UsersController < ApplicationController

  def index
    return nil if params[:keyword] == ""
    @users = User.where(['name LIKE ?', "%#{params[:keyword]}%"] ).where.not(id: current_user.id).limit(10)
    respond_to do |format|
      format.html
      format.json
    end
  end

  def edit
  end

  def update
    if current_user.update(user_params)
      redirect_to root_path
    else
      render :edit
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email)
  end
end
app/views/messages/index.jason.jbuilder
json.array! @users do |user|
  json.id user.id
  json.name user.name
end

2.どんなエラー?

lengthの前のプロパティが定義されていないですよというものです。下記サイトが理解しやすかったためリンクを添付しておきます。

https://sbfl.net/blog/2019/02/01/javascript-error-messages/

ちなみに今回は"users(=複数の|user|ブロック変数)という変数はありません。"というエラーだったのですが、3番目のコードで示したように内容の定義は行っていました。

じゃあパソコンの勘違いですか?と言いたくなってくるのですが、悲しいかな勘違いしてるのはやっぱり筆者なのです。

3.エラーの原因と解決方法

結論から言いますと、index.jason.jbuilderのファイルの位置が間違っているため、それを正しい位置に配置することで解決します。
今回のindex.jason.jbuilderの保存先はapp/views/users/index.jason.jbuilderとする必要があります。

※下記にも示す理由から、0から作成されてる方は意味のわからないありえないミスだと思われると思います。ですが、理解の浅い初学者で誘導式の教材を解くときにつまづく内容なのです。ただし慣れた方でも普段使わないjavascript(rubyでない)ファイルを使用することによりヒューマンエラーが起こりやすくなり、こういったミスは起こる可能性はあるのではないかと筆者は考えています。

4.エラーの理由

railsのviewフォルダとファイルの命名規則として"コントローラフォルダのController以外の部分のフォルダ名"でかつ"コントローラ内で定義されているメソッド名の"とすることで互換性を持つような仕組みとしています。(今回は"usersフォルダでindexファイル"が正しい。)

つまり、コントローラの変数をviewファイルで反映させるためにはその命名規則に則った配置でなければなりませんでした。

素材を受け取り配置をすることで学ぶ段階の人だとこのミスは起こりやすいと思います。もし同じミスをした場合はぜひrailsの命名規則を学んでいただければ良いのではないかなと思います。

また、railsに慣れた方でも斜め上のミスからjavascriptにエラーが出て戸惑う可能性もありますのでこんな例もあるんだよということを知っていただければ良いのかなと思います。

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

[JavaScript,Rails]Uncaught TypeError: Cannot read property 'length' of undefined のエラー解決例

1.エラーの状況とそのエラーに関係したファイル(Railsの中のJavaScriptのエラー)

<エラー文>
スクリーンショット 2020-01-19 5.15.53.png

<エラーに関連したファイル>

app/asset/javascripts/users.js
$(function() {
  function addUser(user) {
    let html = `
      <div class="chat-group-user clearfix">
        <p class="chat-group-user__name">${user.name}</p>
        <div class="user-search-add chat-group-user__btn chat-group-user__btn--add" data-user-id="${user.id}" data-user-name="${user.name}">追加</div>
      </div>
    `;
    $("#user-search-result").append(html);
  }

  function addNoUser() {
    let html = `
      <div class="chat-group-user clearfix">
        <p class="chat-group-user__name">ユーザーが見つかりません</p>
      </div>
    `;
    $("#user-search-result").append(html);
  }
  function addDeleteUser(name, id) {
    let html = `
    <div class="chat-group-user clearfix" id="${id}">
      <p class="chat-group-user__name">${name}</p>
      <div class="user-search-remove chat-group-user__btn chat-group-user__btn--remove js-remove-btn" data-user-id="${id}" data-user-name="${name}">削除</div>
    </div>`;
    $(".js-add-user").append(html);
  }
  function addMember(userId) {
    let html = `<input value="${userId}" name="group[user_ids][]" type="hidden" id="group_user_ids_${userId}" />`;
    $(`#${userId}`).append(html);
  }
  $("#user-search-field").on("keyup", function() {
    let input = $("#user-search-field").val();
    $.ajax({
      type: "GET",
      url: "/users",
      data: { keyword: input },
      dataType: "json"
    })
      .done(function(users) {
        $("#user-search-result").empty();

        if (users.length !== 0) {         #エラーの原因となった行(43行目)
          users.forEach(function(user) {
            addUser(user);
          });
        } else if (input.length == 0) {
          return false;
        } else {
          addNoUser();
        }
      })
      .fail(function() {
        alert("通信エラーです。ユーザーが表示できません。");
      });
  });
  $(document).on("click", ".chat-group-user__btn--add", function() {
    const userName = $(this).attr("data-user-name");
    const userId = $(this).attr("data-user-id");
    $(this)
      .parent()
      .remove();
    addDeleteUser(userName, userId);
    addMember(userId);
  });
  $(document).on("click", ".chat-group-user__btn--remove", function() {
    $(this)
      .parent()
      .remove();
  });
});
app/controllers//users_controller.rb
class UsersController < ApplicationController

  def index
    return nil if params[:keyword] == ""
    @users = User.where(['name LIKE ?', "%#{params[:keyword]}%"] ).where.not(id: current_user.id).limit(10)
    respond_to do |format|
      format.html
      format.json
    end
  end

  def edit
  end

  def update
    if current_user.update(user_params)
      redirect_to root_path
    else
      render :edit
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email)
  end
end
app/views/messages/index.jason.jbuilder
json.array! @users do |user|
  json.id user.id
  json.name user.name
end

2.どんなエラー?

lengthの前のプロパティが定義されていないですよというものです。下記サイトが理解しやすかったためリンクを添付しておきます。

https://sbfl.net/blog/2019/02/01/javascript-error-messages/

ちなみに今回は"users(=複数の|user|ブロック変数)という変数はありません。"というエラーだったのですが、3番目のコードで示したように内容の定義は行っていました。

ということはパソコンの勘違いじゃないですか?と言いたくなってくるのですが、悲しいかな勘違いしてるのはやっぱり筆者なのでした。

3.エラーの原因と解決方法

結論から言いますと、index.jason.jbuilderのファイルの位置が間違っているため、それを正しい位置に配置することで解決します。
今回のindex.jason.jbuilderの保存先はapp/views/users/index.jason.jbuilderとする必要があります。

※下記にも示す理由から、0から作成されてる方は意味のわからないありえないミスだと思われると思います。ですが、理解の浅い初学者で誘導式の教材を解くときにつまづく内容なのです。ただし慣れた方でも普段使わないjavascript(rubyでない)ファイルを使用することによりヒューマンエラーが起こりやすくなり、このような保存先を間違えるというミスは起こる可能性はあるのではないかと筆者は考えています。

4.エラーの理由

railsのviewフォルダとファイルの命名規則として"コントローラフォルダの〇〇〇_controllerの〇〇〇の部分のフォルダ名"でかつ"コントローラ内で定義されているメソッド名のファイル名"とすることで互換性を持つような仕組みとしています。(今回は"usersフォルダでindexファイル"が正しい。)

※そのほかのファイルは全て部品化とみなすため'_'から始まるファイル名を名付けます。

つまり、コントローラの変数をviewファイルで反映させるためにはその命名規則に則った配置でなければなりませんでした。

素材を受け取り配置をすることで学ぶ段階の人だとこのミスは起こりやすいと思います。もし同じミスをした場合はぜひこの記事からrailsの命名規則を学んでいただければ良いのではないかなと思います。

また、railsに慣れた方でもjavascriptファイルにエラーが出て、関連ファイルの中をぐるぐる回ってしまいドツボにハマる可能性もありますので、この例のようにファイルの配置ミスというミスもあるんだよということを知っていただければ良いのかなと思います。

5.中級者以上の方へ

慣れた方にとっては今回のエラー例は稀なもので、基本的にこのエラーはグローバル変数がうまく定義できていないことが多いと思います。

そういった記事は外国の方のものも含めればたくさんあったのでそちらの記事を見ていただければ解決すると思います。

今回の記事は期待はずれだったかもしれませんが、エラーに対する多角的な視野を持つ参考としていただければと思います。

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

RailsのTime.zone.parseでハマった

Time.zone.parseに渡す引数は日付っぽいものであり、かつvalidであればきちんとparseされ、
日付として認識されるフォーマットでありつつも値がおかしい(2020-99-99 99:99:99など)場合はArgumentErrorがraiseされ、
そもそも日付として認識されない文字列はnilを返す。
nilを返すんですね。(2回目)

最後の場合もArgumentErrorがraiseされる想定だったためハマった。

要件としては「ユーザーが自由入力できる入力欄において、日付としてvalidな文字列であればそのまま保存し、invalidな文字列であればエラーを吐かせる」ものだったのであまりないパターンかとは思う。

参考

リファレンス

https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html#method-i-parse
20:00:00とかもいけるっぽい(日付は現在日付になる)

Qiita

https://qiita.com/slightair/items/fa5f63016c0c848111a9
ここでもnilになる挙動について少し触れられている

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