20200520のRubyに関する記事は18件です。

【Rails】ActionCableを使う際のnginxの設定でハマった

はじめに

Ruby on Railsで作ったアプリにリアルタイムチャットの機能を付けたいと思って、ActionCableを使ったときの話です。

ローカル環境ではうまく動いたのですが、本番環境ではリアルタイムチャットの部分が動きませんでした。

原因を調べたところ、ActionCableで使うwebsocketという通信をするために、それに対応するようなnginxの設定が必要なようでした。

 (為参考)元々のnginxの設定

元々のnginxの設定です。
修正前と修正後の差分がわかりやすいようにこちらも載せます。
ActionCableを実装する前はこれで問題なく動いていました。

nginx.conf
 # https://github.com/puma/puma/blob/master/docs/nginx.md
upstream app {
  server unix:///app/tmp/sockets/puma.sock;
}

server {
  listen 80;
  server_name ***.***.***.***; # アプリのIPアドレス

  keepalive_timeout 5;

  # static files
  root /app/public;

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;

    # static files
    if (-f $request_filename) {
      break;
    }
    if (-f $request_filename.html) {
      rewrite (.*) $1/index.html break;
    }
    if (-f $request_filename.html) {
      rewrite (.*) $1.html break;
    }

    if (!-f $request_filename) {
      proxy_pass http://app;
      break;
    }
  }

  location ~* \.(ico|css|gif|jpe?g|png|js)(\?[0-9]+)?$ {
    expires max;
    break;
  }
}

ActionCableに対応したnginxの設定

websocketの通信を扱えるようにするために
location /cable以下を追加しました。

nginx.conf
# https://github.com/puma/puma/blob/master/docs/nginx.md
upstream app {
  server unix:///app/tmp/sockets/puma.sock;
}

server {
  listen 80;
  server_name ***.***.***.***; # アプリのIPアドレス
  keepalive_timeout 5;

  # static files
  root /app/public;

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;

    # static files
    if (-f $request_filename) {
      break;
    }
    if (-f $request_filename.html) {
      rewrite (.*) $1/index.html break;
    }
    if (-f $request_filename.html) {
      rewrite (.*) $1.html break;
    }

    if (!-f $request_filename) {
      proxy_pass http://app;
      break;
    }
  }

  #以下を追加

  location /cable {
    proxy_http_version 1.1;
    proxy_set_header Upgrade websocket;
    proxy_set_header Connection Upgrade;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_pass http://app/cable;
  }

  #追加部分ここまで

  location ~* \.(ico|css|gif|jpe?g|png|js)(\?[0-9]+)?$ {
    expires max;
    break;
  }
}

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

paizaランクCの達成方法(timesメソッド)

はじめに

paizaランクCを達成するにはどうすれば良いか?
目安を解説しました。

前提

筆者がRubyを主に学習しているので、Rubyで解説しています。

実行

Cランクを目指すには、繰り返し条件を使いこなす必要があります。
私が良く使うのはtimesメソッドです。

t = gets.to_i

t.times do
  puts "hello!"
end

以上のプログラムを実行すれば、入力した回数だけhello!と出力されます。
応用すれば、次のような事も出来ます。

t = gets.to_i
i = 1

t.times do
  puts i
  i += 1
end

以上プログラムを実行すれば、1から入力され数までの整数が順番に出力されます。
配列も使用できます。

t = gets.to_i
num = []
i = 1

t.times do
  num << i
  i += 1
end

このプログラムを実行すれば、[1, 2, 3, …,i]という配列ができます。

paizaランクCを目指すには、まずはこのtimesメソッドを使いこなすことが必要です。

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

Rails6について

前提

Rails6について学んだことを書いていきます。

macOS
Docker・Docker Composeインストール済み

本題

仮想環境を使う理由

Railsアプリケーションの開発をしようとするときに問題がある。
それは開発用マシン(作業マシン)と実運用を行うマシン(プロダクションマシン)の環境が異なること。
ここで言う環境とは、OSの種類やバージョン、あるいはOS上で動くソフトウェアやライブラリの種類やバージョンをさす。
Ruby on RailsはWindowsでもMacOSでもLinuxでも動作するように設計されている。
しかし、Railsを拡張するために導入するGemパッケージは必ずしもそうではない。
仮に動いたとしても、特定の環境では振る舞いが微妙に異なったり、不具合が出たりすることがある。

この問題の解決方法として、作業マシンとプロダクションマシンの環境を一致させること。
そこで現れるのが仮想環境と言うオプション。
Windows、MacOS、ubuntuなどのデスクトップOSの上に仮想環境を構築し、そこにLinuxベースのServerOSをインストールする。

開発中のRailsアプリケーションは仮想環境で動作させる。
デスクトップOS上のWebブラウザからRailsアプリケーションにアクセスして動作を確認。
他方、Railsアプリケーションのソースコードは共有フォルダの機能によって、デスクトップOS上で開いて編集する。
こうすれば、使い慣れたテキストエディタやIDEを使い続けながら、プロダクションマシンと同等の環境下でRailsアプリケーションの開発が行える。

Dockerとは

仮想環境を提供するオープンソース・ソフトウェア。
設定の容易さや起動の早さが人気。
個々の仮想環境をコンテナと呼ぶ。
コンテナの内容はDockerfileと呼ばれるテキストファイルで記述される。
このファイルがあれば様々なOS上でコンテナを復元できる。

Docker-composeとは

Dockerを用いて、Railsアプリケーションを開発したり、プロダクション環境で動かすとき、Railsアプリケーションとデータベースサーバーを別々のコンテナとして構築するのが一般的。
Docker-composeはこれらの複数のコンテナをまとめて起動・停止するためのツール。
docker-compose upというコマンドを実行するだけで、Webアプリケーションを構成する全てのコンテナ群が動き出す。

Rails開発環境の構築

DockerとDocker Composeを用いてRails開発用コンテナ群を構築。

terminal.
$ git clone https://github.com/oiax/rails6-compose.git
$ cd rails6-compose
$ ./setup.sh

コンテナ群の起動と停止

コンテナ群を全て起動するにはターミナルで下記のコマンドを実行。
オプション-dはコンテナをデーモン(バックグラウンドプロセス)として動かすためのもの。

terminal.
$ docker-compose up -d

コンテナ群を停止するにはターミナルで下記のコマンドを実行。

terminal.
$ docker-compose stop

webコンテナにログイン

terminal.
$ docker-compose exec web bash

Railsのバージョン確認

bash-4.4$ rails --version

Rails6.0.0と結果出力されればOK!

rails newコマンドで新規Railsアプリケーションの作成。

bash-4.4$ rails new アプリ名 -d postgresql --skip-test-unit

--skip-test-unitはTest::Unit関連のコードの生成を省略するためのオプション。

中身の確認

bash-4.4$ ls -a アプリ名/

Gemfileの編集

gem "bcrypt" #パスワードの暗号化
gem "rails-i18n" #railsが出力するエラーメッセージ、日付、時刻、通貨単位などの翻訳ファイルを集めたもの
gem "kaminari" #ページネーション機能
gem "date_validator" #日付のバリデーションを行う
gem "valid_email2" #メールアドレスへのバリデーションを行う
gem "nokogiri" #XML/HTMLの解析・生成のため

※email_validatorはバージョンアップでバリデーションの基準が緩くなり、実用性が乏しくなった。
例えば、@を2個含むようなメールアドレスを有効と判定してしまう。

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

【Ruby on Rails】undefined method `id' for nil:NilClassのエラー解消方法

"undefined method `id' for nil:NilClass"のエラー

スクリーンショット 2020-05-20 19.33.13.png

もし打ち間違えなどがないのにこのようなNoMethodErrorのundefined method `id' for nil:NilClassのエラーというエラーがでた場合どんなことが原因として考えられるか。メソッドが定義されていないというエラーがでました...どうしてきちんと定義したのにこのようなエラーがでたのだろう...

この実装はログインしたユーザーには自分の情報が見れても、他のユーザーには見せたくない場合にこのような実装を用います。
<% if @user.id == current_user.id %>

実装したコードの記述に間違いがないのにどうしてこうなったんだろうと考えてあることに気がつきます!

ログインをしていませんでした!

ログインをしていないと当然成立しない処理なので当然ですよね^_^;
ログインしてから再度確認をしたら無事にエラーが解消されました!

S__56467458.jpg

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

100日後に1人前になる新人エンジニア(0日目)

100日後に1人前になる新人エンジニア(自己紹介)

営業社員から転職をしてエンジニアになりました。
Javaでポートフォリオを作り、今後はRailsで開発を行っていく予定です。

筋トレが好きです。
社会人の2年目です。
餃子も好きです。

目的

  • 技術的な蓄積
  • アドバイスをいただく
  • たくさんLGTMが欲しい

以上が主な目的です。
特に3つ目は私のエンジニアとしてのモチベーションに関わります笑

100日で1人前

なれるかはわからないです。
ただ期限を持ってやることが大切だと思うので
ワニさんを見習って100日にしました。ちなみに今日はまだ0日目

1日1日を大切にして進んでいきたいと思っています!
本格的な更新は明日からです。

本日はまだ導入です。

お願いします

多くの人に見てもらうこと、いろんな意見をいただくことが大切だと感じています。

肯定的な意見でも、否定的な意見でもすべて僕の財産になっていくと思うので、
何か思ったことがありましたらコメントをいただけると幸いです。

それではまた明日

1人前のエンジニアになるまであと100日

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

AtCoderでRuby学習8【第一回アルゴリズム実技検定 2倍チェック 】正規表現

はじめに

Ruby学習の一環として「アルゴリズム実技検定」に挑戦します。
そのための学習の中で学んだことをアウトプットしていきます。
今回は「第一回アルゴリズム実技検定」の一問目(2倍チェック)より。
第一回アルゴリズム実技検定 過去問

自分の解答と、解答する中で学んだ表現・メソッドなどを紹介していきます。

問題

3桁の整数を受け取り、それを2倍にして出力するプログラムの作成をします。
しかし、入力される文字列 S には英小文字が紛れ込むことがあります。その場合はエラーを出力します。
S が3桁の整数である場合('0'で始まる場合も含む)はその2倍の整数を出力し、そうでない場合は'error'と出力すること。

制約
・S は長さ3の文字列である
・S の各文字は、数字もしくは英小文字である

入力は以下の形で与えられる。

S

入力例①
678
入力例②
4g4
出力例
# 入力例①678
=> 678
# 入力例②4g4
=> error

解答

まずは僕が提出した解答から。

s = gets.chomp
print /^[0-9]+$/ === s ? s.to_i * 2 : "error"

受け取った文字列を、正規表現を用いて各桁の文字列が0から9であるかを判定し、
真ならInteger型に変換した後に2倍した数字を、偽ならerrorと出力するようにしました。

正規表現

ここで、正規表現とは何か、を改めて確認しておきたいと思います。
初めは、「正規表現」という言葉自体わかりづらかったのですが、
こちらの記事を読んでなんとなく腑に落ちました。
正規表現とは?

正規表現とは「様々な文字列をひとつの文字列で表現する表現法」
そして「文字列の検索や置換に便利」

他にもあるかもしれませんが、"他"は、その"他"に出会った時に覚えることにします。

整数を表現した正規表現で判定する

正規表現とは何かをひとまず定義したところで、今回用いた正規表現を見ていきます。

/^[0-9]+$/ === s

ここで使用している正規表現に関係するものは以下の通り

===
左辺の正規表現の文字列パターンが、右辺の文字列に含まれているかを判定します。
左辺と右辺は入れ替えて記述してはいけません。

/ /
//で囲むことで正規表現オブジェクトを生成することが出来ます。
つまり、「//で囲まれたものが正規表現だよ」という宣言になります。

[ ]
角括弧[ ]で挟まれた部分を文字クラスと言います。
1個以上の文字を列挙したもので、いずれかの1文字にマッチします(上記では0から9のうち1文字)。

-(ハイフン)
文字の範囲を示します。
[A-C]ならA,B,Cのいずれか、[0-9]なら0から9のうちいずれか、となります。

^(/^xxx/)
行頭。文字列の先頭や改行文字の直後の位置にマッチします。

+
1回以上の繰り返しを表します。

$(/xxx$/)
行末。文字列の末尾や改行文字の直前の位置にマッチします。

よって、下記の記述の意味は、
「sは、全ての文字が0から9の数字から成る文字列か」を判定していることになります。

/^[0-9]+$/ === s

最後に

以上、「第一回アルゴリズム実技検定」の一問目(2倍チェック)を解く中で学んだ正規表現についてまとめました。
正規表現は、とにかく問題をこなして慣れていくのが大事かなと思います。
使えそうな場面では、積極的に調べながら使っていきたいです。

もし間違いなどございましたら、ご指摘いただけると嬉しいです。

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

はじめてのWSLとrubyをインストールする

WSL (Windows Subsystem for Linux)

Windows上でLinuxを動かすことができる(詳細は他記事に譲る)
- Linuxカーネルが動作するLinux環境
- Ubuntuの種類が3種類ほどあるので、今回はUbuntuを使う

DockerやVirtualBoxなどは多少触ったことありますが、勉強がてら使ってみます。

Ruby install

まずはUpdate。最初に実行しましょう!

$ sudo apt-get update

Get rbenv

$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv

Set Path

$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc
$ source ~/.bashrc

Get install Command

$ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
$ sudo apt install build-essential

Install Ruby

$ rbenv install 2.7.1
Downloading ruby-2.7.1.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.1.tar.bz2
Installing ruby-2.7.1...


BUILD FAILED (Ubuntu 20.04 using ruby-build 20200519)

Inspect or clean up the working tree at /tmp/ruby-build.20200520163201.5902.R4jqnb
Results logged to /tmp/ruby-build.20200520163201.5902.log

Last 10 log lines:
        from ./tool/rbinstall.rb:846:in `block (2 levels) in install_default_gem'
        from ./tool/rbinstall.rb:279:in `open_for_install'
        from ./tool/rbinstall.rb:845:in `block in install_default_gem'
        from ./tool/rbinstall.rb:835:in `each'
        from ./tool/rbinstall.rb:835:in `install_default_gem'
        from ./tool/rbinstall.rb:799:in `block in <main>'
        from ./tool/rbinstall.rb:950:in `block in <main>'
        from ./tool/rbinstall.rb:947:in `each'
        from ./tool/rbinstall.rb:947:in `<main>'
make: *** [uncommon.mk:373: do-install-all] Error 1

ログファイルを見ると、opensslがないためインストールする

$ sudo apt-get install openssl

引き続き検証しながら、完結したら続き更新します

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

rails の link_to

railsのhelper(link_to・form_withなど)でget以外のメソッドが使えなくなるとき。
それは、jsの設定がうまくいってないことが多々あります。

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

Gem::ConflictErrorについて(activesupport)

今回はminitestを実行しようとした際に急にエラーが出始めましたのでその経緯と解決策を記載します。

環境は下記でした。

ruby 2.6.3
Rails 5.1.7
minitest (5.14.1, 5.14.0, 5.11.3, 5.10.3, 5.10.1)

この記事は以下のテキストを進めていた際に発生したエラーについての執筆です。

※書籍内容に誤り等があるわけではなく、他の作業と並行して進めていた際に意図せずgemを更新してしまったことによるエラーとその解決法についての執筆です

minitestのエラーに関しては著者様のほうでも各種の解決策を投稿していただいておりましたが、どれにも当てはまらない状況でした。
書籍進行中に急にテストでエラーが出るようになったので、おそらく、合間に行った別の作業が要因かと思いましたのでそのような経験がある方のみ、参考にしてください。

以下、エラー内容です。

ec2-user:~/environment/ruby-book $ ruby lib/sample_test.rb
Traceback (most recent call last):
        9: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:68:in `block in autorun'
        8: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:126:in `run'
        7: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:97:in `load_plugins'
        6: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:97:in `each'
        5: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:103:in `block in load_plugins'
        4: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `                   require'
        3: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        2: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/railties-5.1.7/lib/minitest/rails_plugin.rb:2:in `<top (required)>'
        1: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_require.rb:117:in `require'
/home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_require.rb:117:in `require': cannot load such file -- rails/test_unit/reporter (LoadError)
        12: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:68:in `block in autorun'
        11: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:126:in `run'
        10: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:97:in `load_plugins'
         9: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:97:in `each'
         8: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:103:in `block in load_plugins'
         7: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
         6: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
         5: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/railties-5.1.7/lib/minitest/rails_plugin.rb:2:in `<top (required)>'
         4: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_require.rb:34:in `require'
         3: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_require.rb:123:in `rescue in require'
         2: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems.rb:218:in `try_activate'
         1: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/specification.rb:1415:in `activate'
/home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/specification.rb:2298:in `raise_if_conflicts': Unable to activate railties-5.1.7, because activesupport-6.0.3.1 conflicts with activesupport (= 5.1.7) (Gem::ConflictError)
        13: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:68:in `block in autorun'
        12: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:126:in `run'
        11: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:97:in `load_plugins'
        10: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:97:in `each'
         9: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/minitest-5.14.1/lib/minitest.rb:103:in `block in load_plugins'
         8: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
         7: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
         6: from /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/railties-5.1.7/lib/minitest/rails_plugin.rb:2:in `<top (required)>'
         5: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_require.rb:34:in `require'
         4: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/core_ext/kernel_require.rb:123:in `rescue in require'
         3: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems.rb:217:in `try_activate'
         2: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems.rb:224:in `rescue in try_activate'
         1: from /home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/specification.rb:1415:in `activate'
/home/ec2-user/.rvm/rubies/ruby-2.6.3/lib/ruby/site_ruby/2.6.0/rubygems/specification.rb:2298:in `raise_if_conflicts': Unable to activate railties-5.1.7, because activesupport-6.0.3.1 conflicts with activesupport (= 5.1.7) (Gem::ConflictError)

activesupportの6.0.3.1と5.1.7がコンフリクトしているよというもの。

思い当たる要因としては、前日にsinatraでのアプリ制作時にいろいろと作業したことかと。

結果的にはgemをアンインストールすることで解決しました。

ec2-user:~/environment/ruby-book $ gem uninstall activesupport -v 6.0.3.1

You have requested to uninstall the gem:
        activesupport-6.0.3.1

activemodel-6.0.3.1 depends on activesupport (= 6.0.3.1)
activerecord-6.0.3.1 depends on activesupport (= 6.0.3.1)
If you remove this gem, these dependencies will not be met.
Continue with Uninstall? [yN]  y
Successfully uninstalled activesupport-6.0.3.1

上記で6.0.3.1のほうをアンインストールしました。

ec2-user:~/environment/ruby-book $ ruby lib/sample_test.rb
Run options: --seed 27045

# Running:

E

Finished in 0.002129s, 469.6838 runs/s, 0.0000 assertions/s.

  1) Error:
SampleTest#test_sample:
NoMethodError: undefined method `upcase' for nil:NilClass
Did you mean?  case
    lib/sample_test.rb:5:in `test_sample'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

ec2-user:~/environment/ruby-book $ ruby test/deep_freezable_test.rb
Run options: --seed 56480

# Running:

.

Finished in 0.002055s, 486.6611 runs/s, 486.6611 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

結果的に無事、テスト検証できました。

参考に、アンインストール後のバージョンを再度掲載します。

ec2-user:~/environment/ruby-book $ gem list | grep activesupport
activesupport (5.1.7, 5.1.6, 5.0.0)

同じように詰まった方の参考になれば。

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

新しくなったStripeのCheckoutスニペットジェネレータがでない時

Ruby on Rails などではレガシー版のCheckoutの実装しかない。2019年にリニューアルされたCheckoutはクライアントサイドのみの実装であれば導入がJavaScriptのコードをどこかのHTMLに貼り付けるだけで超簡単なのだが、そのCheckout機能を有効化しないとコードスニペットが出てこないという罠にハマった。

IMG_0394.PNG
入力フォームも洒落乙になった。

公式リファレンス

https://stripe.com/docs/payments/checkout/client
英語だけどこれを見つけられれば早い。ググってもレガシー版のリファレンスばかりがでてきた。

新しいChecoutのやり方Qiita

https://qiita.com/kskinaba/items/2ba42eda07227a9b96d5
基本はkん

スクリーンショット 2020-05-20 12.04.01.png
この中段のCheckoutの設定から
スクリーンショット 2020-05-20 11.59.35.png
クライアント専用組み込みを有効にする。
スクリーンショット 2020-05-20 12.02.13.png
すると左側に商品メニューがでて、商品を作成すると(本番とテストで商品は別扱いとなることに注意)
スクリーンショット 2020-05-20 14.06.45.png
Checkoutで使用というボタンが出る。これを押して出てくるJavaScriptコードを任意のWebページに貼り付けてやれば、あとはよしなにやってくれる決済ボタンが表示される。ちなみに商品とSKU(購入情報)を混同しないように注意。

補足

レガシー版のCheckoutのドキュメントではRails向けに書かれていたのに、新しい版のサーバーサイドも開発するドキュメントではRubyのコードとしてしか書かれていいないので、初心者からすると「ProgateでRailsはやったけど純粋なRubyのコードだけ見せられてもわけわからん…」となって結構ハードルが高い印象。

その他の参考記事

Railsでのやり方

https://qiita.com/roadfox303/items/b93e440c10af82bda7bd

レガシー版のRailsでのやり方

https://qiita.com/disk131/items/8381f35be9c70bd7b9e2
ただしURLの:idの取得をうまくしてくれずルーティングがうまくできませんみたいなエラーが出た。

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

Railsで多階層カテゴリー(ancestry)にカウンター(counter_culture)を実装する

肉 > 鶏肉 > むね肉のような多階層カテゴリーがあった時、
レシピにむね肉カテゴリーを登録したら、親カテゴリーである肉 > 鶏肉もカウントする機能を実装してみた。

ancestryを用いた多階層カテゴリーにカウンターをつけている記事が見つからず、自分で調べてみて実装したので解説してみる。

同じような人がいたらぜひ参考にしてください。

はじめに

環境

Ruby: 2.6.6
Rails: 6.0.2.2

前提

  • レシピを掲載するサイトを作成している。

  • レシピDBとカテゴリーDBがある。

  • レシピには複数のカテゴリーがついている。(中間テーブル)

  • カテゴリーは多階層カテゴリになっている。
    例: 肉 > 鶏肉 > むね肉
    ※ ancestryで実装(経路列挙モデル)

  • 1カテゴリーに何個レシピがあるかカウンターを実装している

使用しているgem

ancestry: 3.0.7
counter_culture: 2.5.1

counter_cultureの導入はこちらを参照。
関連レコード数の集計(カウンターキャッシュ) - Qiita

現状

Recipes Table

Table name: recipes

 id            :bigint       not null, primary key
 title         :string       not null
 description   :string       not null

RecipeとCategoryの中間テーブル
1つのレシピに対して、複数のカテゴリーが登録される関係になっている。

Table name: recipe_categories

 id          :bigint           not null, primary key
 recipe_id   :bigint           not null
 category_id :bigint           not null

Categories Table

Table name: categories

 id            :bigint           not null, primary key
 name          :string           not null
 ancestry      :string
 recipes_count :integer          default(0), not null

Model

class Recipe < ApplicationRecord
  has_many :recipe_categories, dependent: :destroy
end
class Category < ApplicationRecord
  has_ancestry
  has_many :recipe_categories, dependent: :destroy
end
class RecipeCategory < ApplicationRecord
  belongs_to :recipe
  belongs_to :category
  counter_culture :category, column_name: :recipes_count
end

現状の問題点

1:1の関係だと下記を中間テーブルに入れるだけでカウントされる。

counter_culture :category, column_name: :recipes_count

今回は親カテゴリーも同時にカウントされる機能をつけたい。

例えば
照り焼きチキンのレシピに、むね肉のカテゴリーがついている。

Recipe.find(1)
=> #<Recipe
 id: 1,
 title: "照り焼きチキン"
>

RecipeCategory.find(1)
=> #<RecipeCategory
 id: 1,
 recipe_id: 1,
 category_id: 20
>

Category.find(20)
=> #<Category
 id: 20,
 name: "むね肉",
 ancestry: "1/10",
 recipes_count: 1
>

むね肉カテゴリーは以下の関係になっている。

id:1 > id:10 > id:20
肉 > 鶏肉 > むね肉

1:1のカウンターだと関連付けした「むね肉」はカウントされるが、「肉」と「鶏肉」にカウントされない。
親カテゴリーである「肉」と「鶏肉」もカウントされるように実装をしてみた。

親子カテゴリのカウンター実装方法

結論からいうと、こう実装した。

class RecipeCategory < ApplicationRecord
  belongs_to :recipe
  belongs_to :category
  counter_culture :category, column_name: :recipes_count,
    foreign_key_values: proc { |category_id| Category.find(category_id).path_ids }
end

foreign_key_valuesは外部キーを上書きするオプション。

通常だと関連づいているcategory_id: 20が外部キーとなり、Categoryのid:20のカウントが増減する。
foreign_key_valuesに配列形式で数値を渡すと、渡した値を外部キーとして対象すべてのカウントを増減する。

親子カテゴリーのすべてをカウントしたいため、例だと[1, 10, 20]の値を渡す形で実装する。

実装の解説

公式のリファレンスと翻訳解説されている記事を参考にした。
GitHub - magnusvk/counter_culture: Turbo-charged counter caches for your Rails app.
Rails向け高機能カウンタキャッシュ gem 'counter_culture' README(翻訳)|TechRacho

foreign_key_values: proc { |category_id| Category.find(category_id).path_ids }

proc { |category_id| }
まずprocで引数を渡す、この時に渡されるのは通常の時の外部キー(今回だと20)

Category.find(category_id)
対象のレコードオブジェクトを取得する。

.path_ids
ancestryの機能でpath_idsを行うとオブジェクトの親子関係のIDをリストで取得することができる。

参照: 【翻訳】Gem Ancestry公式ドキュメント - Qiita

実行してみると=> [1, 10, 20]が返ってくることがわかる。

Category.find(20).path_ids
  Category Load (1.0ms)  SELECT "categories".* FROM "categories" WHERE "categories"."id" = $1 LIMIT $2  [["id", 20], ["LIMIT", 1]]
=> [1, 10, 20]

[1, 10, 20]foreign_key_values:にわたすことで親子カテゴリーすべてをカウントできるようになった。

カウント再計算の機能実装

counter_cultureにはcounter_culture_fix_countsというカウントを再計算する機能が実装されている。
すでにあるデータをカウントしたり、カウントのズレを修正するために使うのだが、foreign_key_valuesオプションを使うとこの機能が使えない。

RecipeCategory.counter_culture_fix_counts
=> Fixing counter caches is not supported when using :foreign_key_values;
you may skip this relation with :skip_unsupported => true

調べたところ、foreign_key_valuesを使った場合、自分で再計算機能を実装するしかないようなので実装してみた。

結論から、こう実装した。
すべて計算し直しているため、件数が多いと時間がかかるかもしれない。
良い実装方法があったら教えてください。

class RecipeCategory < ApplicationRecord

  #...

  # Categoryのrecipes_countをすべて再計算する
  def self.fix_counts
    Category.update_all(recipes_count: 0)

    target_categories = pluck(:category_id)
    # categoriesの個数を計算する => { 1: 10, 10: 3, 20: 1 }
    count_categories = target_categories.group_by(&:itself).transform_values(&:size)
    count_categories.each do |category_id, count|
      count_up_categories = Category.find(category_id).path_ids
      Category.update_counters(count_up_categories, recipes_count: count)
    end
  end
end

実行している内容は以下のとおり。

  1. カウンターをすべて0にする
  2. 中間テーブルにあるcategory_idを配列で取得
  3. 個数を計算する => { 1: 10, 10: 3, 20: 1 }
  4. それぞれのカテゴリーの親子カテゴリーIDの配列を取得
  5. 親子カテゴリーすべてのカウントを個数分増やす

3の個数を計算する方法は、以下の記事を参考にした。
配列に同じ要素が何個あるかを数える - patorashのブログ

5のカウントを個数分増やす方法は、以下の記事を参考にした。
[Rails+MySQL] カウンターの実装 【なければ新規作成したいし、あれば適切にインクリメントしたい】 - Qiita
ActiveRecord::CounterCache::ClassMethods

これにて、RecipeCategory.fix_countsを実行するとカウントの再計算を行うようになった。

結論

RecipeCategoryを以下にすることで親子カテゴリーのカウンター機能を実装できた。

class RecipeCategory < ApplicationRecord
  belongs_to :recipe
  belongs_to :category
  counter_culture :category, column_name: :recipes_count,
                             foreign_key_values: proc { |category_id| Category.find(category_id).path_ids }

  # Categoryのrecipes_countをすべて再計算する
  def self.fix_counts
    Category.update_all(recipes_count: 0)

    target_categories = pluck(:category_id)
    # categoriesの個数を計算する => { 100: 3, 102: 2 }
    count_categories = target_categories.group_by(&:itself).transform_values(&:size)
    count_categories.each do |category_id, count|
      count_up_categories = Category.find(category_id).path_ids
      Category.update_counters(count_up_categories, recipes_count: count)
    end
  end
end

ぜひ、参考にしてみてください。
FBや改善点がありましたら、コメントで教えていただけると助かります。

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

devise ページネーション など 基礎

忘れがちな余談

コントローラーのアクション内に定義されたインスタンス変数はそのアクションのビューで使用することができる。

書くカラムに保存されている値のことをプロパティ値と言います。

ヘルパーメソッド

Railsでは予めviewでHTMLタグを出現させたり、テキストを加工するためのメソッドが用意されています。
これらをヘルパーメソッドと言います。
simple_format 改行で<br>を付与、文字列を<p>で囲ってくれるメソッド
form_tag    フォームを出現させるメソッド
link_to     aタグを出現させるメソッド
などがあります。

CSSファイルの読み込みはHTMLのheadタグ内のstylesheet_link_tagから読み込まれます。

require_treeは、引数として与えられたディレクトリ以下のCSSファイルをアルファベット順に全て読み込むという意味がある。.は引数であり、カレントディレクトリを表します。

application.css
require_tree .

MVC

ルーティング→コントローラー→モデル→ビュー の順に処理が行われるのがRailsです。
モデル・ビュー・コントローラーを使用して処理を行うシステムをそれぞれの頭文字をとってMVCと言います。

フォーム

フォームとはユーザーが情報を入力し、その情報をサーバーに送信するためのもの。
HTMLのコード内にform要素を作成し、その中にinput要素やtextarea要素を入れることで作成できます。
formタグでもフォームは作成できるが、セキュリティの観念上、Railsではヘルパーメソッドである、form_tag, form_for, form_withを使うのが推奨されている。

ユーザーがフォームに入力した値は、コントローラー内ではparamsという変数に入っています。
paramsはハッシュオブジェクトだと考えましょう。
ビューでフォームに入力された情報は、コントローラーにキーと一緒にパラメーターとして送られます。
このパラメーターはparamsというメソッドを使うことで取得することができます。
使用方法は

params[:キー名]

form内のinputやtextareaにあるname属性の値がキー名に当たります。

ストロングパラメーター

ストロングパラメーターとは、指定したキーを持つパラメーターのみを受け取るようにするもの。
不正な情報などを受け取らないようにするのでセキュリティを強化できます。
任意のストロングパラメーター名_paramsというメソッドを作成することで定義ができます。

xxxxx.rb
private
def sample_params    ###任意のストロングパラメーター名_params
  params.permit(:キー名, :キー名)
end

メソッドの中にはparams.permitと記述して受け取ることを許可するパラメーターのキー名を続けます。
permitメソッドは、ビューから送られてきた情報のハッシュであるparamsから後に続けたキーの名前がバリューと共に新たなハッシュとして生成します。

privateメソッド

class内でprivateと記述するとそれ以降に定義したメソッドはclassの外部から呼び出せなくなります。
外部から呼ばれたら困るメソッドを守れたり、private以下は読まなくて良くなるのでコードの可読性が上がります。

image_tag

Railsではロゴやバナーなど固定して表示したい画像はapp/assets/images以下に置くことが普通です。

sample.html.erb
<%= image_tag 'sample.png' %>

pry-rails

pry-railsとは、Gemの一つであり、Rails向けのデバッグツールです。
これを使うことによりバグの有無を確認したり処理を途中で止めてソースコードが正しいか確認できます。
特に使う機能がbinding.pryです。
これを使うことによって処理を途中で止めて、かつrailsコンソールと同じことができます。止めたいソースコードのところでbinding.pryを記述しましょう。変数の値を取得したり、binding.pryを複数個設置することによって処理が正しく行われているか確認することができます。
処理を再開させたい場合はターミナルでexitを入力します。

sample_controller.
def create
  Sample.create(sample_params)
binding.pry
end

private
def sample_params
  params.permit(:name, :image, :text)
end

投稿画面で投稿する。
(new.html.erb)

するとターミナルでコンソールが立ち上がる

ターミナル.
[1] pry(#<SamplesController>)> params
=> <ActionController::Parameters {"authenticity_token"=>"DnHBKql5oQq9lARgEnK8gQiFAhnTvIVa1XH8d13u7tuVjDUGWPM1eEjVbF44iFms57+VE7+vT3SYUePDy3lwWQ==", "name"=>"aa", "image"=>"https://kumamoto.photo/archives/_data/i/upload/2019/03/13/20190313172208-2113bf9d-la.jpg", "text"=>"dddddddd", "controller"=>"samples", "action"=>"create"} permitted: false>
[2] pry(#<SamplesController>)> params[:text]
=> "dddddddd"

コンソールでparamsと入力すると投稿画面で入力した値がキー名と共にパラメーターとして出力される。

exitと入力すると処理が再開する。

ルートパス

URLにパスを付けない、ホスト名だけのURLのことをルートパスと言います。
routes.rbでの書き方は、

routes.rb
root 'コントローラー名#アクション名'

orderメソッド

インスタンスを並び替えるメソッドです。
全てのレコードを取得してきた場合レコードを並び替えられます。

.order("カラム名 順序の指定”)

ASCは昇順 日付で言うと古い方から
DESCは降順 日付で言うと新しい方から

ページネーション

ページを分割させてくれるもの。1ページ当たりの表示件数を決められます。
gem kaminariを利用することで実装できます。

pageメソッド
このメソッドでページ数を指定できます。全体のページ数でなく、今何ページにいるかと言うことです。

perメソッド
1ページあたりの表示件数を決められます。

sample_controller.
@samples = Sample.order("id ASC").page(params[:page]).per(10)

pageメソッドの引数は、kaminari を導入した際に、モデルクラスにpageというキーが追加されておりそれを記述しています。そのキーの値はビューで指定したページの番号となります。

devise-ログイン機能-

ログイン機能を簡単に実装できるgemです。

Gemfileにdeviseを記述して
bundle install後
アプリのディレクトリで
rails g devise:installと叩くと
設定ファイルが作成されます。
config/initialize/devise.rbconfig/locales/devise.en.ymlです。

アカウント作成のためのモデルを作らなければいけないので
rails g devise モデルクラス名でモデルなどの関連ファイルを作成します。
モデルファイルや、マイグレーションファイルが作成されます。
それと同時に、routes.rb

devise_for :モデル名の複数形

が追記されます。
devise_forはログイン周りに必要なルーティングを一気に生成してくれるdeviseのヘルパーメソッドです。

current_user, user_signed_in?などのヘルパーメソッドも使えるようになっています。

rails g devise:viewsでデバイスのビューファイルを作成できます。

Prefix

ルーティングのパスが入った変数のことです。

unless文

if文の逆で、条件式が偽の時にする処理を記述します。

unless 条件式
 偽の時に実行する処理
end

条件分岐が1行の場合は、1行で記述できる。
puts "ログインしてください" unless user_signed_in?
###ログインしてなければ(つまり偽)"ログインしてください"が表示される

redirect_toメソッド

Railsでは基本的にアクションの処理が終わるとアクションと同名のビューに遷移しますが、redirect_toメソッドを使えば別のアクションを実行したり、ビューに遷移させたりできます。

redirect_to { action: :index }

キーにaction:をとり、値にアクションの名前のシンボル型をとります。
例えば、:indexなどです。{}は省略できます。

before_actionメソッド

コントローラー内のアクションが実行する前に実行したいメソッドを指定できます。シンボル型のメソッド名で指定できます。
オプションでexceptonlyがあり、アクションごとに制限をかけられます。

テーブルにカラムを追加

rails g  migration Addカラム名Toテーブル名 カラム名:型

configure_permitted_parametersメソッド

deviseでログイン機能を実装した際、パラメーターの受け取り方が普通とは違います。ストロングパラメーターは初期状態では、メールアドレスとパスワードのみ受け取る設定がされています。追加するにはconfigure_permitted_parametersメソッドを使います。

application_controller.rb
before_action :configure_permitted_parameters, if: :devise_controller?

def configure_permitted_parameters
  devise_parameter_sanitizer.permit(:追加したいアクション名, keys: [:追加するキー])
end
application_controller.rb
class ApplicationController < ActionController::Base
#  protect_from_forgery with: :exception
 before_action :configure_permitted_parameters, if: :devise_controller?

 def configure_permitted_parameters
  devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname])
 end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FontAwesomeのiconをulやliに使う方法

はじめに

某プログラミングスクールにてフリマアプリを作成する際にFontAwesomeのiconが□に文字化けして悩んだ事を記事にします!!

完成図

以下の様にしたい!

Image from Gyazo

しかし色々調べてみると

FontAwesomeのiconをulやolのlist-styleに使いたくなることってありますよね?
残念ながら、list-styleに直接アイコンを使うことは出来ないのです・・・・

え!?

しかし似た様な事はできる様なので早速実践!!

show.html.haml
          .main__showMain__contentRight__topContent__itemBox__optionalArea
            %ul
              %li.optionalBtn.likeBtn#likeBtn
                %i.fas.fa-star   お気に入り 0

            %ul.optional
              %li.optionalBtn
                =link_to  '' do
                  %i.fa-flag.likeIcon 不適切な商品の通報
show.scss
          &__optionalArea {
            display: flex;
            justify-content: space-between;
            ul {
              margin: 10px 0 0;
              display: flex;
              list-style: none;
              .likeBtn {
                margin-right: auto;
                padding: 6px 10px;
                border-radius: 40px;
                color: #3CCACE;
                border: 1px solid #ffb340;
                i {
                  display: inline-block;
                  font: normal normal normal 14px/1 FontAwesome;
                  font-size: inherit;
                  text-rendering: auto;
                  -webkit-font-smoothing: antialiased;
                }
                i:before {
                  content:'\f005';
                  font-weight: 400;
                  font-family: "Font Awesome 5 Free";
                }
              }
            }
            .optional {
              margin: 10px 0 0;
              display: flex;
              .optionalBtn {
                font-size: 14px;
                a {
                  padding: 6px 10px;
                  display: inline-block;
                  border-radius: 4px;
                  color: #333;
                  border: 1px solid #333;
                  text-decoration: none;
                  i {
                    display: inline-block;
                    font: normal normal normal 14px/1 FontAwesome;
                    font-size: inherit;
                    text-rendering: auto;
                    -webkit-font-smoothing: antialiased;
                  }
                  i:before {
                    content:'\f024';
                    font-weight: 400;
                    font-family: "Font Awesome 5 Free";
                  }
                }
              }
            }
          }

ポイントのなる所

show.scss
            ul {
              list-style: none;        ←これ
            }
              i:before {
                  content:'\f005';             ←これ
                  font-weight: 400;             ←これ
                  font-family: "Font Awesome 5 Free";   ←これ
                }

これで□に文字化けせずに表示できる様になりました!!

最後まで見て頂きありがとうございます!!

この記事が少しでも参考になれば嬉しいです:pray_tone2:

参考記事

https://qiita.com/ryounagaoka/items/0f7e81d703761b3e3e77

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

ancestoryを使ってカテゴリー機能を実装

はじめに

某プログラミングスクールに通いフリマアプリをチームで作成した際にカテゴリーの多階層構造の実装をしたので紹介します!!

完成図

Image from Gyazo

それでは紹介したいと思います。

1.モデルの作成

まず商品とカテゴリーを紐付けさせたいので『category』と『item』のモデルを作成します。

$ rails g model category 

$ rails g model item 

2.ancestoryの導入

Gemfileにancestoryを導入します。

gem 'ancestry'

ターミナルで下記コマンドを実行

$ bundle install

$ rails g migration add_ancestry_to_category ancestry:string:index

$ rails db:migrate

3.アソシエーション

ancestoryを使うことで多対多の関係が1対多の関係になりDB設計がシンプルになります。

category.rbにhas_ancestoryを記述

category.rb
class Category < ApplicationRecord
 has_many :items
 has_ancestry  
end
item.rb
class Item < ApplicationRecord
 belongs_to :category
end

3.データを導入

seeds.rbにカテゴリーのレコードを導入していきます。

seeds.rb
lady = Category.create(name: "レディース")
lady_1 = lady.children.create(name: "トップス")
lady_1.children.create([{name: "Tシャツ/カットソー(半袖/袖なし)"},{name: "Tシャツ/カットソー(七分/長袖)"},{name: "シャツ/ブラウス(半袖/袖なし)"},{name: "シャツ/ブラウス(七分/長袖)"},{name: "ポロシャツ"},{name: "キャミソール"},{name: "タンクトップ"},{name: "ホルターネック"},{name: "ニット/セーター"},{name: "チュニック"},{name: "カーディガン/ボレロ"},{name: "アンサンブル"},{name: "ベスト/ジレ"},{name: "パーカー"},{name: "トレーナー/スウェット"},{name: "ベアトップ/チューブトップ"},{name: "ジャージ"},{name: "その他"}])

ancestryを入れると.childrenと記述することで直前の変数の子要素として扱うことができます。

以下の記述で親カテゴリーであるレディースが登録されます。

seeds.rb
lady = Category.create(name: "レディース")

次に、以下の記述で子カテゴリーであるトップスが登録されます。

seeds.rb
lady_1 = lady.children.create(name: "トップス")

そして、以下の記述で孫カテゴリー群が登録されます。

seeds.rb
lady_1.children.create([{name: "Tシャツ/カットソー(半袖/袖なし)"},{name: "Tシャツ/カットソー(七分/長袖)"},{name: "シャツ/ブラウス(半袖/袖なし)"},{name: "シャツ/ブラウス(七分/長袖)"},{name: "ポロシャツ"},{name: "キャミソール"},{name: "タンクトップ"},{name: "ホルターネック"},{name: "ニット/セーター"},{name: "チュニック"},{name: "カーディガン/ボレロ"},{name: "アンサンブル"},{name: "ベスト/ジレ"},{name: "パーカー"},{name: "トレーナー/スウェット"},{name: "ベアトップ/チューブトップ"},{name: "ジャージ"},{name: "その他"}])

seeds.rb内にレコードを記述したら、ターミナルで以下のコマンドを実行しDBに反映させます。

$ rails db:seed

DBではこのように登録されます。

Image from Gyazo

最後まで見て頂きありがとうございます!!

この記事が少しでも参考になれば嬉しいです:pray_tone2:

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

マイグレーションファイルのrollback方法

はじめに

某プログラミングスクールにてフリマアプリを作成する際にカラム名を変更したい時にマイグレーションファイルのrollbackが必要だった為その時に調べた事を書きます。

1.マイグレーションファイルを1つずつ差し戻す方法

まずターミナルで以下を実行

$ rails db:rollback

$ rails db:migrate:status

   ↓ターミナルにこの様に表示されると思います
 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200510074001  Create spending categories
   up     20200510074005  Create income categories
   up     20200510074056  Create incomes
  down    20200510074140  Create spendings

2.複数のファイルを差し戻す

下記のように末尾にSTEP=で数を指定してあげると複数のファイルを差し戻すことが可能です。
今回は4つまとめて差し戻します。

$ rails db:rollback STEP=4 
  
$ rails db:migrate:status

   ↓ターミナルにこの様に表示されると思います
 Status   Migration ID    Migration Name
--------------------------------------------------
  down    20200510074001  Create spending categories
  down    20200510074005  Create income categories
  down    20200510074056  Create incomes
  down    20200510074140  Create spendings

3.任意のマイグレーションファイルのみを差し戻す

下記のようにVERSIONで "Migration ID" を指定してあげると選択のマイグレーションファイルのみを差し戻すことが可能です。

$ rails db:migrate:down VERSION=20200510074001 

$ rails db:migrate:status

   ↓ターミナルにこの様に表示されると思います
 Status   Migration ID    Migration Name
--------------------------------------------------
  down    20200510074001  Create spending categories
   up     20200510074005  Create income categories
   up     20200510074056  Create incomes
   up     20200510074140  Create spendings

最後まで見て頂きありがとうございます!!

この記事が少しでも参考になれば嬉しいです:pray_tone2:

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

Railsで画像を使用する

Railsで画像を使用する。

利用したい画像ファイルをapp/assets/imagesに入れておく。
画像を使用したいhtml.hamlファイルにて、

html.haml
= image_tag src= "/assets/sample.png", size: "50x50"

Railsで背景画像をcssで指定する。

背景として利用した画像ファイルをapp/assets/imagesに入れておく。
そしてcssファイルにて

css
background: image-url("sample.png");
background-size: cover;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RSpecによるTDDでRailsAPIを実装してみた。part1

はじめに

この記事は自分のための記憶を整理したりまとめたいという理由で書く記事です。もちろん読まれても恥ずかしく無いように書くますが、あくまでも自分で整理できればそれでOKな記事でありますので、説明不足ということがあるかと思いますが、あしからず...。

記事の内容

  • この記事はTDDの形で開発をすめていきます。テストフレームワークはRSpecを使っています。
  • 簡単なArticleとUserのモデルを使った記事管理システムを作っていきます。
  • 認証はgithubアカウントを使ったOAuthを使います。
  • databaseはapiに集中するためにデフォルトのsqlite3を使います
  • part1はindexアクションの実装までを目標に進めていきます
# versions
ruby 2.5.1
Rails 6.0.3.1
RSpec 3.9
  - rspec-core 3.9.2
  - rspec-expectations 3.9.2
  - rspec-mocks 3.9.1
  - rspec-rails 4.0.1
  - rspec-support 3.9.3

開発の準備

terminal
$ rails new api_name -T --api

-T はminitestに関わるファイルを生成しない
--api はこのプロジェクトはapiのためだけに使いますという宣言を前もってすることで、apiのためのファイル構成で始めることができる(ビューファイルが生成されないなど)

gemfile
gem 'rspec-rails'              # rspecのgem
gem 'factry_bot_rails'         # テスト用のデータを入れるgem
gem 'active_model_serializers' # serializeするためのgem
gem 'kaminari'                 # pagenateするためのgem
gem 'octokit', "~> 4.0"        # githubユーザーを扱うためのgem

以上のgemをdevelopmentの中に挿入

bundle install

これでいったん開発の準備は終わる

では開発していく

まず全体的な流れをなぞっておくと、
indexアクションの実装

認証関係の実装(userモデルの実装)

createアクションの実装

updateアクションの実装

という流れで進めていく

articleモデルを作成

$ rails g model title:string content:text slug:string

こんな構成のモデルを作成する

そして先ほど作成されたmodels/article_spec.rbにテストコードを記述していく

models/article_spec.rb
require 'rails_helper'

RSpec.describe Article, type: :model do
  describe '#validations' do
    it 'should validate the presence of the title' do
      article = build :article, title: ''
      expect(article).not_to be_valid
      expect(article.errors.messages[:title]).to include("can't be blank")
    end
  end
end

このテストの内容はもしもタイトルが空だった時にエラーメッセージを出す、というものと、もう一つはvalidationに引っかかるというテスト

$ rspec spec/models/article_spec.rb

を実行し、テストを動かす

その後エラーが出ることを確認する
今回の場合は

undefined method build

というようなエラーが出る、これはfactory_botが動いていない証拠なので、以下を追加

rails_helper.rb
config.include FactoryBot::Syntax::Methods

その後再びrspecを実行
$ rspec spec/models/article_spec.rb

failure
expected #<Article id: nil, title: "", content: "MyText", slug: "MyString", created_at: nil, updated_at: nil> not to be valid

validationに引っかからずにtitleがnilで保存されてしまっていることがわかる
なので、実際にmodelにvalidationを記述していく

app/models/article.rb
class Article < ApplicationRecord
  validates :title, presence: true
end

再度テストを実行して通る

そしてこれをcontentとslugも同じように記述していく

models/article_spec.rb
    it 'should validate the presence of the content' do
      article = build :article, content: ''
      expect(article).not_to be_valid
      expect(article.errors.messages[:content]).to include("can't be blank")
    end

    it 'should validate the presence of the slug' do
      article = build :article, slug: ''
      expect(article).not_to be_valid
      expect(article.errors.messages[:slug]).to include("can't be blank")
    end
app/models/article.rb
  validates :content, presence: true
  validates :slug, presence: true

テストを実行し全て通る

3 examples, 0 failures

そしてもう一つvalidationをかけておきたいのが、slugが一意かどうか

なので、uniqueに関するテストも追加で記述

models/article_spec.rb
    it 'should validate uniqueness of slug' do
      article = create :article
      invalid_article = build :article, slug: article.slug
      expect(invalid_article).not_to be_valid
    end

一度articleをcreateで作り再びbuildでarticleを作る。そして二つ目のarticleには一つ目のarticleのslugを指定しているので、二つのarticleのslugは同じものになる。これでテストをする。
ちなみにcreateとbuildの違いはデータベースに保存するかどうかで、全てcreateで完結させることもできる。しかし、createを全てに使ってしまうと、重くなってしまうので、データベースに保存する必要のない(その後直接expectで判定される)ものなどはbuildを使いメモリの消費をできるだけ減らす。

テストを実行しエラーが出るのを確認

failure
expected #<Article id: nil, title: "MyString", content: "MyText", slug: "MyString", created_at: nil, updated_at: nil> not to be valid

そのままuniqueではないのに保存されてしまっているのがわかる
なので、modelを記述していく

app/models/article.rb
validates :slug, uniqueness: true

その後テストは全てクリアする
これでarticleモデルのテストは終わり

articles#index

次はコントローラーを実装していく、まずはルーティングからテストしていく

articles#index routing

spec/routing/を作成
その後spec/routing/articles_spec.rbを作成

中身を記述していく

spec/routing/articles_spec.rb
require 'rails_helper'

describe 'article routes' do
  it 'should route articles index' do
    expect(get '/articles').to route_to('articles#index')
  end
end

これはrouteが正しく動いているかを確認するテスト

実行するとエラーが出る

No route matches "/articles"

/articlesのルーティングは存在していないというエラーが出る
なのでルーティングを追加する

routes.rb
resources :articles, only: [:index]

テストを実行するがエラーが出る

failure
 A route matches "/articles", but references missing controller: ArticlesController

これは/articleというルートは存在しているが、articlescontrollerに当てはまるものが存在していないということなので、実際にコントローラーに記述していく

$ rails g controller articles

controllerを作成
中身を記述していく

app/controllers/articles_controller.rb
def index; end

テストがとおる

ついでにshowアクションも実装していく

spec/routing/articles_spec.rb
  it 'should route articles show' do
    expect(get '/articles/1').to route_to('articles#show', id: '1')
  end
routes.rb
resources :articles, only: [:index, :show]
articles_controller.rb
  def show
  end

そしてテストを実行し、通ることを確認する。

articles#index 実装

次に実際にcontrollerの中身を実装していく

まずはテストから書いていく

spec/controllersというファイルを作りその中にarticles_controller_spec.rbというファイルを作っていく

spec/controllers/articles_controller_spec.rb
require 'rails_helper'

describe ArticlesController do
  describe '#index' do
    it 'should return success response' do
      get :index
      expect(response).to have_http_status(:ok)
    end
  end
end

このテストは単純に、get :indexのリクエストを送り、200番が返って来ることを期待するというテスト。:okは200と同じ意味。

そのままテストを実行する。

expected the response to have status code :ok (200) but it was :no_content (204)

というメッセージが出て、テストが失敗する。
このメッセージは200を期待していたが204が返ってきたことを意味する。
204は何も返ってこなかった、ということなので、大体はdeleteやupdateなどのレスポンスで利用される。しかし今回は200が返ってきて欲しいので、controllerを編集していく。

app/controllers/articles_controller.rb
  def index
    articles = Article.all
    render json: articles
  end

内容はシンプルで、データベースから全てのarticleを取り出し、それをrenderで返すという処理をしている。
json形式で返すためにjson:という書き方をしている
ちなみに、render articlesとそのまま返しても200が返るのでこのテストは成功する。json形式ではないresponseは好ましいとは言えない。後にserializerを使ってresponseを解析していくが、その時にはやはりjsonに変換しておく必要があるので json:はつけ忘れないようにする。

それではjson形式を確認するtestを書いていく

spec/controllers/articles_controller_spec.rb
    it 'should return proper json' do
    create_list :article, 2
      get :index
      json = JSON.parse(response.body)
      json_data = json['data']
      expect(json_data.length).to eq(2)
      expect(json_data[0]['attributes']).to eq({
        "title" => "My article 1",
        "content" => "The content of article 1",
        "slug" => "article-1",
      })
    end

いったんこのテストを実行する。説明は必要な時に書いていく。

Validation failed: Slug has already been taken

まずこのエラーが出る。slugはunique出ないとvalidationにかかってしまう。なのでfactory botを編集して一つひとつのarticleをuniqueにする。

spec/factories/articles.rb
FactoryBot.define do
  factory :article do
    sequence(:title) { |n| "My article #{n}"}
    sequence(:content) { |n| "The content of article #{n}"}
    sequence(:slug) { |n| "article-#{n}"}
  end
end

このようにsequenceを使えば、一つ一つのarticleの名前をuniqueにすることができる。

これで実行してみる。

Failure/Error: json_data = json[:data]
no implicit conversion of Symbol into Integer

このようなメッセージが出て失敗する。これは[:data]が存在しないことによるエラーで、この[:data]はserializerを使って形式を変換したときのものなので、serializerを使って、jsonの形式を変換していく。

最初にあらかじめ
gem 'active_model_serializers'という記述をしてgemを入れているので、それを使って、記述していく。

まずはserializer用のファイルを作っていく。

$ rails g serializer article title content slug

これにより、app/serializers/article_serializer.rbが作成される。

そしてその新しく導入したactive_model_serializerを適応させるために新しく記述する。

config/initializers/active_model_serializers.rb
ActiveModelSerializers.config.adapter = :json_api

このファイルを作り、記述を追加することで新しく導入したserializerを適応させる。

これにより、defaultで使われていたものを変更することができ、[:data]などで取り出すことができる形式に変換することができる。

それぞれのレスポンスの違いを見ていく。

ActiveModel::Serializer導入前

JSON.parse(response.body)
=> [{"id"=>1,
  "title"=>"My article 1",
  "content"=>"The content of article 1",
  "slug"=>"article-1",
  "created_at"=>"2020-05-19T06:22:49.045Z",
  "updated_at"=>"2020-05-19T06:22:49.045Z"},
 {"id"=>2,
  "title"=>"My article 2",
  "content"=>"The content of article 2",
  "slug"=>"article-2",
  "created_at"=>"2020-05-19T06:22:49.049Z",
  "updated_at"=>"2020-05-19T06:22:49.049Z"}]

ActiveModel::Serializer導入後

JSON.parse(response.body)
=> {"data"=>
  [{"id"=>"1",
    "type"=>"articles",
    "attributes"=>
     {"title"=>"My article 1", "content"=>"The content of article 1", "slug"=>"article-1"}},
   {"id"=>"2",
    "type"=>"articles",
    "attributes"=>
     {"title"=>"My article 2", "content"=>"The content of article 2", "slug"=>"article-2"}}]}

中の構造が変わっていることがわかる。また、serializerで指定していない、created_atやupdated_atの属性は切り捨てられている。

これで再びテストを実行すると成功する。

成功したものの重複した表現が多いので、少しリファクタリングをする。

get :indexだが、これは複数回送ることが多いので、まとめて一回で定義する。

describe '#index' do
  subject { get :index }

このように記述することで、まとめて記述できる。
その定義を使うためには定義した場所から下でsubjectと打つだけで使うことができる。
仮に、その下の階層でもう一度定義された場合は、そのあとから定義した方が使われる。

それを使い二箇所にsubjectと置き換えることができる。

    it 'should return proper json' do
      articles = create_list :article, 2
      subject
      json = JSON.parse(response.body)
      json_data = json['data']
      articles.each_with_index do |article, index|
        expect(json_data[index]['attributes']).to eq({
          "title" => article.title,
          "content" => article.content,
          "slug" => article.slug,
        })
      end

そして、この部分はeach_with_indexを使うことで、重複した表現をまとめることができる。

そしてさらに

json = JSON.parse(response.body)
json_data = json['data']

この二つの記述は繰り返し使うことが多いので、helperメソッドに定義しておく。

spec/support/を作成し、その下にjson_api_helpers.rbを作成する。

spec/support/json_api_helpers.rb
module JsonApiHelper
  def json
    JSON.parse(response.body)
  end

  def json_data
    json["data"]
  end
end

これを全てのファイルで扱えるようにするために、spec_helper.rbでincludeさせておく。

spec/rails_helper.rb
config.include JsonApiHelpers

そしてsupportを読み込ませるようにしたの記述のコメントアウトを外しておく。

spec/rails_helper.rb
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

これにより、以前の記述を省略して書ける。

spec/controllers/articles_controller_spec.rb
    it 'should return proper json' do
      articles = create_list :article, 2
      subject
      articles.each_with_index do |article, index|
        expect(json_data[index]['attributes']).to eq({
          "title" => article.title,
          "content" => article.content,
          "slug" => article.slug,
        })
      end

これで、indexはほぼ完成した。しかし、記事の順番が最新が最後になってしまう。最新は最初に来るように、sortを設定する。

まずは期待するテストから書く。

spec/controllers/articles_controller_spec.rb
    it 'should return articles in the proper order' do
      old_article = create :article
      newer_article = create :article
      subject
      expect(json_data.first['id']).to eq(newer_article.id.to_s)
      expect(json_data.last['id']).to eq(old_article.id.to_s)
    end

以上の記述を追記する。
2度articleをcreateし、古い方と新しい方を作る。
そして、それらがjson_dataの何番目かで、最新が、先に来ているかどうかを判定する。
to_sメソッドを使っているのはserializerを使うとvalueの全てが文字列に変換されるので、to_sメソッドを使って、factorybotで生成したデータは文字列にしなければいけない。idも文字列に変換されているので注意が必要。

これでテストを実行する。
rspec spec/controllers/articles_controller_spec.rb

failure
expected: "2"
got: "1"

このようなメッセージが出力される。
まだ、sortを何も触っていないので、当たり前だが、最後のarticleは最初に来ている。

sortを実装したいのだが、メソッドとしてmodelに記述したいのでmodelのテストをまず先に書く。
メソッド名は.recentとする。

spec/models/article_spec.rb
  describe '.recent' do
    it 'should list recent article first' do
      old_article = create :article
      newer_article = create :article
      expect(described_class.recent).to eq(
        [ newer_article, old_article ]
      )
    end
  end

これはcontrollerのテストと似たようなことをしているが、違うのはcontrollerとは分離していて、メソッドを呼び出す処理のみにフォーカスしていること。要するにこのテストのみで完結する。

described_class.method_name
これにより、クラスメソッドを呼び出すことができる。
この場合のdescribed_classはArticleとなるが記述するファイルにより、そこに何が入るかは変わって来る。

あと、updateした時に古い記事が最近に来るかどうかもテストしたい。
なので以下を追記する。

spec/models/article_spec.rb
    it 'should list recent article first' do
      old_article = create :article
      newer_article = create :article
      expect(described_class.recent).to eq(
        [ newer_article, old_article ]
      )

      old_article.update_column :created_at, Time.now
      expect(described_class.recent).to eq(
        [ old_article, newer_article ]
      )
    end

テストを実行すると

undefined method recentと出るので、recentを定義していく。

これまでrecentをメソッドとして定義するというふうに書いていたが、scopeの方が用途に合っているのでscopeで実装する。
scopeはほとんどの場合、classメソッドと同じように扱われる。

長々とテストを書いたが、実装はすぐ終わる。

app/models/article.rb
scope :recent, -> { order(created_at: :desc) }

一行を追加してscopeを定義する。

そして、テストを実行するmodelのテストは全てがグリーンになる。
rspec spec/models/article_spec.rb

しかし、controllerの方はいくつか失敗する。
rspec spec/controllers/articles_controller_spec.rb
それはrecentを定義しただけで、controllerdで使ってないからなので、実際にcontrollerに記述していく。

app/controllers/articles_controller.rb
articles = Article.recent

Article.allをArticle.recentに変更する。

これでもう一度テストを実行する。
rspec spec/controllers/articles_controller_spec.rb

すると、test側でsortできていなくて失敗するので、test側もsortする。

spec/controllers/articles_controller_spec.rb
 Article.recent.each_with_index do |article, index| # 14行目

このようにarticles.recentではなく、Article.recentと記述したのは、articlesはfactory_botの生成したものなので、実際のArticleのインスタンスではないので、.recentが使えないため。

直接factorybotで作成したものを利用する必要がなくなったので、
articles = create_list :article, 2
この記述は
create_list :article, 2
このようにcreateするだけにしておく。

これでテストを実行すれば成功する。

paginationを次は実装する。
paginationとは一ページあたり幾つの記事というふうに指定できるようにするもの。

まずはいつものようにテストから書いていく。

spec/controllers/articles_controller_spec.rb
    it 'should paginate results' do
      create_list :article, 3
      get :index, params: { page: 2, per_page: 1 }
      expect(json_data.length).to eq 1
      expected_article = Article.recent.second.id.to_s
      expect(json_data.first['id']).to eq(expected_article)
    end

この行を追加。paramsに新しいparameterを指定している。
pageは何ページ目かを指定していて、per_pageは一ページあたり、幾つのarticleかを指定している。今回の場合は、2ページ目が欲しくて、そのページに記載されているのは1つのarticleということになる。

テストを実行する。

failure
expected: 1
got: 3

一つしか返ってこないことを期待したが、全てのarticleが返ってきてしまった。なので、ここからpaginationを実装していく。

この記事の一番初めにkaminariというgemは既に追加している。kaminariはpaginationを簡単に実現することができるgem。

なのでそのgemを使って実装していく。

app/controllers/articles_controller.rb
    articles = Article.recent.
      page(params[:page]).
      per(params[:per_page])

recentに続けて、追記する。このように、orマッパーのように、.pageや.perを使うことができる。そしてその引数にparamsで送られてきた値を挿入すれば良い。

これで、responseを絞り込むことができ、一度のresponseによって返すデータ量を指定することができる。

これでもう一度テストを実行すれば成功する。

いったんここで予定していたpart1は終わり。indexの実装は完了。

pert2は随時投稿予定。お疲れ様でした。

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

ターミナルとは?-絶対パス&相対パス-

ターミナルとは

ターミナルとは、コマンドラインの一つで、PCに命令を与えるためのツールです。記述したコードのファイルを実行するのに必要となります。
コマンドラインとは、コマンドラインインターフェース(CLI)の略で、PC対してキーボードからコマンドという文字を打ち込んで操作を行う方法です。
これとは対照に、グラフィックを用いてPCの操作を行う方法をグラフィカルユーザーインターフェース(GUI)と言います。


コマンド

ターミナルでよく使うコマンドを紹介します。


cdコマンド
デイレクトりを移動するコマンドです。

lsコマンド
現在いるディレクトリの直下にあるファイルやフォルダを表示します。


ディレクトリとは
  • ディレクトリとはファイルの入れ物のようなものでフォルダを指します。 ディレクトリのは階層構造になっており、 その一番上の階層のディレクトリのことをルートディレクトリと言います。 自分が現在いるディレクトリのことをカレントディレクトリと言います。 また、ターミナルを開いた時に表示されるディレクトリのことをホームディレクトリと言います。Macでのデフォルトのホームディレクトリは/Users/ユーザー名です。



話戻ります。
cdコマンドを使ってディレクトリを行き来できるわけですが、cdコマンドに続けてパスを入力することによって目的のディレクトリに辿り着くことができます。

そのパスに記述の仕方には2通りあります。
・絶対パス
・相対パスです

絶対パス

  • 絶対パスとはルートディレクトリから記述するパスのことです。 /から記述するのが決まりです。
ターミナル.
$ cd /

###これでルートディレクトリに移動できます

MacのデフォルトのルートディレクトリはMacintosh HDだと思いますのでcd /でMacintosh HDに移動します。
lsコマンドで直下のディレクトリを確認すればパスがわからなくても確認して移動できます。

相対パス

相対パスは先頭に/をつけません。
現在いるディレクトリの直下にあるディレクトリから記述することによって移動することができます。

この記事を読んでいただきありがとうございました。

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