20200530のRubyに関する記事は30件です。

予測変換を非表示にする

フォームの予測変換機能をオフにする

問題を解くアプリを作成しているときに、フォームに文字を打ち込むときに予測変換が出てしまい、答えが事前にわかってしまうケースがあったため調べた。

結果

<input>のautocomplete属性をoffにすると予測変換が消えるみたいです。
autocomplete: 'off'
予測変換のことをオートコンプリートと言うと。

form.rb
<%= text_field_tag "word#{word.id}","", {class: 'form-control',autocomplete: 'off'} %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kinx ライブラリ - Process

Kinx ライブラリ - Process

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。言語はライブラリが命。ということでライブラリの使い方編。

今回は Process です。子プロセス起動とかやっぱり必要ですよね、ということで急遽こしらえました。

System.exec()

昔から標準で用意していたコマンド実行インタフェース。単純に C レベルでの system() を呼ぶだけなので手軽だが、終了するまで帰ってこないとか、標準出力を取得できないとか、色々不便ではある。ただし、シェル経由でコマンド実行するので、リダイレクトとかは使える。

で、今回はもっと色々とできる Process クラスを作ったので、そちらが今回の説明での本命。

Process

using Process

Process ライブラリは標準組み込みではないため、using ディレクティブを使用して明示的に読み込む。

using Process;

Exec

Process オブジェクトは new Process(command, opts) で作成する。引数はコマンド名と引数の配列、またはコマンド文字列。配列の場合は引数を別々に渡す感じで、コマンドライン文字列の場合は内部で解析して配列形式に自動で分解。

  • 配列: ["ls", "-1"] のような感じ。
  • 文字列: "ls -1" のような感じ。

作成されたプロセス・オブジェクトは以下のメソッドを持つ。

メソッド 概要
run() プロセスを開始させる。
launch() プロセスを開始させて切り離す。
std() 引数で渡されたオプションが返る。
{ in: opts.in, out: opts.out, err: opts.err }

この時点ではまだ実行していない。run() または launch() を行った時点で起動する。run() すると ProcessController クラスのオブジェクトが返る。

Run

run() することで ProcessController クラスのオブジェクトが返る。

var p = new Process(["cmd","arg1"]).run();

Launch

launch() は何も返さない(というか null が返る)。突き放して以後、子供の面倒は見ない、という方法。

new Process(["cmd","arg1"]).launch();

ProcessController

run() で返された ProcessController クラスは以下のメソッドを持つ。

メソッド 概要
isAlive() プロセスが生きている場合は true、既に終了している場合、または detach された後は false
wait() プロセスの終了を待ち、終了後にプロセスの終了コードを返す。detach 後は 0 を返す。
detach() プロセスを detach する

detach() はプロセス起動後に切り離す。Linux では launch() で切り離した場合と微妙に動作が違うがやりたいことは同じ。Windows で内部動作も同じ。

Linux ではプロセス起動時に切り離すためにいわゆる double-fork というやり方で切り離すが、これはプロセス起動時にしか使えない。プロセス起動後に切り離すのは実質的に不可能で、親プロセスではきちんと wait なり waitpid なりしてやらないと子はゾンビとなって生き残ってしまう。

そこで、detach() した瞬間に waitpid するためだけのスレッドを起動して、子が死ぬまで面倒見るようにしてある。

ちなみに double-fork とは Linux の、

  • 親プロセスが死ぬと子プロセスは init 配下になった上に init はひたすら wait しまくって面倒見てくれる

...という機能を利用して、一度 fork したプロセスからさらに fork した上で、最初に fork したプロセスを速攻終了させて孫プロセスの管理を init に任せる、というやり方です。

一番上の親プロセスは、最初に fork した子供の waitpid を忘れずに。勝手に面倒見てもらうのは孫のほうなので。

Wait

終了を待って終了コードを取得する例は以下の通り。

var p = new Process(["cmd", "arg1"]).run();
var status = p.wait();

detach していた場合は当然取得できない(0 が返る)。

Detach

先ほどから出てきている detach。プロセスは detach(切り離し)することもできる。切り離してしまえば子との縁は切れる。wait する必要もないし、終了を気にする必要もない。というか、気にしたくてもできなくなる。

var p = new Process(["cmd", "arg1"]).run();
p.detach();

Pipe

お待ちかねパイプ。Process を作った一番の目的はパイプ。子プロセスとの標準入出力をパイプに自由自在につないで情報のやり取りをしたい、というのが一番欲しい機能ですよね。

パイプの指定は、new Process(cmd, opts)opts で指定。パラメータは以下の 3 種類。

パラメータ 内容
in 標準入力を指定。
指定可能なものは、パイプ・オブジェクト、文字列、$stdin
out 標準出力を指定。
指定可能なものは、パイプ・オブジェクト、文字列、$stdout または $stderr
err 標準エラー出力を指定。
指定可能なものは、パイプ・オブジェクト、文字列、$stdout または $stderr
  • パイプ・オブジェクト ... パイプを使うためのオブジェクト。詳細は後述。
  • 文字列 ... ファイル名として、入力元、出力先ファイル。
  • $stdin$stdout$stderr ... 入力元、出力先を本プロセスの標準入出力にバインドする。

パイプ・オブジェクト

パイプ・オブジェクトは new Pipe() で作成する。[Read, Write] の 2 つのオブジェクトをペアで配列で返す。パイプオブジェクトには、以下のメソッドがある。

通常は Write パイプを子プロセスの out または err に指定して、Read パイプから読み込む。

Read Pipe

パイプのクローズは run() してからすること。run() するときに設定されるため。

メソッド
peek() パイプにデータがなければ 0、あれば 0 より大きい数値を返す。-1 はエラー。
read() パイプのデータを全て文字列として取得する。データがない場合は空文字列を返す。
close() パイプを閉じる。
Write Pipe

パイプのクローズは run() してからすること。run() するときに設定されるため。

メソッド
write(data) パイプにデータを書き込む。全て書き込めるとは限らず、書き込んだバイト数を返す。
close() パイプを閉じる。
サンプル

一般的な形として以下のように使う。

using Process;

var [r1, w1] = new Pipe();
var p1 = new Process([ "ls", "-1" ], { out: w1 }).run();
w1.close(); // もう使わないのでクローズしてよい
while (p1.isAlive() || r1.peek() > 0) {
    var buf = r1.read();
    if (buf.length() < 0) {
        System.println("Error...");
        return -1;
    } else if (buf.length() > 0) {
        System.print(buf);
    } else {
        // System.println("no input...");
    }
}
System.println("");

Write Pipe を親プロセス側で使う場合は、こんな感じ。

using Process;

// stdin はパイプから読み込み、標準出力に出力
[r1, w1] = new Pipe();
var p1 = new Process("cat", { in: r1, out: $stdout }).run();
r1.close(); // もう使わないのでクローズしてよい

// p1 の stdin に送り込む
var nwrite = w1.write("Message\n");
w1.close(); // パイプクロ―ズ、送信終了

p1.wait();

ちなみに、こうすると標準出力と標準エラー出力を制御できる。

new Process("cmd", { out: $stdout, err: $stdout }); // 標準エラー出力を標準出力に合流
new Process("cmd", { out: $stderr, err: $stderr }); // 標準出力を標準エラー出力に合流
new Process("cmd", { out: $stderr, err: $stdout }); // 入れ替え

Pipeline

パイプをつないでいくのは結構面倒(というか、どっちがどっちだっけ...? みたいな)な作業なので、一括して行ってくれる Process.pipeline というのも定義してみた。最後にコールバック関数を置いて、以下のように使う。

var r = Process.pipeline(cmd1, cmd2, cmd3, ..., &(i, o, pipeline) => {
    // i ... 最初のコマンドの stdin への書き込みパイプ
    // o ... 最後のコマンドの stdout からの読み込みパイプ
    // pipeline ... パイプライン・オブジェクト
    //    pipeline.input ....... 上記 i と同じ
    //    pipeline.output ...... 上記 o と同じ
    //    pipeline.peek() ...... pipeline.output.peek() と同じ
    //    pipeline.read() ...... pipeline.output.read() と同じ
    //    pipeline.write() ..... pipeline.input.write() と同じ
    //    pipeline.isAlive() ... パイプラインのいずれかのプロセスが生きていたら true
    //    pipeline.wait() ...... パイプラインの全てのプロセスが完了するのを待ち、
    //                           終了コードを配列で返す

    // コールバックの復帰値がそのまま Process.pipeline() の復帰値になる。
    return pipeline.wait();
});

コールバックしなくても使える。

var pipeline = Process.pipeline(cmd1, cmd2, cmd3, ...);
// pipeline ... パイプライン・オブジェクト
//  以下省略。

おわりに

子プロセス関係は Windows と Linux で違うので、そういうのを統一的に扱えるのはスクリプトの良いところ。ただし、コマンド自体は違ったりするので、そこはなかなか吸収できませんね。私は Windows ユーザーですが、UnxUtils を使って Unix コマンドをある程度コマンドプロンプトでも使えるようにしています。(Cygwin は環境を変えてしまうのであまり好きではない...)

ということで、ではまた次回。

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

rails newしたときにインストールしている最新のRailsバージョンでアプリが作成されない場合の対処法

rails newを実行したときにインストールしているRailsの最新バージョンでアプリが作成されず、どハマりしたので投稿します。

はじめに

バージョンを指定せずにrails newすると、インストールしているRailsの最新バージョンでアプリが作成されます。
が、私が遭遇したのはrails newすると最新版ではなく、古いバージョンでアプリが作成されるという現象でした。しかもそのバージョンはgem listコマンドでも表示されないバージョンでした。。

問題の原因

先に原因を書くと、環境変数の設定に問題がありました。

/usr/local/bin/Users/user_name/.rbenv/shimsより先に定義されており、$PATHは先に書いたほうが優先されるため、/usr/local/bin/railsが使われたことが原因でした。
つまり、rbenvで管理されているRailsのバージョンではなく、macにインストールされているRailsが参照されていました。

$ echo $PATH
/usr/local/bin:(中略)/Users/user_name/.rbenv/shims:/Users/user_name/.rbenv/bin:
(以下略)

問題発生から解決までの流れ

起きたこと

新規のアプリを作成しようとしてrails newコマンドをバージョン指定無しで実行したところ、インストールしている最新バージョン(6.0.3)ではなく、古いバージョン(5.2.3)で作成されました。

Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.3.7'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.2.3'
(以下略)

Railsのバージョンを確認したところ5.2.3になっていたのですが、gem listコマンドで確認したバージョンには5.2.3は含まれていませんでした。

$ rails -v
Rails 5.2.3

which railsでRailsの実行場所を確認すると、/usr/local/bin/railsになっていました。こうなっていた原因は上に書いたとおり、環境変数でrbenvのパスより先に/usr/local/bin/railsが定義されていたためです。

$ which rails
/usr/local/bin/rails

蓋を開けてみると単純な問題だったのですが、一度gemをすべてアンインストールしたりと無駄なこともしてしまったので、同じ問題で困った方の助けになれば幸いです。

他に原因として考えられること

rails -vで存在しないはずのRailsのバージョンが表示されるときは、railtiesも疑わしい場合があるようです。
Railsのバージョンがなんかおかしい時はrailtiesをチェック - かなりすごいブログ

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

Rubyを使用してのカレンダー作成

Ruby学習のアウトプットとして行おうとしましたが、
正直手も足も出なかったので回答例を見て、
読み解く作業をおこなうこととしました。

その記録です。
「プロを目指す人のためのRuby入門」で学習を進めていますが、
まだまだ落とし込みが足りないようです。。。

・回答例
https://qiita.com/sackey_24/items/8fc236bb054aff6b74c8

・参考サイト
https://www.javadrive.jp/ruby/date_class/index5.html

コードごとで読み解き

回答見てもわかるところもあれば初見のところもあったので、
そちらを中心にまず調べて見ました。

どちらにしてもDateクラスを理解しないことには始まらなそうです。。。

Dateクラス

・Timeクラスは組み込みライブラリーだが、Dateクラスは違うので、”require”で呼び込む。
・主要メソッドは、day・mouth・year・wday(それぞれ整数で取得。wdayも日曜日を0として整数で取得)
・オブジェクト作成後、フォーマットを使用し文字列に変換可能(strftimeメソッド)
→%a 曜日の省略形(sun,mon,thu等々) %w 曜日を表す数字

Dateクラスがある程度理解できたので、回答コードの意味もわかってきました。

まずは変数を作り、

    head = Date.today.strftime("%B, %Y") #今日の月と西暦を取得→カレンダー上部に表示
    year = Date.today.year #今年の西暦
    mon = Date.today.mon   #今日の月

Dateクラスから今日の西暦と月を作成できたところで、
要件に書いてあるように今月1日と末日もDateクラスから作成。

    firstday_wday = Date.new(year,mon,1).wday
    lastday_date = Date.new(year,mon,-1).day
    week = %w(Su Mo Tu We Th Fr Sa)

ここの重要点としては、

・インスタンスの引数として先ほどの変数を持ってくる。
・末日は”−1”と表記することで取得可能。
・%wは配列をつくる表記(*フォーマット文字列とは別)

次にここまでを実際に出力。

   puts head.center(20)
   puts week.join(" ")
   print "  " * firstday_wday

ここも同じく?になったところをまとめます。

・centerメソッドは引数の数が文字数となりそれに沿って中央揃い
・join(" ")→引数のスペースはweek配列の曜日を感覚を開けて表示させるため
・printは改行せず表示するという特性を使いカレンダー1日までの空白を表示。
→スペースを曜日の数字分作る

最後に日付を表記。
数字はくり返し処理をし、改行に関しては条件分岐で設定

   wday = firstday_wday
   (1..lastday_date).each do |date|    
    print date.to_s.rjust(2) + " " 

右詰で数字を並べる。(1..lastday_wdayで指定した範囲)

そして最後にスタートした曜日(月曜日だったら1)から繰り返し処理ごとに1を足して行き7の倍数に差し掛かったら改行する。

    wday = wday+1
    if wday%7==0                     
      print "\n"
    end
   end


 if wday%7!=0
   print "\n"
 end

これで出来上がり。

      May 2020      
Su Mo Tu We Th Fr Sa
            1  2 
 3  4  5  6  7  8  9 
10 11 12 13 14 15 16 
17 18 19 20 21 22 23 
24 25 26 27 28 29 30 
31     

振り返り

参考書に書いてある部分が大半だったがそれをいざ0から実装していくとなるとちんぷんかんぷになってしまった。。。

アウトプットすることでその部分や参考書では深く語られてない箇所(今回でいうとDateクラスの詳細)に触れることができたりを確認することができるのでどんどんつづけていければと思います。

また、本記事は自分なりに解釈した内容となるので、もし間違い等があれば、ご指摘頂ければ幸いです。。。

とりあえず配列の章読み返そう。。。

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

Ruby クラスとインスタンスの作成

クラスの作成

オブジェクトはクラスから作成されます。
クラスからオブジェクトを作成することをインスタンス化といいます。

実際にクラスを作成していきます。
以下のように、class構文を使用します。

sample.rb
class House
end

クラス名の先頭は大文字にする規則があります。
この記述で、最小限のHouseクラスを定義できました。

インスタンス化

次は、作成したHouseクラスからインスタンス(オブジェクト)を作成していきます。
newメソッドを使用することで、インスタンス(オブジェクト)を作成できます。

sample.rb
class House
end
House.new

クラスを呼び出すためには、クラス名を記述します。

sample.rb
class House
end
House

インスタンス(オブジェクト)のクラス

インスタンスのクラスがHouseクラスであることを確認してみます。

sample.rb
class House
end

puts House.new.class

コンソール上で、sample.rbを実行し、以下のように出力されていれば正しくインスタンスが、Houseクラスから作成できています。

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

Ruby環境構築まとめ~mac版~

はじめに

あたらしくmacを購入して、Rubyの環境を作り直す機会があったので備忘録としてまとめます。

Homebrewをインストール

まずhomebrewをインストールします。(もうすでにインストール済みの人は大丈夫です)

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

このコマンドをターミナルで実行するとインストールが完了します。

wgetのインストール

wgetをインストールします。

brew install wget

Rubyのインストール

wgetのインストールが完了したら、Rubyのインストールに入っていきます。
手順としては
- homebrewでrbenvをインストール
- パスを通す
- rubyのインストール
と言った感じです。
では、rbenvをインストールしていきます。

brew install rbenv
brew install ruby-build

この二つのコマンドを実行してインストールします。
インストールが終わったら、パスを通していきます。

echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.zshrc
echo 'if which rbenv > /dev/null; then eval "$(rbenv init -)"; fi' >> ~/.zshrc
source ~/.zshrc

これでrbenvのインストールとパスを通すことができました。
いよいよRubyをインストールしていきます。

rbenv install --list
rbenv install 2.6.6
rbenv rehash
rbenv global 2.6.6

1つ目のコマンドでダウンロードできるRubyのバージョン一覧を確認できます。
2つ目のコマンドでダウンロードするRubyのバージョンを指定します。(今回は2.6.6を選択しました)
3つ目のコマンドでrbenvの再読み込み、4つ目でdefaultで使うバージョンの指定をしています。

一通りの作業が終わったら、ターミナルで

ruby -v

をやって自分が先ほど指定したバージョンになっているか確認してみてください。
なっていなかったら、pathがうまく通ってない可能性があるので、zshrcを確認してみてください。

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

【Ruby】親クラスと子クラスの関係。クラスとインスタンスの関係。

 初学者の備忘録。

 親クラスと子クラスの関係、クラスとインスタンスの関係について、少し混乱したので。

 4つは全てオブジェクトであり、オブジェクトはクラスを包含する概念。

○クラスとインスタンスの関係
 ・クラスを元にして、インスタンスという「オブジェクト」が作られる。
 ・クラス内で定義されたメソッドは、「インスタンス.メソッド」のようにして呼び出すことができる。
 ・@〇〇〇のような、クラス内で定義されたインスタンス変数は、インスタンス内でも呼び出すことができる。

○親クラスと子クラスの関係
・親クラスを元に、子クラスという新しい「クラス」が作られる。
・子クラスでは、親クラスで定義されたメソッドを、同名のメソッドを定義することで変更できる(オーバーライド )。

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

Ruby モジュール入門

Ruby モジュール入門

Rubyの機能であるモジュールですが、Rubyを始めた方にとって理解が難しい部分の一つではないかと感じています。

そこで今回はモジュールについてまとめていこうと思います。

モジュールの機能

● ミックスイン( includeとextend )
● 名前空間の作成
● 関数やメソッドを提供する

他にも機能はありますが、3つ主要な機能を挙げてみました。
今日はこれらについてみていきたいと思います。

ミックスイン

ミックスインと言われてもなんのことだかわからないと思いますが、
モジュールをクラスに組み込むことで多重継承を行えるようになります。
Rubyではclassの単一継承しかできませんが、moduleは多重継承が可能です。
またclassと違いis-aの関係(あるオブジェクトが「あるクラスもしくはその子孫クラスのインスタンスである」という関係)でなくても同じ機能を共有できます。

追記:is-aの関係の説明について修正いたしました。コメントをいただきありがとうございます。

include

ruby.rb
module Hoge
  def hello
    puts 'Hello'
  end
end

module Bar
  def bye
    puts 'Bye'
  end
end

class Greet
  #上で作ったモジュールをinclude
  include Hoge
  include Bar
end

greet = Greet.new
greet.hello #=> "Hello"
greet.bye   #=> "Bye"

以上のようにincludeすることでクラスはモジュールで定義されたメソッドを使えるようになります。
このようにモジュールをクラスにincludeして機能を追加することをミックスインといいます。

extend

extendを使うとモジュール内のメソッドをクラスメソッドにすることができます。

ruby.rb
module Hoge
  def hello
    puts 'Hello'
  end
end

class Greet
  #上で作ったモジュールをextend
  extend Hoge
end

#クラスメソッドとしてhelloを呼び出せる
Greet.hello #=> "Hello"

こんな感じでincludeやextendでモジュールで定義したメソッドをクラス内で使えるのが、
モジュールの使い方の一つであるミックスインです。

名前空間を提供する

モジュール名の中にクラスを書くとモジュールに属するクラスという意味になり
名前の衝突を防ぐことができます。

module Bar
  class Baz
    def self.foo
      puts 'foo'
    end
  end
end

#Barというモジュールに属するBazクラスよりfooメソッドを呼び出した。
Bar::Baz.foo #=> "foo"

エンジニアとして働いてから感じたことですが、
名前空間ってかなり使う機会が多いんですよね。
というのもプロジェクトが大きくなればなるほど名前の衝突が起こる危険性があるので
こうして名前空間を設定して衝突を防ぐということを頻繁に行っています。

関数やメソッドを定義する

定数

モジュール内で定義した定数は、モジュール名を経由して呼び出すことが可能。

ruby.rb
module Hoge
  Year = "2020"
end

Hoge::Year #=> "2020"

メソッド

インスタンスメソッドはmodule_functionメソッドを使って、メソッドをモジュール関数にすることで呼び出すことができるようになります。

ruby.rb
module Hoge
  def hello
    puts 'Hello'
  end

  module_function :hello
end

Hoge.hello #=> "Hello"

モジュールのまとめは以上になります。
本日で100日後に一人前になるエンジニアの連載10日目でした。

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

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

Rails Deviseでユーザー編集をパスワードを入力しないで更新する方法

Rails Deviseでユーザー編集をパスワードを入力しないで更新する方法

Deviseで現在のパスワードを入力せずにユーザ情報を更新する方法をまとめます。

目次

動作環境

OS : macOS Mojave 10.14.6
ruby : 2.6.5p114
rails : 5.2.4
devise : 4.7.1

前提条件

すでにgemのインストールからviewの作成までの手順が終わっていると仮定します。

  1. devise gemのインストール済
  2. rails generate devise install済
  3. rails generate devise:views済
  4. usersテーブルにnameなどのデフォルト以外のカラムが追加済

手順概略

STEP1. 新規登録のためのストロングパラメータをapplication_controllerに追加

STEP2. registrations_controller.rbcontrollers/users/に作成し,更新するためのストロングパラメータを追加, ルーティングを修正

STEP3. パスワード無しでアップデートするためのメソッドをregistrations_controller.rbuser.rbに記載

STEP4. Viewからcurrent_passwordフィールドを削除する

詳細手順

新規登録のためのストロングパラメータの設定

現状では後から追加したnameのパラメータはstrongパラメータではじかれてしまうため
application_controllerに以下のコードを記載します.

application_controller.rb
class ApplicationController < ActionController::Base
 before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  end
end

コンソールで確認するとnameパラメータを受け取りユーザを作成できています.

irb(main):001:0> User.create(name: 'abc' , email:'abc@example.com',password:'123456')
   (1.3ms)  COMMIT
=> #<User id: 2, email: "abc@example.com", created_at: "2020-05-30 10:41:46", updated_at: "2020-05-30 10:41:46", name: "abc">

次はviewにnameの入力フィールドを記載します.

new.html.erb
<h2>Sign up</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>

<div class="field">
  <%= f.label :email %><br />
  <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
</div>

//追加
<div class="field">
  <%= f.label :name %><br />
  <%= f.text_field :name, autofocus: true, autocomplete: "name" %>
</div>

<div class="field">
  <%= f.label :password %>
  <% if @minimum_password_length %>
  <em>(<%= @minimum_password_length %> characters minimum)</em>
  <% end %><br />
  <%= f.password_field :password, autocomplete: "new-password" %>
</div>

<div class="field">
  <%= f.label :password_confirmation %><br />
  <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
</div>

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

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

これでuserの新規登録は完了です。

アップデートのストロングパラメータの設定

次にユーザ編集用のviewにもnameフィールドを追加します.

edit.html.erb
//追加
<div class="field">
  <%= f.label :name %><br />
  <%= f.text_field :name, autofocus: true, autocomplete: "name" %>
</div>

ここでアップデートボタンを押してもnameはupdateされないことがわかります.

そこでnameカラムをアップデートするためにusers/registrations_controller.rbを作成し,以下のように記載します.

registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_account_update_params, only: [:update]

  protected

  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [:name])
  end
end

そして、このregistartions_controllerを参照するためにルーティングを修正します.

routes.rb
Rails.application.routes.draw do
  root 'blogs#index'
  #変更箇所
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }
  resources :blogs
  end

するとUsersのnameカラムがアップデートできるようになります.

パスワード無しで更新するためのメソッドを定義

ただ,現時点ではcurrent_passwordを入力しないとupdateの際にエラーになります.
image.png

そこで, まずはuserモデルにパスワード無しでアップデートするメソッドを定義します.

user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  //追加するメソッド
  def update_without_current_password(params, *options)
    params.delete(:current_password)

    if params[:password].blank? && params[:password_confirmation].blank?
      params.delete(:password)
      params.delete(:password_confirmation)
    end

    result = update_attributes(params, *options)
    clean_up_passwords
    result
  end
end

その後,registrations_controllerからupdate_without_passwordを呼び出します.

registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_account_update_params, only: [:update]

  protected
  //追加(必須)
  def update_resource(resource, params)
    resource.update_without_password(params)
  end

  //必須ではないがupdate後にtop画面にリダイレクトするメソッド
  def after_update_path_for(_resource)
    blogs_path
  end

  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [:name])
  end
end

viewファイルからcurrent_passwordフィールドの削除

viewファイルからcurrent_passwordを削除します.

edit.html.erb
//削除
<div class="field">
  <%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
  <%= f.password_field :current_password, autocomplete: "current-password" %>
</div>

結果

image.png

エラーが出ずにuser nameがアップデートされていることがわかります.

image.png

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

Rails consoleのIncorrect string valueエラー対応

概要

Rails Consoleでcsvインポート中に以下のエラーが出たのでその対応記録

ActiveRecord::StatementInvalid: 
Mysql2::Error: Incorrect string value: '\xE3\x82\xA8\xE3\x82\xB3...' 
for column 'name' at row 1: 
INSERT INTO `contracts` (`account_id`, `name`, `created_at`, `updated_at`) 
VALUES (101, 'エコノミー', '2020-05-30 01:50:58', '2020-05-30 01:50:58')
from /usr/local/bundle/gems/mysql2-0.4.10/lib/mysql2/client.rb:120:in `_query'

character_set_databaseの対応

character_set_database、character_set_serverがlatin1になっていた。

MySQL [example]> show variables like "chara%";
+--------------------------+-------------------------------------------------+
| Variable_name            | Value                                           |
+--------------------------+-------------------------------------------------+
| character_set_client     | utf8mb4                                         |
| character_set_connection | utf8mb4                                         |
| character_set_database   | latin1                                          |
| character_set_filesystem | binary                                          |
| character_set_results    | utf8mb4                                         |
| character_set_server     | latin1                                          |
| character_set_system     | utf8                                            |
| character_sets_dir       | /rdsdbbin/oscar-5.7.12.200076.0/share/charsets/ |
+--------------------------+-------------------------------------------------+
8 rows in set (0.001 sec)

AWS RDS Auroraのパラメータグループでutf8mb4に設定

image.png

MySQL [example]> show variables like "chara%";
+--------------------------+-------------------------------------------------+
| Variable_name            | Value                                           |
+--------------------------+-------------------------------------------------+
| character_set_client     | utf8mb4                                         |
| character_set_connection | utf8mb4                                         |
| character_set_database   | utf8mb4                                         |
| character_set_filesystem | binary                                          |
| character_set_results    | utf8mb4                                         |
| character_set_server     | latin1                                          |
| character_set_system     | utf8                                            |
| character_sets_dir       | /rdsdbbin/oscar-5.7.12.200076.0/share/charsets/ |
+--------------------------+-------------------------------------------------+
8 rows in set (0.001 sec)

DEFAULT_CHARACTER_SET_NAMEの対応

それでもエラーが出たので継続調査。
DEFAULT_CHARACTER_SET_NAMEがlatin1になっていた。

MySQL [example]> select * from INFORMATION_SCHEMA.SCHEMATA;
+--------------+--------------------+----------------------------+------------------------+----------+
| CATALOG_NAME | SCHEMA_NAME        | DEFAULT_CHARACTER_SET_NAME | DEFAULT_COLLATION_NAME | SQL_PATH |
+--------------+--------------------+----------------------------+------------------------+----------+
| def          | information_schema | utf8                       | utf8_general_ci        | NULL     |
| def          | example            | latin1                     | latin1_swedish_ci      | NULL     |
| def          | mysql              | latin1                     | latin1_swedish_ci      | NULL     |
| def          | performance_schema | utf8                       | utf8_general_ci        | NULL     |
| def          | sys                | utf8                       | utf8_general_ci        | NULL     |
| def          | tmp                | latin1                     | latin1_swedish_ci      | NULL     |
+--------------+--------------------+----------------------------+------------------------+----------+
6 rows in set (0.004 sec)

MySQL [example]> SELECT @@character_set_database, @@collation_database;
+--------------------------+----------------------+
| @@character_set_database | @@collation_database |
+--------------------------+----------------------+
| latin1                   | latin1_swedish_ci    |
+--------------------------+----------------------+
1 row in set (0.000 sec)

以下のSQLでutf8mb4に設定

MySQL [example]> ALTER DATABASE example CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;

MySQL [example]> SELECT @@character_set_database, @@collation_database;
+--------------------------+----------------------+
| @@character_set_database | @@collation_database |
+--------------------------+----------------------+
| utf8mb4                  | utf8mb4_bin          |
+--------------------------+----------------------+
1 row in set (0.000 sec)

テーブルのDEFAULT CHARSETの対応

それでもエラーが出たので、テーブルを調査。
DEFAULT CHARSETがlatin1になっていた。

MySQL [example]> SHOW CREATE TABLE contracts;
+-----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table     | Create Table                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
+-----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| contracts | CREATE TABLE `contracts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `account_id` int(11) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+-----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.006 sec)

以下のSQLでutf8mb4に設定

MySQL [example]> ALTER TABLE contracts CONVERT TO CHARACTER SET utf8mb4;

MySQL [example]>  SHOW CREATE TABLE contracts;
+-----------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table     | Create Table                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
+-----------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| contracts | CREATE TABLE `contracts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `account_id` int(11) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 |
+-----------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.005 sec)

これで正しく実行できるようになった。

テーブルの設定が最優先されていたので、
はじめからテーブルのDEFAULT CHARSETをutf8mb4に設定していれば解決したと思うが、
DBMS、DBの文字コード設定を見直す良い機会になったと思う。

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

【rails】GoogleMapsAPI 緯度、経度が保存できない時の記述

はじめに

以前書いた記事の追記でgooglemapAPIを使用した際に、住所は登録できているが緯度、経度が保存されないことでハマったので追記として残しておきます。

【rails】google maps api 地図情報含んだ投稿をして表示させる方法

実現したいこと

住所入力して投稿できてデータベースにも保存されているが、緯度、経度が保存されないことを解決させたい。

geocoderについて

いろいろな記事を調べているとgeocoderは何も設定しないと精度があまり良くないことがあるそうです。
解決するためにはGoogle Map APIの情報源を使えるように設定すれば良いそうです。

geocoder.rbファイルを作成

ではさっそく実装していきましょう。

configフォルダ内にgeocoder.rbファイルを作成します。

ターミナル
$ bin/rails g geocoder:config

上記の記述によりconfig/initializers/geocoder.rb  ファイルが作成されます。

作成されたファイルを編集していきます。

geocoder.rb
Geocoder.configure(
  # Geocoding options
  # timeout: 3,                 # geocoding service timeout (secs)
   lookup: :google,         # name of geocoding service (symbol)
  # ip_lookup: :ipinfo_io,      # name of IP address geocoding service (symbol)
  # language: :en,              # ISO-639 language code
   use_https: true,           # use HTTPS for lookup requests? (if supported)
  # http_proxy: nil,            # HTTP proxy server (user:pass@host:port)
  # https_proxy: nil,           # HTTPS proxy server (user:pass@host:port)

#YOUR_API_KEYにはご自身のAPIキーを記述してください。
   api_key: YOUR_API_KEY,               # API key for geocoding service
  # cache: nil,                 # cache object (must respond to #[], #[]=, and #del)
  # cache_prefix: 'geocoder:',  # prefix (string) to use for all cache keys

  # Exceptions that should not be rescued by default
  # (if you want to implement custom error handling);
  # supports SocketError and Timeout::Error
  # always_raise: [],

  # Calculation options
  # units: :mi,                 # :km for kilometers or :mi for miles
  # distances: :linear          # :spherical or :linear
)

これでgeocoderの精度をあげてより詳細な場所を調べられるようになるそうです。

終わりに

以上で自分の問題は解決できました!
他にも記述ミスや処理が抜けている事が原因として考えられる可能性がありますが参考になれば嬉しいです!

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

【rails】google maps api 地図情報含んだ投稿をして表示させる方法

はじめに

・Maps JavaScript API
・Geocoding API

上記のAPIを使用して個人アプリの制作で地図を含んだ投稿をして、表示させる処理を実装しました。
いろいろな記事を参考にさせていただきでき結構ハマったのでまとめておきます。

※地図以外の投稿機能はできているものとしてまとめています

追記
【rails】GoogleMapsAPI 緯度、経度が保存できない時の記述
自分は緯度、軽度がうまく取得できていなかったので、同じような方がいましたらこちらの記事も参考にしてください。

実装内容・イメージ写真

1.ユーザーに地名もしくは住所をぬ有力してもらう

2.詳細ページにてgooglemapにマーカーを落として表示させる

投稿時

※フロント部分はほぼデフォルトのままですご了承ください。

投稿.png

詳細ページ

投稿表示.png

Google API

googlemapを使用するときはAPIを取得しなければいけません。
下記リンクからAPIのKEYを取得してください。
Google Maps Platform
取得方法については今回は割愛します。

今回作成たアプリでは
・Maps JavaScript API
・Geocoding API
を使用しますので有効にしておいてください。

データベース作成

まずデータベースを作成します。
既に作成済みの場合はカラムを追加してください。

postテーブル

Column Type Options
title string null: false
text text null: false

Association

has_one :spot

spotテーブル

Column Type Options
address string null: false
latitude float null: false
longitude float null: false
review_id references foreign_key: true, null: false

Association

belongs_to :post

gemのインストール

Gemfile
gem "gmaps4rails"
gem "geocoder"
gem "gon"
gem "dotenv-rails"

Gemfileに記述できたらbundle installをしてください。

上から
・GoogleMapを簡単に作成できるgem "gmaps4rails"
・地名から緯度経度に変換できるgem "geocoder"
・JSでcontrollerの変数を使えるようにするgem "gon"
・GoogleMapAPIのkeyを隠すためのgem "dotenv-rails"

のために使用します。

JS導入

application.html.hamlを編集う

application.html.haml
!!!
%html
  %head
   .
   .
   .
    = include_gon
    = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
  %body
    = yield
    %script{src: "https://maps.googleapis.com/maps/api/js?key=#{ENV["GOOGLE_MAP_KEY"]}&callback=initMap"}
    %script{src: "//cdn.rawgit.com/mahnunchik/markerclustererplus/master/dist/markerclusterer.min.js"}
    %script{src: "//cdn.rawgit.com/printercu/google-maps-utility-library-v3-read-only/master/infobox/src/infobox_packed.js", type:"text/javascript"}

%head内にはgem "gon"を使えるようにするための記述をします。

%body内にはJSを使うための記述をしています。%head内に記述する方法もあると思いますが今回は%body内に記述しました。

ENV["GOOGLE_MAP_KEY"]には.envファイルに隠したAPIKEYを入れています。

.env
GOOGLE_MAP_KEY = "取得したAPIKEYを記述してください"

.envファイルを作成して上記を記述します。

underscore.jsを作成

app/assets/javascripts下にunderscore.jsを作成して下記リンク先のコードをコピペして貼り付けます。

underscore.js

application.jsを編集

application.jsを編集します。

application.js
//= require underscore
//= require gmaps/google

modelの編集

次に各modelを以下のように編集します。

post.rb
class Post < ApplicationRecord
  has_one :spot, dependent: :destroy
  accepts_nested_attributes_for :spot
end
spot.rb
class Spot < ApplicationRecord
  belongs_to :post

  geocoded_by :address
  after_validation :geocode
end

viewの編集

投稿ページを作成します。googlemapの投稿、表示のぶぶの記述しています。

住所や場所の名前を入力するフォームを作成します。

new.html.haml
= form_with(model: @post, local: true, multipart: true) do |f|
  .spot
    = f.fields_for :spot do |s|
      = s.label :address, "レビュー場所(Google Mapで検索)", class: 'spot__title'
      = s.text_field :address, placeholder: "スポットを入力", id: "address", class: 'spot__text'
    %input{onclick: "codeAddress()", type: "button", value: "検索する"}
    .map{id: "map", style: "height: 320px; width: 640px;"}

次に投稿された詳細ページのgooglemapの部分を記述します。

show.html.haml
.show
  .show__address
    = @post.spot.address
  .show__maps{id: "show_map", style: "height: 320px; width: 400px;"}

controllerの編集

controllerを編集します。

post.controller
def new
  @post = Review.new
  @post.build_spot
end

def create
  @review = Review.new(review_params)
  if @post.save
    redirect_to root_path
  else
    redirect_to new_review_path
  end
end

def show
  @post = Review.find(params[:id])
  @lat = @review.spot.latitude
  @lng = @review.spot.longitude
  gon.lat = @lat
  gon.lng = @lng
end

private

def review_params
  params.require(:post).permit(:title, :text,spot_attributes: [:address])
end

newアクションの.buildメソッドではhas_oneの関係にあたるので
@post.build_spot
としています。

showアクションで記述している

@lat = @review.spot.latitude
@lng = @review.spot.longitude
gon.lat = @lat
gon.lng = @lng

では、controllerで定義した@lat@lngの変数をJavaScriptでも扱えるように、それぞれgon.latgon.lngに代入しています。

JavaScriptの作成

次にJavaScriptファイルを作成していきます。

asset/javascripts/ 内に googlemap.js を作成します。

googlemap.js
let map //変数の定義
let geocoder //変数の定義

function initMap(){ //コールバック関数
  geocoder = new google.maps.Geocoder() //GoogleMapsAPIジオコーディングサービスにアクセス
  if(document.getElementById('map')){ //'map'というidを取得できたら実行
    map = new google.maps.Map(document.getElementById('map'), { //'map'というidを取得してマップを表示
      center: {lat: 35.6594666, lng: 139.7005536}, //最初に表示する場所(今回は「渋谷スクランブル交差点」が初期値)
      zoom: 15, //拡大率(121まで設定可能)
    });
  }else{ //'map'というidが無かった場合
    map = new google.maps.Map(document.getElementById('show_map'), { //'show_map'というidを取得してマップを表示
      center: {lat: gon.lat, lng: gon.lng}, //controllerで定義した変数を緯度・経度の値とする(値はDBに入っている)
      zoom: 15, //拡大率(121まで設定可能)
    });

    marker = new google.maps.Marker({ //GoogleMapにマーカーを落とす
      position:  {lat: gon.lat, lng: gon.lng}, //マーカーを落とす位置を決める(値はDBに入っている)
      map: map //マーカーを落とすマップを指定
    });
  }
}

function codeAddress(){ //コールバック関数
  let inputAddress = document.getElementById('address').value; //'address'というidの値(value)を取得

  geocoder.geocode( { 'address': inputAddress}, function(results, status) { //ジオコードしたい住所を引数として渡す
    if (status == 'OK') {
      let lat = results[0].geometry.location.lat(); //ジオコードした結果の緯度
      let lng = results[0].geometry.location.lng(); //ジオコードした結果の経度
      let mark = {
          lat: lat, //緯度
          lng: lng  //経度
      };
      map.setCenter(results[0].geometry.location); //最も近い、判読可能な住所を取得したい場所の緯度・経度
      let marker = new google.maps.Marker({
          map: map, //マーカーを落とすマップを指定
          position: results[0].geometry.location //マーカーを落とす位置を決める
      });
    } else {
      alert('該当する結果がありませんでした');
    }
  });   
}

上記の記述についてはhttps://qiita.com/kanato4/items/f2f3f7accd880224616a
を参考にさせていただきました。

終わりに

以上なります!
初めてgoogleAPIを使用したアプリケーションの作成でこの部分だけでかなりの時間を使ってしまったので後学者のためにもし参考になれば幸いです。

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

Ubuntu18.04上でのRuby + Railsのインストール(rbenv)

目的

ネット上の記事を参考にしながら、RubyとRailsのインストールを試みたが、
躓いた点がいくつかあったため、一通りのインストール方法を備忘録として纏める。

目次

  1. rbenvのインストール
  2. Rubyのインストール
  3. Railsのインストール
  4. 参考

rbenvのインストール

rbenvをgitからクローンする。

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

ruby-buildというプラグインをインストールすることで、
rbenvにRubyをインストールすることが出来る。

$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build

rbenvを使えるようにするため、.bashrcに設定を記述する。

=>rbenvのpathを環境変数PATHに追加する。

$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc

=>rbenvを初期化する。

$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc

Rubyのインストール

インストール出来るrubyのバージョンを確認する。

$ rbenv install --list-a

1.8.5-p52
1.8.5-p113
1.8.5-p114
1.8.5-p115
1.8.5-p231
1.8.6
1.8.6-p36
1.8.6-p110
1.8.6-p111
1.8.6-p114
  :
  :

rubyをインストールする。

$ rbenv install 2.5.7

下記エラーが表示される場合は、エラーログに従ってパッケージをインストールしてから、
rubyをインストールする。

error: install `curl`, `wget`, or `aria2c` to download packages
error: failed to download ruby-2.5.8.tar.bz2

BUILD FAILED (Ubuntu 18.04 using ruby-build 20200520)

$ apt install wget

下記のエラーが表示される場合は、rbenvの関連パッケージをインストールしてから、
rubyをインストールする。
*私はこのパッケージを全てインストールすることで、エラーを改善することが出来ましたが、
 ここで記載しているパッケージを、全てインストールする必要はないかもしれません...
 必要ないパッケージが見つかりましたら、後ほど修正します。

configure: error: in `/tmp/ruby-build.20200530061452.7106.ZgOAgp/ruby-2.5.7':
configure: error: no acceptable C compiler found in $PATH
$ apt update
$ apt install autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm5 libgdbm-dev

rubyがインストールされたことを確認する。

$ ruby versions

rubyをglobalかlocalに設定する。

$ rbenv gloval 2.5.7
$ rbenv local 2.5.7

rubyがglobalかlocalの設定されたことを確認する。

$ ruby --version

Railsのインストール

railsに必要なパッケージをインストールする。
libsqlite3-devは、Railsインストール時に必要
nodejsは、Rails起動時に必要

*もしかしたらsqlite3のインストールも必要かも...

$ apt install libsqlite3-dev nodejs

railsをインストールする。
-v オプションでバージョンを指定出来る。

$ gem install -v 5.2.2 rails

railsがインストールされたことを確認する。

$ rails --version

プロジェクトの生成

$ rails new hoge_project

参考

ubuntuにRuby on Rails環境を構築してみよう!
rbenvを使ってUbuntu 18.04にRubyをインストールする

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

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

初めに

この記事は
RSpecによるTDDでRailsAPIを実装してみた。part1
この記事のpart2です。もしよろしければpart1からご覧ください。
今回の目標はoctokitを使ってUser認証のログイン機能とログアウト機能を扱えるようになるまでです。
この記事は結構長いです。記事だけの断片的なコードだと理解しづらい部分は多いですので、適度に自分のコードを読んで、内容を理解していってください。また、わかりづらい表現等がありましたら、コメントください。
それでは初めて行きます。

GithubAPIとの通信

Githubに登録

まずはGithubのApiを使って通信をするためにgithubでアプリケーション登録をする必要がある。
https://github.com/settings/apps
このページに飛び、New Github Appから登録に行く。

登録事項は以下。

Application name:
-> 一意で自由にアプリケーションの名前をつける

Homepage URL:
-> http://localhost:3000
開発用のurlを登録します。

Application description:
-> 自由にわかりやすいように説明を入れる

Authorization callback URL:
-> http://localhost:3000/oauth/github/callback
リダイレクト用のURLの設定

入力が終わったら、Register Applicationを押します。
するとかのよような表示が返ってくる。

Owned by: @user_name

App ID: xxxxx

Client ID: Iv1.xxxxxxxxxxxxxxxxxxxxx

Client secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

このClientIDとClientSecreteを使ってgitubAPIに接続します。どこかにコピ-しておく。

octokit

次にoctokitというgemを導入していく。

公式
https://github.com/octokit/octokit.rb

octokitを使うことで、より簡単にgithubとの連携をとることができるらしい。(中で何が起きているかはあまり知らない)

そして既に最初にoctokitのgemは追加してあるので、そのまま続けていく。

ターミナルに移る。

$ GITHUB_LOGIN='githubuser_name' GITHUB_PASSWORD='github_password' rails c

まず、二つの値を環境変数に入れておく。これは普段githubにログインする時に使うusernameとpassword。そしてconsoleが開くことを確認する。
一応
ENV['GITHUB_LOGIN']
などを打って中身が入っていることを確認しておく。

$ client = Octokit::Client.new(login: ENV['GITHUB_LOGIN'], password: ENV['GITHUB_PASSWORD'])
$ client.user

そしてoctokitに接続して、user情報がしっかりと取れていることを確認する。

これはただの演習です。今後、この仕組みを使って実装していく。

User.rb生成

では、Userモデルを作っていく。

$ rails g model login name url avatar_url provider

migrationファイルにデータベースレベルの制限をつけていく。

xxxxxxxxx_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :login, null: false
      t.string :name
      t.string :url
      t.string :avatar_url
      t.string :provider

      t.timestamps
    end
  end
end

ファイルが生成されているので、login属性にnull: falseをつけておく。

$ rails db:migrate

バリデーションテスト

次にモデルレベルでの制限をつけていく。
validationをつけていきたいところですが、まずはテストから書いていく。

spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#validations' do

    it 'should have valid factory' do
      user = build :user
      expect(user).to be_valid
    end

    it 'should validate presence of attributes' do
      user = build :user, login: nil, provider: nil
      expect(user).not_to be_valid
      expect(user.errors.messages[:login]).to include("can't be blank")
      expect(user.errors.messages[:provider]).to include("can't be blank")
    end

    it 'should validate uniqueness of login' do
      user = create :user
      other_user = build :user, login: user.login
      expect(other_user).not_to be_valid
      other_user.login = 'newlogin'
      expect(other_user).to be_valid
    end
  end
end

最初のテストはfactorybotが起動しているかを確認するテスト
二つ目は、loginとproviderが入っているかを確認するテスト
三つ目は、loginがuniqueかどうかを確認するテスト

あと、factorybotが現時点だと、何度createしても同じuserを追加してしまうので、それを修正する。

spec/factories/user.rb
FactoryBot.define do
  factory :user do
    sequence(:login) { |n| "a.levine #{n}" }
    name { "Adam Levine" }
    url { "http://example.com" }
    avatar_url { "http://example.com/avatar" }
    provider { "github" }
  end
end

sequenceを使って解決する。これで毎回作ったuserのloginは一意になる。

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

$ rspec spec/models/user_spec.rb

ここで、typoがなく、正常にエラーが出ていることを確認する。最初のfactorybotが正常に動いているかを確認するテストは成功する。

validation実装

これからvalidationを実装していく。

models/user.rb
class User < ApplicationRecord
  validates :login, presence: true, uniqueness: true
  validates :provider, presence: true
end

テストを実行し成功することを確認する。

次に、githubとやりとりをするためのコードを書いていく。

UserAuthenticator.rb作成

app/libディレクトリを作成
その下に、app/lib/user_authenticator.rbを作成する。

app/lib/user_authenticator.rb
class UserAuthenticator
  def initialize
  end
end

本来先にテストコードを書くのがTDDだが、先にclassを定義してしまった方が、正しいエラーが吐き出されるので、ファイル作成とclassの定義は先にしてしまった方が早い。

codeが正しくない場合のテスト

それからテストを書いていく。
libディレクトリと、ファイルを作成する。
spec/lib/user_authenticator_spec.rb

spec/lib/user_authenticator_spec.rb
require 'rails_helper'

describe UserAuthenticator do
  describe '#perform' do
    context 'when code is incorrenct' do
      it 'should raise an error' do
        authenticator = described_class.new('sample_code')
        expect{ authenticator.perform }.to raise_error(
          UserAuthenticator::AuthenticationError
        )
        expect(authenticator.user).to be_nil
      end
    end
  end
end

今回はperformというインスタンスメソッドを使って、サインインやログインを実行していく

まずは、codeが、不適切なものだった時。
(ちなみにcodeは、githubが発行する一度きりのtokenのことで、今回はそのcodeを実際に受け取ることがないので、codeはただの文字列を使い、そのコードに対して、どうgithubが振舞うか、という部分をモックを使うことで、実際に発行されたcodeなしでテストを完結させるようにしている。codeはgithubuser一意のtokenと交換するために使う。)

described_class.newでインスタンスを作成、authenticator.performでメソッドを実行する。
UserAuthenticator::AuthenticationErrorは独自のクラスで定義する。

テストを実行すると、.performがないと言われる。そして、さらに.userが使えないと言われる。

なので、実際に書いていく。

user_authentiator#perform実装

app/lib/user_authenticator.rb
class UserAuthenticator
  class AuthenticationError < StandardError; end

  attr_reader :user

  def initialize(code)

  end

  def perform
    raise AuthenticationError
  end
end

attr_readerdでいつでもuserを読み込めるようにしておく。
そして、performも定義しておく。
StandardErrorを継承したAuthenticationErrorを定義し、UserAuthenticatorにネストさせておく。
performのなかでraiseさせているのはとりあえずテストを成功させるため。

これで、テストを実行すると成功する。
$ rspec spec/lib/user_authenticator_spec.rb

codeが正しい場合のテスト

そして次は、codeが正しい場合のテストを書く。しかしその前にshould raise an errorで使っている

authenticator = described_class.new('sample_code')
authenticator.perform

この二つの部分を

spec/lib/user_authenticator_spec.rb
  describe '#perform' do
    let(:authenticator) { described_class.new('sample_code') }
    subject { authenticator.perform }

このように定義しておいて、これから書くwhen code is correctでも使っていく。

なので今の全体像は以下のようになる。

spec/lib/user_authenticator_spec.rb
  describe '#perform' do
    let(:authenticator) { described_class.new('sample_code') }
    subject { authenticator.perform }
    context 'when code is incorrenct' do
      it 'should raise an error' do
        expect{ subject }.to raise_error(
          UserAuthenticator::AuthenticationError
        )
        expect(authenticator.user).to be_nil
      end
    end
  end

ではcodeがただしい時のテストも書く

spec/lib/user_authenticator_spec.rb
    context 'when code is correct' do
      it 'should save the user when does not exists' do
        expect{ subject }.to change{ User.count }.by(1)
      end
    end

userがあらかじめdatabaseに存在しないuserだった場合は、User.countが1増える。
これはuserの新規登録という事。

これで、テストを実行するがもちろん失敗する。それはperformアクションでは何があってもraise AuthenticationErrorというふうに書いてあるから。
なので、performメソッドを実装していく。

実行部分の記述

app/lib/user_authenticator.rb
  def perform
    client = Octokit::Client.new(
      client_id: ENV['GITHUB_CILENT_ID'],
      client_secret: ENV['GITHUB_CILENT_SECRET'],
    )
    res = client.exchange_code_for_token(code)
    if res.error.present?
      raise AuthenticationError
    else

    end
  end

ここでやっていることは、まず、記事の最初にプロジェクトをgithubに認証させている。
この記事の最初にこのプロジェクトをgithubに登録した時にclient_idとclient_secretを表示されたその二つの値を、この環境変数の中に入れる。しかし今回、実際の値は使わない。とりあえず、いったんそこは後で説明する。

client.exchange_code_for_token(code)
この部分がそのままではあるが、codeをtokenと交換している。
tokenは上記したようにgithubAPIが生成した一時的なものでしかない。

そして、もしも、返ってきたresponseがエラーの場合はres.errorで取り出すことができるので、errorが入っていた場合にのみエラーをraiseする。

これでいったん、テストを実行する。

404 - Error: Not Found

おそらく404が吐き出される。これはGITHUB_CILENT_IDとGITHUB_CILENT_SECRETの中身がからだから。
しかし、これはテストなのでここで本当の値を入れるわけにはいかない。
できるだけ、テストはネットワーク環境などを排除して、テストのみで完結するようにするのが理想とされている。

mock実装

そこでテストがわでモックを使う。モックとはgithubの通信の代わりとなるものをこちら側で作成して、テストで完結させるためのもの。

spec/lib/user_authenticator_spec.rb
    context 'when code is incorrenct' do
      before do
        allow_any_instance_of(Octokit::Client).to receive(
          :exchange_code_for_token).and_return(error)
      end

そこでこのようにbeforeを使い、allow_any_instance_ofというメソッドを使う。

allow_any_instance_of(インスタンス名).to receive(:メソッド名).and_return(返り値)

このようにして使う。これを使って、指定したインスタンスの指定したメソッドが呼び出されたときの返り値を指定することができる。

Octokit::Clientのインスタンスからexchange_code_for_tokenメソッドを呼び出した時にerrorが返る。

その返り値のerrorを定義する。

spec/lib/user_authenticator_spec.rb
    context 'when code is incorrenct' do
      let(:error) {
        double("Sawyer::Resource", error: "bad_verification_code")
      }

doubleはモックを生成する時のメソッド。
Sawyer::Resourceはクラス名で、そのクラスのメソッドとして、errorを使うことができる。
実際のエラーを忠実に再現することができる。

これでテストを実行すると一つめが成功するが、もう一つは失敗する。
404なので、先ほどと同じ。

二つ目のテストもさっきのモックと同じ要領で定義していく。

spec/lib/user_authenticator_spec.rb
    context 'when code is correct' do
      before do
        allow_any_instance_of(Octokit::Client).to receive(
          :exchange_code_for_token).and_return('validaccesstoken')
      end

しかし今度は、errorを出すのではなく、validaccesstokenを返す。実際に何か意味がある文字列ではないが、errorではないという意味でこの値でも、テストとしては十分有効なtokenとして機能する。

テストを実行。

undefined method `error' for "validaccesstoken":String

というメッセージが出る。

これは

app/lib/user_authenticator.rb
    if res.error.present?

この部分のことだが、resにerrorがない時にもerrorを読み込もうとしているのでエラーが出た。
なので、errorがない時はnilを返すように書く。

app/lib/user_authenticator.rb
    if res.try(:error).present?

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

expected User.count to have changed by 1, but was changed by 0

まだ保存する操作を書いていないので正常なメッセージだと言える。
なので、データを保存していく処理を書いていく。

#perform 保存処理実装

app/lib/user_authenticator.rb
    client = Octokit::Client.new(
      client_id: ENV['GITHUB_CILENT_ID'],
      client_secret: ENV['GITHUB_CILENT_SECRET'],
    )
    token = client.exchange_code_for_token(code)
    if token.try(:error).present?
      raise AuthenticationError
    else
      user_client = Octokit::Client.new(
        access_token: token
      )
      user_data = user_client.user.to_h
        slice(:login, :avatar_url, :url, :name)
      User.create(user_data.merge(provider: 'github'))
    end

このように書き換える。
codeと交換して返ってきたtokenを使って、githubuserのインスタンスを作る。

user_client = Octokit::Client.new(
        access_token: token
      )

上記のこの部分だが、loginとpasswordを使ってインスタンスを生成するのと同じことをしている。tokenを使ってもloginとpasswordを使ってもどちらも同じ結果が出力される。

// ただのサンプルなので実際に打たなくても良い
$ client = Octokit::Client.new(login: ENV['GITHUB_LOGIN'], password: ENV['GITHUB_PASSWORD'])
$ client.user

この記事の最初の方で、このようなコマンドをコンソールで打ったが、これと全く同じことをしている。実際にclient.userをするとgithubuserのデータを取得できる。しかし、形式が、Sawyer::Resourceというもので、非常に扱いづらい。なので、一度to_hでハッシュに変換してからsliceメソッドで中身を取り出している。そしてそのままcreateメソッドを使ってdatabaseに保存している。providerをmergeしているのは、providerは取り出したデータの中にはないので、自分でつける必要がある。もしつけなかったらvalidationに引っかかる。

ついでにだが、resをtokenに変更しておいた。実際にロジック的にどういう意味を持つかを変数名にする方が好ましいから。

そしてテストを実行する。

401 - Bad credentials

次はこのようなメッセージがかえる。
401はログインなどができなかったりする場合に返ってくるエラーのよう
しかし今回はただのモックで作ったインスタンスなので、実際に認証をするができている必要はない。

app/lib/user_authenticator.rb
      user_data = user_client.user.to_h.
        slice(:login, :avatar_url, :url, :name)

現在このuser_client.userの部分でエラーが起きている。
なので、user_client.userをした時にどう返すかというものをモックで再現する。

spec/lib/user_authenticator_spec.rb
        allow_any_instance_of(Octokit::Client).to receive(
          :exchange_code_for_token).and_return('validaccesstoken')
        allow_any_instance_of(Octokit::Client).to receive(
          :user).and_return(user_data)
      end

したの:userの方を追加する。そして、変数のuser_dataを追加する。

spec/lib/user_authenticator_spec.rb
    context 'when code is correct' do
      let(:user_data) do
        {
          login: 'a.levine 1',
          url: 'http://example.com',
          avatar_url: 'http://example.com/avatar',
          name: 'Adam Levine'
        }
      end

これで、テストを実行して成功する。

ついでに、保存されている値が正しいかも確認しておく。

spec/lib/user_authenticator_spec.rb
        allow_any_instance_of(Octokit::Client).to receive(
          :exchange_code_for_token).and_return('validaccesstoken')
        allow_any_instance_of(Octokit::Client).to receive(
          :user).and_return(user_data)
      end
      it 'should save the user when does not exists' do
        expect{ subject }.to change{ User.count }.by(1)
        expect(User.last.name).to eq('Adam Levine')
      end

一番下の行を追加しておく。

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

しかし、毎回新しいuserを生成しているが、一度createしたuserは使いまわしたい。当たり前だが、毎回新規登録をするようなものなので、効率が悪い。
なので使いまわせるようにコードを記述していく。

一度保存したuserを使いまわす

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

spec/lib/user_authenticator_spec.rb
      it 'should reuse already registerd user' do
        user = create :user, user_data
        expect{ subject }.not_to change{ User.count }
        expect(authenticator.user).to eq(user)
      end

一度userを作って、それと同じuser_dataを使って、authenticator.performを行う。
そして、そのauthenticator.performをして作ったuserとfactorybotで作ったuserが同じものかを確認する。

テストを実行して、失敗することを確認する。今はまだ使い回すのではなく、毎回createをしている。
なので、使いまわせるように記述していく。

app/lib/user_authenticator.rb
-      User.create(user_data.merge(provider: 'github'))
+      @user = if User.exists?(login: user_data[:login])
+        User.find_by(login: user_data[:login])
+      else
+        User.create(user_data.merge(provider: 'github'))
+      end

このように書き換える。もし同じuserが存在している時はfind_byを使うという分岐を作る。

テストを実行すると成功する。

リファクタリング

しかし現時点だと、performメソッドの記述量が多すぎることと、performメソッドの責任が曖昧になっている。performメソッドはいわゆる実行、という意味を持つので、実行するためだけのメソッドであることが好ましい。なので、値を生成したり、整えたりしているロジックを別のメソッドに書き出す。

app/lib/user_authenticator.rb
  def perform
-    client = Octokit::Client.new(
-      client_id: ENV['GITHUB_CILENT_ID'],
-      client_secret: ENV['GITHUB_CILENT_SECRET'],
-    )
-    token = client.exchange_code_for_token(code)
    if token.try(:error).present?
      raise AuthenticationError
    else
-     user_client = Octokit::Client.new(
-        access_token: token
-      )
-      user_data = user_client.user.to_h.
-        slice(:login, :avatar_url, :url, :name)
-      @user = if User.exists?(login: user_data[:login])
-        User.find_by(login: user_data[:login])
-      else
-        User.create(user_data.merge(provider: 'github'))
-      end
+      prepare_user
    end

この部分をざくりと削除して他の場所に移していく。移す場所はprivateメソッドで定義する。理由は別に外部のクラスから呼び出す必要のない値を定義するから。

app/lib/user_authenticator.rb
  private

+  def client
+    @client ||= Octokit::Client.new(
+      client_id: ENV['GITHUB_CILENT_ID'],
+      client_secret: ENV['GITHUB_CILENT_SECRET'],
+    )
+  end
+
+  def token
+    @token ||= client.exchange_code_for_token(code)
+  end
+
+  def user_data
+    @user_data ||= Octokit::Client.new(
+      access_token: token
+    ).user.to_h.slice(:login, :avatar_url, :url, :name)
+  end
+
+  def prepare_user
+    @user = if User.exists?(login: user_data[:login])
+      User.find_by(login: user_data[:login])
+    else
+      User.create(user_data.merge(provider: 'github'))
+    end
+  end

  attr_reader :code
end

こんな感じで書きだす。下のメソッドが上のメソッドを呼び出すという構造になっていて、きれいに責任を分離している。

これで、テストを実行して失敗しないことを確認する。

これでいったんリファクタリングは終わり。

次にいく。

User認証用のtoken生成

次はこの今作っているrailsapi専用のaccess_tokenを作っていく。
exchange_code_for_tokenメソッドを使って手に入るtokenはあくまでもgithubAPIにアクセスしてuser情報を取得するためのtokenなのでそれを僕たちが作っているrailsAPIのリクエストを認証するために使うことはできない。

今からは今作っているrailsAPIのリクエスト認証をするためのtokenを作っていく。このtokenが必要になるのは、createアクションをするときや、deleteアクションをする時に必要になる。逆に、indexアクションやshowアクションをする時はtokenがなくてもリクエストを受け付けるようにする。
しかしそれはそれぞれアプリケーション次第ではある。

token生成のテスト

ではそのtokenを作っていくのだが、まずはテストから書いていく。

spec/lib/user_authenticator_spec.rb
      it "should create and set user's access token" do
        expect{ subject }.to change{ AccessToken.count }.by(1)
        expect(authenticator.access_token).to be_present
      end

末尾のこのテストを追加。

そして、その後に、performメソッドを編集していく。

app/lib/user_authenticator.rb
     else
       prepare_user
+      @access_token = if user.access_token.present?
+                 user.access_token
+               else
+                 user.create_access_token
+               end
     end

このように、tokenをインスタンスのattributeとしておく。

app/lib/user_authenticator.rb
attr_reader :user, :access_token

さらにaccess_tokenを呼び出せるようにしておく。
とりあえず説明はのちに詳しくする。

AccessTokenモデル生成

$ rails g model access_token token user:references

とりあえず、access_tokenモデルを作成していく。
これにより、belongs_to :userを持ったaccess_tokenモデルが作成される。

userモデルの方にもアソシエーションを設定する。

app/models/user.rb
class User < ApplicationRecord
  validates :login, presence: true, uniqueness: true
  validates :provider, presence: true

  has_one :access_token, dependent: :destroy # 追加
end
db/migrate/xxxxxxxxx_create_access_tokne.rb
class CreateAccessTokens < ActiveRecord::Migration[6.0]
  def change
    create_table :access_tokens do |t|
      t.string :token, null: false
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end

migrationファイルも確認しておく、tokenにはnill: falseをつけておく。

rails db:migrateを実行。

次にaccesstokenのテストも準備しておく。

spec/models/access_token_spec.rb
require 'rails_helper'

RSpec.describe AccessToken, type: :model do
  describe '#validations' do
    it 'should have valid factory' do

    end

    it 'should validate token' do

    end
  end
end

諸々準備ができたのでテストを実行する。
$ rspec spec/lib/user_authenticator_spec.rb

SQLite3::ConstraintException: NOT NULL constraint failed: access_tokens.token

するとこのようなメッセージが吐かれる。
このエラーは、databaseレベルでのnull: falseをつけているのに、nullだった場合に起こるようだ。

ではnullにならないようにtokenを生成するロジックを書く。その前にテストを書く。

spec/models/access_token_spec.rb
  describe '#new' do
    it 'should have a token present after initialize' do
      expect(AccessToken.new.token).to be_present
    end

    it 'should generate uniq token' do
      user = create :user
      expect{ user.create_access_token }.to change{ AccessToken.count }.by(1)
      expect(user.build_access_token).to be_valid
    end
  end

このコードを末尾に追加する。

一つ目は、AccessTokenをnewした時に、ちゃんと、tokenが入っているかどうか
後で記述するが、newした時に自動的にtokenが入るように後で書く。

二つ目は、AccessTokenのcountが1増えるかどうかと、
validationにひっからないかどうか。
validationにひっからないかどうかだが、いつもならモデルをcreateして、二つ目に一つ目の値を使って、buildをして、validationにちゃんとひっかるかどうかを確認するが、今回は少し特殊でnewした時にtokenが自動生成されるので、そのテストはできない。なぜならAccessToken.new(old_token)のように引数を指定することができないから。AccessToken.newとすれば、tokenは自動ではいる。

token生成ロジック実装

ではtokenを生成するロジックを書いていく。

app/models/access_token.rb
class AccessToken < ApplicationRecord
  belongs_to :user

  after_initialize :generate_token

  private

  def generate_token
    loop do
      break if token.present? && !AccessToken.exists?(token: token)
      self.token = SecureRandom.hex(10)
    end
  end
end

after_inializeで指定したメソッドは、モデルが作成される時に実行される。

loopで回しているのはbreak ifで指定した条件に当てはまらない限り何度でもtokenを作成したいから。
SecureRandomクラスを使ってtokenを生成する。
値はランダムで作成されるので、全く同じ値が生成されないとは限りません。なのでloopさせる。
breakの条件はtokenに値が入っている。かつ、databaseに同じ値が存在していない。
それが当てはまらない限りは何度でもloopする。大抵は一度回ればbreakされる。

テストを実行。
$ rspec spec/models/access_token_spec.rb
$ rspec spec/lib/user_authenticator_spec.rb

このテストが通ることを確認する。

ちなみに、user_authenticator.rbでのuser.create_access_tokenこのメソッドはどこかで定義したわけではなく、railsが自動生成してくれるもの。意味はそのままだが、わかりやすく置き換えると、
AccessToken.create(user_id: user.id)
これと同じ意味になる。

では、token生成のロジックが終わったので、次に行く。

ログイン機能

次はログイン機能の全体像を実装していく。今はtokenを生成する仕組みはできているが、まだそのtokenを利用したログイン機能を実装はできていない。なのでそのあたりを実装していく。

エンドポイントのテスト

しかしまずはテストから書く。今はroutingがまだできていないので、routingのテストから書いていく。
記述するファイルはないので作成する。

spec/routing/access_token_spec.rb
require 'rails_helper'

describe 'access tokens routes' do
  it 'should route to access_tokens create action' do
    expect(post '/login').to route_to('access_tokens#create')
  end
end

記述の説明は割愛。

テストを実行すると、no route match /loginと出るので、routes.rbを編集する。

config/routes.rb
Rails.application.routes.draw do
+  post 'login', to: 'access_tokens#create'
  resources :articles, only: [:index, :show]
end

テスト実行。

A route matches "/login", but references missing controller: AccessTokensController

controllerがないと言われているので、作っていく。

access_tokens_controller 生成

$ rails g controller access_tokens

      create  app/controllers/access_tokens_controller.rb
      invoke  rspec
      create    spec/requests/access_tokens_request_spec.rb

再度テスト実行。テストが通る。これでログインのエンドポイントの設置は終了。

access_tokens_controllerのテスト

ではcontrollerのテストをしていく。次のファイルを作成して記述する。

spec/controllers/access_tokens_controller_spec.rb
require 'rails_helper'

RSpec.describe AccessTokensController, type: :controller do
  describe '#create' do
    context 'when invalid request' do
      it 'should return 401 status code' do
        post :create
        expect(response).to have_http_status(401)
      end
    end

    context 'when success request' do

    end
  end
end

認証をせずに401が返ってくることを期待する。401はunauthorized(不許可)、だが、意味的にはunauthenticated(未認証)であるので、認証がされていない時のレスポンスとして用いることが多い。

今更ではあるが、rails g controllerをするとrequests/access_tokens_request_spec.rbのようなファイルが自動生成されている。これはcontrollerのテストの後継となるものだが、controller_specと書き方が少し変わってくるので、今回はわざわざ自分でファイルを作成して記述していいる。本来はrequest_specで書く方が推奨されている。

テストを実行する。

AbstractController::ActionNotFound:
The action 'create' could not be found for AccessTokensController

createアクションが定義されていないので、記述する。

app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  def create

  end
end

テストを実行。401を期待しているが204が返って来ている。
204は:no_contentのこと。

なのでとりあえず、テストを通すために、controllerに記述していく。

create実装

app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  def create
    render json: {}, status: 401
  end
end

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

さらにテストを追記していく。

spec/controllers/access_token_controller_spec.rb
    context 'when invalid request' do
+      let(:error) do
+        {
+          "status" => "401",
+          "source" => { "pointer" => "/code" },
+          "title" =>  "Authentication code is invalid",
+          "detail" => "You must privide valid code in order to exchange it for token."
+        }
+      end
      it 'should return 401 status code' do
        post :create
        expect(response).to have_http_status(401)
      end

+      it 'should return proper error body' do
+        post :create
+        expect(json['errors']).to include(error)
+      end
    end

401の場合に正しいerrorのresが返ってくることを期待する。
error文は以下のサイトからコピーして来たものを編集して使っている。
https://jsonapi.org/examples/

そして、テストを実行。

expected: {"detail"=>"You must privide valid code in order to exchange it for token.", "source"=>{"pointer"=>"/code"}, "status"=>"401", "title"=>"Authentication code is invalid"}
got: nil

nilが返って来ているので、controlle側できちんとerrorを返す処理を書く。

app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  def create
    error = {
      "status" => "401",
      "source" => { "pointer" => "/code" },
      "title" =>  "Authentication code is invalid",
      "detail" => "You must privide valid code in order to exchange it for token."
    }
    render json: { "errors": [ error ] }, status: 401
  end
end

これでテストが通ることを確認する。

現在はcreateアクションが呼び出され時に全てにおいてerrorを出しているが、それを修正する。

app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  rescue_from UserAuthenticator::AuthenticationError, with: :authentication_error

  def create
    authenticator = UserAuthenticator.new(params[:code])
    authenticator.perform
  end

  private

  def authentication_error
    error = {
      "status" => "401",
      "source" => { "pointer" => "/code" },
      "title" =>  "Authentication code is invalid",
      "detail" => "You must privide valid code in order to exchange it for token."
    }
  end

リファクタリングもかねて、コードを編集している。
ここでやっとUserAuthenticator.new(params[:code])を書くことになる。
これまでずっと書いていた、codeとtokenを交換して、userを作成するロジックがUserAuthenticatorには書かれているが、それをここで呼び出す。

そして、performで実行する。

401エラーの本体はメソッドに書き出している。
現時点で想定される返ってくるerrorはUserAuthenticator::AuthenticationErrorなので、rescue_fromによって、rescueする。メソッドに書き出しているので、rescue_fromで呼び出す操作が可能。

後、UserAuthenticator::AuthenticationErrorではcodeがblankの時も同じようにエラーを出したい。
ついでに、リファクタリングが必要なのでしていく。

リファクタリングと修正

app/lib/user_authenticator.rb
  def perform
    raise AuthenticationError if code.blank? || token.try(:error).present?
    prepare_user
    @access_token = if user.access_token.present?
               user.access_token
             else
               user.create_access_token
             end
  end

これでcodeがblankの時はerrorを出すことができる。

もう一度おさらいしておくとcodeはフロントエンドから送られてくるtokenのこと。フロントエンドがgithubからtokenを取得して来てくれてそれをapiに送ってくる。それがcode(github_access_code)。
APIはそのcodeを受け取ってGitHubと通信を行いcodeをtokenと交換してもらう(exchange_code_for_tokenメソッドによって)。そのtokenによって、githubuserの情報をgithubAPIから取得することができる。

それを踏まえた上で、codeは十分にblankである可能性は考えられるので、errorを用意しておく。

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

さらにリファクタリングをする。

app/controlers/access_token_controller.rb
class AccessTokensController < ApplicationController
-  rescue_from UserAuthenticator::AuthenticationError, with: :authentication_error

  def create
    authenticator = UserAuthenticator.new(params[:code])
    authenticator.perform
  end

-  private
-
-  def authentication_error
-    error = {
-      "status" => "401",
-      "source" => { "pointer" => "/code" },
-      "title" =>  "Authentication code is invalid",
-      "detail" => "You must privide valid code in order to exchange it for token."
-    }
-    render json: { "errors": [ error ] }, status: 401
-  end
end
app/controllers/application_controller.rb
class ApplicationController < ActionController::API
+  rescue_from UserAuthenticator::AuthenticationError, with: :authentication_error

+  private

+  def authentication_error
+    error = {
+      "status" => "401",
+      "source" => { "pointer" => "/code" },
+      "title" =>  "Authentication code is invalid",
+      "detail" => "You must privide valid code in order to exchange it for token."
+    }
+    render json: { "errors": [ error ] }, status: 401
+  end
end

完全にauthentication_errorはapplication_controllerに任せてしまい、全てのコントローラーでこのエラーを拾って来れるようにする。理由は認証エラーというのはどのコントローラーでも起きる可能性があるから。

テストを実行して、挙動が何も変わっていないことを確認する。

そして、この実装はテストでも同じように使いまわせるとなお良い。
説明が長くなってしまうので、コードはひとまず全ての変更を貼り付ける

spec/controllers/access_token_controller_spec.rb
RSpec.describe AccessTokensController, type: :controller do
  describe '#create' do
-    context 'when invalid request' do
+    shared_examples_for "unauthorized_requests" do
      let(:error) do
        {
          "status" => "401",
@ -11,17 +11,34 @@ RSpec.describe AccessTokensController, type: :controller do
          "detail" => "You must privide valid code in order to exchange it for token."
        }
      end

      it 'should return 401 status code' do
-        post :create
+        subject
        expect(response).to have_http_status(401)
      end

      it 'should return proper error body' do
-        post :create
+        subject
        expect(json['errors']).to include(error)
      end
    end

+    context 'when no code privided' do
+      subject { post :create }
+      it_behaves_like "unauthorized_requests"
+    end
+    context 'when invalid code privided' do
+      let(:github_error) {
+        double("Sawyer::Resource", error: "bad_verification_code")
+      }
+      before do
+        allow_any_instance_of(Octokit::Client).to receive(
+          :exchange_code_for_token).and_return(github_error)
+      end
+      subject { post :create, params: { code: 'invalid_code' } }
+      it_behaves_like "unauthorized_requests"
+    end

    context 'when success request' do

    end

何をしているかはコードをじっくりと読んで欲しいのだが、ここでは二つのテストをshared_examples_forによって使いまわしている。
should return 401 status code
should return proper error body

この二つのテストは今後も使い回すことが多い。また、shared_examples_forを呼び出すには、it_behaves_likeを使って呼び出すことができる。
subjectを使って、DRYにすることで、subjectにはcontextごとに自由に値を入れることができる。

spec/controllers/access_token_controller_spec.rb
      let(:github_error) {
        double("Sawyer::Resource", error: "bad_verification_code")
      }
      before do
        allow_any_instance_of(Octokit::Client).to receive(
          :exchange_code_for_token).and_return(github_error)
      end

また、この部分だが、この記述は以前もテストで使ったもので、githubAPIに直接接続することなく、mockで再現している。これにより、githubに実際に接続しなくともgithubAPIを再現することができる。

次はcodeが正しい時のテストを書いていく。

spec/controllers/access_token_controller_spec.rb
    context 'when success request' do
      let(:user_data) do
        {
          login: 'a.levine 1',
          url: 'http://example.com',
          avatar_url: 'http://example.com/avatar',
          name: 'Adam Levine'
        }
      end

      before do
        allow_any_instance_of(Octokit::Client).to receive(
          :exchange_code_for_token).and_return('validaccesstoken')
        allow_any_instance_of(Octokit::Client).to receive(
          :user).and_return(user_data)
      end

      subject { post :create, params: { code: 'valid_code' } }
      it 'should return 201 status code' do
        subject
        expect(response).to have_http_status(:created)
      end
    end

これは単純にmockでcodeが正しいか正しくないかを操作している。
単純にcodeが正しい場合は201が返ってくることを期待している。

テストを実行。

expected the response to have status code :created (201) but it was :no_content (204)

このメッセージが表示される
なので、responseで201が返ってくるようにcontrollerを編集する。

app/controlers/access_token_controller.rb
  def create
    authenticator = UserAuthenticator.new(params[:code])
    authenticator.perform

    render json: {}, status: :created
  end

renderを追加し、createdを返す。

これで再びテストを実行し、通ることを確認。

次にしっかりとresponseを返すように実装したい。なので、テストから書いていく。

spec/controllers/access_token_controller_spec.rb
      it 'should return proper json body' do
        expect{ subject }.to change{ User.count }.by(1)
        user = User.find_by(login: 'a.levine 1')
        expect(json_data['attributes']).to eq(
          { 'token' => user.access_token.token }
        )
      end

このテストを末尾に追加。
テストの内容は、articleの時と同じように、json_data['attributes']で値を受け取り中身が正しいかを確認する。User.find_byで取り出しているuserは前に記述したuser_dataを使ったmockによって記述されているので、その値と、responseとして返ってくる値は同じである、というテスト。

しかしテストを実行しても、json_dataでは取り出せていない、理由はserializerを使っていないから、json.dataが存在しないから。なので、きちんと整った形式でのresponseをするためにserializerを導入していく。

serializer生成

$ rails g serializer access_token

これにより、作られるファイルに記述していく。

app/serializers/access_token_serializer.rb
class AccessTokenSerializer < ActiveModel::Serializer
  attributes :id, :token
end

tokenの記述を足しておく。これにより、responseにtokenを含めることができる。

そしてcontrollerにもrenderで返す値を指定しておく。

access_tokens_controller.rb
-    render json: {}, status: :created
+    render json: authenticator.access_token, status: :created
  end

これにより、からのハッシュではなく、しっかりと整形されたresponseが返せるようになる。

テストを実行。するとメッセージが出る。

       expected: {"token"=>"6c7c4213cb78c782f6f6"}
            got: {"token"=>"2e4c724d374019f3fb26"}

どこかで、tokenが再度作られてしまい、値が切り替わっている。
これはリロードをすると、tokenがその度に作られてしまっているというバグ。

なので、そのバグ修正のためのテストを書いていく。

spec/models/access_token_spec.rb
    it 'should generate token once' do
      user = create :user
      access_token = user.create_access_token
      expect(access_token.token).to eq(access_token.reload.token)
    end

まずは、バグが再現できているかどうかを確認するために、テストを実行する。

expected: "3afe2f824789a229014c"
got: "c5e04c73aa7ff89fd0a1"

きちんと再現できているので、メッセージが出た。

では改善していく。まず、バグが起きているgenerate_tokenメソッドを見てみる。

app/models/access_token.rb
def generate_token
  loop do
    break if token.present? && !AccessToken.exists?(token: token)
      self.token = SecureRandom.hex(10)
  end
end 

ここでおかしいところがある、問題はbreakの条件がいけなかった。
break if token.present? && !AccessToken.exists?(token: token)
この条件は、tokenにしっかりと値が入っている。かつそのtokenはデータベースに存在しない。という条件になる。しかしそれだと、少し矛盾したことになってしまう。tokenが存在しているということはデータベースに保存しているということなので、この条件式は満たされることがない。
なので、指定したtoken以外のtokenで同じtokenを持っているものが存在しないという条件にしていく。

app/models/access_token.rb
-      break if token.present? && !AccessToken.exists?(token: token)
+      break if token.present? && !AccessToken.where.not(id: id).exists?(token: token)

このように今指定のtoken以外のtokenという条件を作ることができる。
これで、テストを実行して通ることを確認する。

ログアウト機能

エンドポイントの追加テスト

それでは、ログアウト機能を実装していく。

spec/routeing/access_token_spec.rb
  it 'should route  to acces_tokens destroy action' do
    expect(delete '/logout').to route_to('access_tokens#destroy')
  end

routingのテストを書く。

config/routes.rb
Rails.application.routes.draw do
  post 'login', to: 'access_tokens#create'
  delete 'logout', to: 'access_tokens#destroy'
  resources :articles, only: [:index, :show]
end

logoutの行を追加。

テストが通る。

実装

次にコントローラーのテストを書いていく。

spec/controllers/access_token_controller.rb
@@ -1,9 +1,9 @@
require 'rails_helper'

RSpec.describe AccessTokensController, type: :controller do
- describe '#create' do
+ describe 'POST #create' do
    shared_examples_for "unauthorized_requests" do
-     let(:error) do
+     let(:authentication_error) do
        {
          "status" => "401",
          "source" => { "pointer" => "/code" },
@ -19,7 +19,7 @@ RSpec.describe AccessTokensController, type: :controller do

      it 'should return proper error body' do
        subject
-       expect(json['errors']).to include(error)
+       expect(json['errors']).to include(authentication_error)
      end
    end

@ -74,4 +74,33 @@ RSpec.describe AccessTokensController, type: :controller do
      end
    end
  end

+ describe 'DELETE #destroy' do
+   context 'when invalid request' do
+     let(:authorization_error) do
+       {
+         "status" => "403",
+         "source" => { "pointer" => "/headers/authorization" },
+         "title" =>  "Not authorized",
+         "detail" => "You have no right to access this resource."
+       }
+     end
+
+       subject { delete :destroy }
+
+     it 'should return 403 status code' do
+       subject
+       expect(response).to have_http_status(:forbidden)
+     end
+
+     it 'should return proper error json' do
+       subject
+       expect(json['errors']).to include(authorization_error)
+     end
+   end
+
+   context 'when valid request' do
+
+   end
+ end
end

元々errorとして扱っていた403エラーだが、役割をはっきりさせるために、命名を変更。
そして、destroy専用のテストを丸々書いていく。
内容は読んだまま。

@@の表記は何行の記述かを表しているコード、実際に書く必要はない。

そして、controllerを実装していく。

app/controllers/access_tokens_controller.rb
  def destroy
    raise AuthorizationError
  end

destroyメソッドを定義する。まずはエラーのレスポンスのテストを通すために、AuthorizationErrorをraiseして、application_controllerに実際にそのエラーの実態を定義していく。

app/controllers/application_controller.rb
class ApplicationController < ActionController::API
+ class AuthorizationError < StandardError; end
  rescue_from UserAuthenticator::AuthenticationError, with: :authentication_error
+ rescue_from AuthorizationError, with: :authorization_error

  private

@ -12,4 +14,14 @@ class ApplicationController < ActionController::API
    }
    render json: { "errors": [ error ] }, status: 401
  end

+ def authorization_error
+   error = {
+     "status" => "403",
+     "source" => { "pointer" => "/headers/authorization" },
+     "title" =>  "Not authorized",
+     "detail" => "You have no right to access this resource."
+   }
+   render json: { "errors": [ error ] }, status: 403
+ end
end

エラーの内容はテストに書いたものと同じ。

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

しかし少し重複している記述があるのでDRYにしていく。

spec/controllers/access_tokens_controller_spec.rb
  describe 'DELETE #destroy' do
    shared_examples_for 'forbidden_requests' do
    end

まず、describeの下にshared_examples_forを使って、記述をまとめていく。

shared_examples_forの中に、入れるのは以下の記述。

spec/controllers/access_tokens_controller_spec.rb
    shared_examples_for 'forbidden_requests' do
      let(:authorization_error) do
        {
          "status" => "403",
          "source" => { "pointer" => "/headers/authorization" },
          "title" =>  "Not authorized",
          "detail" => "You have no right to access this resource."
        }
      end

      it 'should return 403 status code' do
        subject
        expect(response).to have_http_status(:forbidden)
      end

      it 'should return proper error json' do
        subject
        expect(json['errors']).to include(authorization_error)
      end
    end

今まで記述していたテストを一つにまとめる。

spec/controllers/access_tokens_controller_spec.rb
    context 'when invalid request' do
      subject { delete :destroy }
      it_behaves_like 'forbidden_requests'
    end

そしてshared_expample_forを呼び出すのはit_behaves_likesという記述なので、これで文字列でさっき指定したforbidden_requestsを呼び出す。

これでさっきと同じ環境を作り出す事ができたので再度実行して、テストが通ることを確認する。

次にこれらのshared_example_forを使いまわせるようにさらに一つのファイルにまとめていく。今のaccess_tokens_controller_spec.rbにはshared_example_forが二つ存在しているのでその二つを同じファイルにまとめていく。

spec/support/shared/json_errors.rbを作成

中にshared_example_forの記述を入れていく。

spec/support/shared/json_errors.rb
require 'rails_helper'

shared_examples_for 'forbidden_requests' do

  let(:authorization_error) do
    {
      "status" => "403",
      "source" => { "pointer" => "/headers/authorization" },
      "title" =>  "Not authorized",
      "detail" => "You have no right to access this resource."
    }
  end

  it 'should return 403 status code' do
    subject
    expect(response).to have_http_status(:forbidden)
  end

  it 'should return proper error json' do
    subject
    expect(json['errors']).to include(authorization_error)
  end
end

shared_examples_for "unauthorized_requests" do
  let(:authentication_error) do
    {
      "status" => "401",
      "source" => { "pointer" => "/code" },
      "title" =>  "Authentication code is invalid",
      "detail" => "You must privide valid code in order to exchange it for token."
    }
  end

  it 'should return 401 status code' do
    subject
    expect(response).to have_http_status(401)
  end

  it 'should return proper error body' do
    subject
    expect(json['errors']).to include(authentication_error)
  end
end

そして、切り取り元の記述は全て消しておく。

spec/controllers/access_tokens_controller_spec.rb
  describe 'DELETE #destroy' do
    subject { delete :destroy }

subject定義のネストを一段上げておく。
そして、テストを二つ追加する。

spec/controllers/access_tokens_controller_spec.rb
  describe 'DELETE #destroy' do
    subject { delete :destroy }

    context 'when no authorization header provided' do
      it_behaves_like 'forbidden_requests'
    end

    context 'when invalid authorization header provided' do
      before { request.headers['authorization'] = 'Invalid token' }

      it_behaves_like 'forbidden_requests'
    end

    context 'when valid request' do

    end
  end

このテストは、subjectを書かないのは、既にshared_example_forにsubject書いてあるので、自動でsubject { delete :destroy }が呼び出されるようになっている。
そして、beforeを使えば、requestの中身を編集する事ができる。
今回はtokenをInvalid_tokenを入れておく事で、認証ができていないuserを作り上げる。
もちろん認証エラーが出るので、それを期待するテスト。

これでテストを実行して、成功することを確認する。

spec/controllers/access_tokens_controller_spec.rb
    context 'when valid request' do
      let(:user) { create :user }
      let(:access_token) { user.create_access_token }

      before { request.headers['authorization'] = "Bearer #{access_token.token}" }

      it 'should return 204 status code' do
        subject
        expect(response).to have_http_status(:no_content)
      end

      it 'should remove the proper access token' do
        expect{ subject }.to change{ AccessToken.count }.by(-1)
      end
    end

次に、when valid requestのテストを書いていく。
正しいリクエストを送るためにはまず、headers['authorization']にトークンを入れて、アクセス権限を渡す必要がある。
Bearerとは無記名認証のことで、今回はこれを使う。

テストではAccessTokenモデルがデータベースから一つ減っていることを期待している。

では正しくテストが失敗することを確認する。
ここで、正しく失敗することを確認するとtypoが見つかる事が多い。

expected the response to have status code :no_content (204) but it was :forbidden (403)

テストを実行するとこのようなメッセージが表示される。

forbiddenが返って来ているのは、destroyアクションで常にエラーを返すように記述をしているから。

なので実際にdestroyアクションを実装していく。

app/controllers/access_tokens_controller.rb
  def destroy
    raise AuthorizationError
  end

まずこのdestroyでしたいことはrequestを送って来たuserのaccess_tokenをdestroyすること。なので以下のように記述する。

app/controllers/access_tokens_controller.rb
  def destroy
    raise AuthorizationError unless current_user

    current_user.access_token.destroy
  end

current_userは現在ログインしているuserのことを指す。
current_userをどのようにして、持ってくるかを考える。

current_userはrequestから一気に取得する事ができない。しかし、request.authorizationとすると、さっきテストで送ったBearer xxxxxxxxxxxxxxxxxxxxx

というようなtokenを取得する事ができる。
なのでそのtokenを使って、current_userを取得していく。

app/controllers/access_tokens_controller.rb
  def destroy
    provided_token = request.authorization&.gsub(/\ABearer\s/, '')
    access_token = AccessToken.find_by(token: provided_token)
    current_user = access_token&.user

    raise AuthorizationError unless current_user

    current_user.access_token.destroy
  end

まず、request.authorizationでtokenを取得し、データベースでそのtokenを検索するために、gsubメソッドを使って、正規表現で切り取りをしている。tokenの数字の部分だけが取り出せたら、それでAccessToken.find_byで検索をかけて、取り出している。
そして、そのaccess_token.userとすれば、requestを送って来たuserを取り出す事ができる。そして、そのtokenをdestroy
すればログアウトが完了する。

&.の記述はボッチ演算子と言って、nilが帰って来て、undifind methodというふうになるかもしれない事があらかじめわかっているメソッドに対してつけておくと、nilの場合にエラーが出ずにそのままnilを返り値として返してくれるので、エラーが出ない。というもの。今回は、requestでInvalid_tokenが混ざっている場合があるので、その場合はnilが返ってしまうので、ボッチ演算子を使わないとエラーが出る。

これでテストを実行して、テストが全て通ることを確認する。

次にこのコードをリファクタリングしていく。

app/controllers/access_tokens_controller.rb
   def destroy
-    provided_token = request.authorization&.gsub(/\ABearer\s/, '')
-    access_token = AccessToken.find_by(token: provided_token)
-    current_user = access_token&.user
-
-    raise AuthorizationError unless current_user

    current_user.access_token.destroy
  end

まず、このように記述を切り取る。そして、その記述をapplication_controller.rbに移していく。なぜうつすかというと、まさにこのrequestを受けとり、current_userを生成するロジックはどのコントローラーでも使いたい記述だから。

app/controllers/application_controller.rb
  private

  def authorize!
    raise AuthorizationError unless current_user
  end

  def access_token
    provided_token = request.authorization&.gsub(/\ABearer\s/, '')
    @access_token = AccessToken.find_by(token: provided_token)
  end

  def current_user
    @current_user = access_token&.user
  end

そして、privateしたにこのようにメソッドを記述していく。
authorize!メソッドはcurrent_userが入っていない時に401エラーを出す。
access_tokenメソッドで正しいaccess_tokenを取り出し、
current_userメソッドで、そのtokenのuserを取り出している。
ここでaccess_tokenとcurrent_userを分けているのは、それぞれの役割をはっきりとさせ責任の分離を行うため。

そして、最後にその定義したauthorize!メソッドを常に呼び出せるように記述していく。

app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  before_action :authorize!, only: :destroy

before_actionで常に呼び出している状況にしている。destroyのみの指定にしているのは、createアクションの時に呼び出してしまうと、呼び出し不可能なメソッドになってしまうため。

これらのアプローチは一般的だが、before_actionを書き忘れたり、もしくは記述があまりにも多くなってしまう。なので、skip_before_actionを使い、逆にskipするメソッドを指定しておく。基本的にauthorize!メソッドに限って言えばcreateさえskipしてしまえば良さそう。

app/controllers/application_controller.rb
  before_action :authorize!

  private

privateの上に常に呼び出す記述を追記。

app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  skip_before_action :authorize!, only: :create

before_actionとメソッドを変更しておく。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  skip_before_action :authorize!, only: [:index, :show]

そして、article_controllerも忘れずにskipさせておく。
indexとshowは認証をしていなくても行いたいから。

これでテストを実行して、リファクタリング前と同じ結果が得られるかを確認する。

$ bundle exec rspec

全てのテストを実行して、全てが緑になることを確認する。

最後に

お疲れ様でした。これで最初に目標としていたuser認証機能を実装する事ができました。これらはdeviseというgemを使えば代用してしまえるかもしれませんが、その仕組みを知っているかどうかでuser認証周りの問題への対応が変わって来ますし、理解度が全く違うと思います。token周りは非常に想像しづらい部分ですし、oauthを使う場合は、やはり、gemで全てが代用されてしまうものもありますので、仕組みがブラックボックス化されてしまいがちです。なので今回はこのようにuser認証を行いました。

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

Ruby と Python で解く AtCoder ABC138 D 隣接リスト

はじめに

AtCoder Problems の Recommendation を利用して、過去の問題を解いています。
AtCoder さん、AtCoder Problems さん、ありがとうございます。

今回のお題

AtCoder Beginner Contest D - Ki
Difficulty: 923

今回のテーマ、隣接リスト

以前解きました Ruby と Python と networkx で解く AtCoder ABC168 D 隣接リスト の応用版といった問題です。

今回は、木の根から頂点へカウンターの値が送られますますので、カウンター用の配列を準備し、根から頂点へ探索を行うことによりカウンターの値の受け渡しを行います。

Ruby

ruby.rb
n, q = gets.split.map(&:to_i)
a = Array.new(n){[]}
n.pred.times do
    x, y = gets.split.map(&:to_i)
    a[x - 1] << y - 1
    a[y - 1] << x - 1
end
p = Array.new(n){0}
q.times do |i|
  x, y = gets.split.map(&:to_i)
  p[x - 1] += y
end
c = Array.new(n){0}
c[0] = 1
que = []
que << 0
while que.size > 0
  e = que.shift
  a[e].each do |i|
    next if c[i] > 0
    c[i] = 1
    p[i] += p[e]
    que << i
  end
end
puts p
index.rb
    p[i] += p[e]

ここでカウンターの値の受け渡しを行っています。

Python

python.py
from sys import stdin

def main():
    from collections import deque
    input = stdin.readline
    n, q = map(int, input().split())
    a = [[] for _ in range(n)]
    for i in range(n - 1):
        x, y = map(int, input().split())
        a[x - 1].append(y - 1)
        a[y - 1].append(x - 1)
    p = [0] * n
    for i in range(q):
        x, y = map(int, input().split())
        p[x - 1] += y
    c = [0] * n
    c[0] = 1
    que = deque([])
    que.append(0)
    while len(que) > 0:
        e = que.popleft()
        for i in a[e]:
            if c[i] > 0:
                continue
            c[i] = 1
            p[i] += p[e]
            que.append(i)
    for i in range(n):
        print(p[i])
main()
stdin.py
from sys import stdin
    input = stdin.readline

stdinを使用すると実行時間がかなり速くなります。

Ruby Python Python (stdin) PyPy PyPy (stdin)
コード長 (Byte) 442 681 736 681 736
実行時間 (ms) 802 1734 1044 1959 864
メモリ (KB) 25468 53008 53040 105164 91852

まとめ

  • ABC 138 D を解いた
  • Ruby に詳しくなった
  • Python に詳しくなった
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

マークアップ共通化:アクション毎にurlパスの変更

概要

前回の記事で、マークアップの共通化を掲載しました
コチラ↓↓
https://qiita.com/kazuko___3o3___/items/019174474f4f258ba19b

実は、この記事がメインでございましたっ:triumph:

newアクション、editアクション毎に指定したいURLが異なり、最初はif文でコードを書くもエラー続きでした:cold_sweat:
その解消法を備忘録として残します!

事象

以前、【form_forが自動的に生成してくれるパスは複数形のみ】という記事を書かせていただきました(https://qiita.com/kazuko___3o3___/items/cf8e6966772d629d5927

そのため、HTMLは下記のように記載されています。

_form.html.haml
#省略
.new_display
  = form_for @task, url: group_tasks_path do |f|
#省略

【group_tasks_path】はnewアクションの時のみに有効で、editアクションではエラーになってしまいます:scream:

解決方法

共通ビューに記載されているform_forの箇所を別ファイルに記載します:writing_hand:

before

new.html.haml/edit.html.haml
= render "form"

after

new.html.haml
= form_for @task, url: group_tasks_path do |f| 
  = render partial: "form", locals: {f: f} |= render "new_main"
edit.html.haml
= form_for @task, url: group_task_path do |f| 
  = render partial: "form", locals: {f: f} |= render "new_main"

上記内容に変更することで、newアクション時には新規登録画面、editアクション時には編集画面(入力されている内容も反映)されるようになり、create、updateもバッチリでした:laughing:

参考

https://qiita.com/seiya1121/items/fba02afcd8d54f1628ba

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

プロを目指す人のためのRuby 4章

配列

配列を使った多重代入
  • divmodメソッド:割り算の商と余りを配列として格納

14.divmod(3) #=> [4, 2]

  • 多重代入で別々の変数として受け取る

quotient, remainder = 14.divmod(3) #=> quotient=4, remainder=2

ブロック

  • Rubyではfor文を使わない
  • 取り出した要素をどう扱うかは要件で変わってくるのでブロック内で記述
配列の要素を削除する条件を自由に指定する
  • deleteメソッド
a = [1, 2, 3, 1, 2, 3]
a.delete(2)
a #=> [1, 3, 1, 3]
  • delete_ifメソッド(今回は奇数を削除したい場合)
    • odd?は奇数の場合にtrue返す
a = [1, 2, 3, 1, 2, 3]
a.delete_if do |n|
  n.odd?
end
a #=> [2, 2]
ブロック引数とブロックないの変数
  • ブロック引数を使わない場合はブロック自体を省略できる
numbers.each do
  sum += 1
end
  • 名前の重複により他の変数やメソッドが参照できなくなることをシャドーイングという
do ... endと{ }
  • do .. endを使う代わりに{ }で囲んでもブロックを作れる
  • 開業を含むブロックを書く時はdo ... end
  • 1行でコンパクトに描きたい時は{ }

ブロックを使う配列のメソッド

map(エイリアスメソッドはcollect)
  • 空の配列を用意して他の配列をループ処理した結果を空の配列に詰め込んでいくような処理の大半はmapメソッドに置き換えることができる
select(エイリアスメソッドはfind_all)とrejectメソッド

selectメソッド

numbers = [1 ,2 ,3 ,4 ,5 ,6]
even_numbers = numbers.select { |n| n.even? }
even_numbers #=> [2, 4, 6]

rejectメソッド
- selectメソッドの反対で、ブロック内の戻り値が真になった要素を除外した配列を返す

find(エイリアスメソッドはreduce)
  • ブロックの戻り値が真になった最初の要素を返す
inject(エイリアスメソッドはreduce)
  • たたみ込み演算を行うメソッド(p97参照)
&とシンボルを使ってもっと簡潔に書く
# このコードは
[1, 2, 3, 4, 5, 6].select { |n| n.odd? } #=> [1, 3, 5]
# こう置き換えられる
[1, 2, 3, 4, 5, 6].select(&:odd?) #=> [1, 3, 5]

- ブロックを渡す代わりに&:メソッド名
- 条件が揃ったときに使うことができる
1. ブロック引数が1個だけである
2. ブロックの中で呼び出すメソッドには引数がない
3. ブロックの中では、ブロック引数に対してメソッドを1回呼び出す以外の処理がない

範囲(Range)

最初の値..最後の値(最後の値を含む)
最初の値...最後の値(最後の値を含まない)

  • 範囲オブジェクトはRangeクラスのオブジェクト

(1..5).class #=> Range

n以上m以下、n以上m未満の判定をする
  • n以上m以下、n以上m未満の判定をしたい場合は不等号よりも範囲オブジェクトの方がシンプルになる
# 不等号を使う場合
def liquid?(temperature)
# 0度以上100度未満であれば液体と判定したい
  0 <= temperature && temperature < 100
end

# 範囲オブジェクトを使う場合
def liquid?(temperature)
  (0...100).include?(temperature)
end
case文で使う
# 年齢に応じて料金を判定するメソッド
def charge(age)
  case age
  when 0..5
    0
  when 6..12
    300
  when 13..18
    600
  else
    1000
  end
end
値が連続する配列を作る

(1..5).to_a #=> [1, 2, 3, 4, 5]
(a..e).to_a #=> [a, b, c, d, e]

  • [ ]の中に*と範囲オブジェクトを書いても同じように配列を作ることができる(splat展開)

[*1..5] #=> [1, 2, 3, 4, 5]

繰り返し処理を行う
# 範囲オブジェクトを配列に変換してから繰り返し処理を行う
numbers = (1..4).to_a
sum = 0
numbers.each { |n| sum += n }
sum #=> 10
  • stepメソッドを使うと値を増やす間隔を指定できる
numbers = []
#1から10まで2つ飛ばしで繰り返し処理を行う
(1..10).step(2){ |n| numbers << n }
numbers #=> [1, 3, 5, 7, 9]
  • TDD(テスト駆動開発)が向いている時
    • プログラムのインプットとアウトプットが」明確である
    • テストコードの書き方が最初からイメージできる

rjustメソッド:右寄せ
- 第一引数は桁数、第二引数は埋めたい文字列
'0'.rjust(5) #=> " 0"
'0'.rjust(5, '0') #=> "00000"
'0'.rjust(5, '_') #=> "____0"

hexメソッド(Stringクラス)
- 16進数の文字列を10進数の整数に変換

'00'.hex #=> 0
'ff'.hex #=> 255

scanメソッド:正規表現にマッチした文字列を返す

配列についてもっと詳しく

様々な要素の取得方法
  • 添字の位置と」取得する長さを指定

配列[位置, 取得する長さ]

a = []1, 2, 3, 4, 5]
a[1, 3] #=> [2, 3, 4]
  • value_at:取得したい要素のインデックスを複数指定
a = [1, 2, 3, 4, 5]
a.value_at(0, 2, 4) #=> [1, 3, 5]
  • last(配列の最後)やfirst(配列の最初)メソッドがある

  • pushメソッド:<<との違いは複数の値を追加できる

a = []
a.push(1, 2) #=> [1, 2]
配列の連結
  • concatメソッド(破壊的) or +演算子(非破壊的)
  • 破壊的メソッドは他の箇所に影響を及ぼす可能性があるので、基本は+演算子を使う
a = [1]
b = [2, 3}

a.concat(b) #=> [1, 2, 3]

a #=> [1, 2, 3]
b #=> [2, 3]
a = [1]
b = [2, 3}

a + b #=> [1, 2, 3]

a #=> [1]
b #=> [2, 3]
配列の和集合、差集合、積集合
  • 和集合:|
  • 差集合:-
  • 積集合:&
  • 全て非破壊的
a = [1, 2, 3]
b = [3, 4, 5]

a | b #=> [1, 2, 3, 4, 5]
a - b #=> [1, 2]
a & b #=> [3]
  • 配列よりも効率的に集合を扱えるSetクラスがある
多重代入で残りの全要素を配列として受け取る
  • 左辺に*を付けると残りの全要素を配列して受け取る
a, *b = 100, 200, 300
a #=> 100
f #=> [200, 300]
1つの配列を複数の引数として展開する
  • 配列を展開して「複数の引数」として渡したい時は配列の前に*を置く(splat展開)
a = [1]
b = [2, 3]
a.push(b) #=> [1, [2, 3]]
a = [1]
b = [2, 3]
a.push(*b) #=> [1, 2, 3]
メソッドの可変長引数
  • 可変長引数:個数に制限のない引数
  • メソッドで可変長引数を使いたい場合は引数名の前に*を付ける
def メソッド名(引数1, 引数2, *可変長引数)
  # メソッドの処理
end
%記法で文字列の配列を簡潔に作る
  • 文字列は%記法の%w or %Wで作成
  • カンマではなくスペースや改行が要素の区切り文字
  • 文字列を' 'や" "で囲む必要がない
# []で文字列の配列を作成
['apple', 'melon', 'orange'] #=> ["apple", "melon", "orange"]

# %wで文字列の配列を作成(!で囲む場合)
%w! apple melon orange ! #=> ["apple", "melon", "orange"]

# %Wで文字列の配列を作成(丸カッコで囲む場合)
%w(apple melon orange) #=> ["apple", "melon", "orange"]
  • 値にスペースを含めたい場合は\でエスケープ
%w(big\ apple small\ melon orange) #=> ["big apple", "small melon", "orange"]
  • 式展開や改行文字(\n)、タブ文字(\t)などを含める場合は。%W(大文字)を使う
prefix = 'This is'
%W(#{prefix}\ an\ apple small\n melon orange)
#=> ["This is an apple", "small\nmelon", orange]
文字列を配列に変更する
  • charsメソッド:文字列中の1文字1文字を配列の要素に分解
'Ruby'.chars #=> ["R", "u", "b", "y"]
  • splitメソッド:引数で渡した区切り文字で文字列を配列の分割
'Ruby, Java, Perl, PHP'.split(',') #=> ["Ruby", "Java", "Perl", "PHP"]
配列に初期値を設定する
a = Array.new(5)
a #=> [nil, nil, nil, nil, nil]
a = Array.new(5, 0)
a #=> [0, 0, 0, 0, 0]
  • Array.newはブロックを使って初期値を設定できる
# 要素数が10で1,2,3,1,2,3...と繰り返す配列
a = Array.new(10) { |n| n % 3 + 1 }
a #=> [1, 2, 3, 1, 2, 3, 1, 2, 3, 1]
配列に初期値を設定する場合の注意点
  • 第2引数で初期値を渡す場合は1つのオブジェクトに結びついている
a = Array.new(5, 'default')
a #=> ["default", "default", "default", "default", "default"]

str = a[0]
str.upcase!
str #=> "DEFAULT"

a #=> ["DEFAULT", "DEFAULT", "DEFAULT", "DEFAULT", "DEFAULT"]
  • ブロックで初期値を渡すようにすると異なるオブジェクトが作成される
a = Array.new(5){ 'dafault' }
a #=> ["default", "default", "default", "default", "default"]

str = a[0]
str.upcase!
str #=> "DEFAULT"

a #=> ["DEFAULT", "default", "default", "default", "default"]
ミュータブル(変更可能な)、イミュータブル(不変の)

イミュータブルなオブジェクトでは破壊的な変更ができない(ex.強制的に負の値に変更する)
- 数値(IntegerクラスやFloatクラス)
- シンボル(Symbolクラス)
- true/false(TrueClassクラスとFalseClassクラス)
- nil(NilClassクラス)

ミュータブルなオブジェクトはfreezeメソッドを使って変更不可にすることができる

ブロックについてもっと詳しく

添字付きの繰り返し処理
  • 繰り返し処理をしつつ、処理している要素の添字も取得したい場合はeach_with_indexメソッド
  • ブロック引数の第2引数に添字を渡す
fruits = ['apple', 'orange', 'melon']
fruits.each_with_index { |fruit, i| puts "#{i}: #{fruit}" }
#=> 0: apple
#   1: orange
#   2: melon
with_indexメソッドを使った添字付きの繰り返し処理
  •  with_indexメソッドはEnumeratorクラスのインスタンスメソッド
  •  eachやmap、delete_ifメソッドなど繰り返し処理を行うメソッドの大半はブロックを省略して呼び出すとEnumeratorオブジェクトを返す
  •  そのため、with_indexメソッドはあたかも様々な繰り返し処理用のメソッドと組み合わせて実行できるように見える
添字を0以外の数値から開始させる
  • with_indexメソッドに引数を渡す
  • each_with_indexメソッドに引数は渡せないため、each.with_index(1)の形で呼び出す
配列がブロック引数に渡される場合
  • 配列の配列に対して繰り返し処理を実行するとブロック引数に配列が返ってくる
dimensions = [
  #[縦, 横]
  [10, 20],
  [39, 40],
  [50, 60]
]

#面積の計算結果を格納する配列
areas = []

#ブロック引数が1個であればブロック引数の値が配列になる
dimensions.each do |dimension|
  length = dimension[0]
  width = dimension[1]
  areas << length * width
end
areas #=> [200, 1200, 3000]
  • ブロック引数を要素の数にすると縦と横の値を別々に受け取ることができ、上記よりもシンプルに書ける
dimensions = [
  #[縦, 横]
  [10, 20],
  [39, 40],
  [50, 60]
]

#面積の計算結果を格納する配列
areas = []

#配列の要素分だけブロック引数を用意すると、各要素の値が別々の変数に格納される
dimensions.each do |length, width|
  areas << length * width
end
areas #=> [200, 1200, 3000]
繰り返し処理以外でも使用されるブロック
File.open("./sample.txt", "w") do |file|
  file.puts("1行目のテキストです。")
  file.puts("2行目のテキストです。")
  file.puts("3行目のテキストです。")
end
  • ファイルのような外部リソースを扱う場合にはオープンしたらクローズする処理が必要
  • RubyのFile.openメソッドはブロックと組み合わせるとクローズする処理まで行う
do...endと{ }の結合度の違い
  • do...endよりも{ }の方が結合度が高い
a = [1, 2, 3]

#ブロックを渡さない時は指定した値が見つからないとnilが返る
a.delete(100) #=> nil

#ブロックを渡すとブロックの戻り値が指定した値が見つからないときの文字列になる
a.delete(100) do
  'NG'
end
#=> "NG"
  • Rubyメソッドは引数を囲む()を省略できる
a.delete 100 do
  'NG'
end
#=> "NG"
a.delete 100 { 'NG' }
#=> SyntaxError
  • { }の結合度が強いため100(メソッド) { 'NG' }と解釈される
  • 解決するには100を( )で囲む
ブロックを使うメソッドを定義する
ブロックの後ろに別のメソッドを続けて書く
names = ['田中', '鈴木', '佐藤']
san_names = names.map { |name| "#{name}さん" } #=> ["田中さん", "鈴木さん", "佐藤さん"]
san_names.join('と') #=> "田中さんと鈴木さんと佐藤さん"
  • 変数を使わずにメソッドの戻り値に対して直接他のメソッドを呼び出していくコーディングスタイルをメソッドチェーンという
names = ['田中', '鈴木', '佐藤']
names.map { |name| "#{name}さん" }.join('と') #=> "田中さんと鈴木さんと佐藤さん"
names = ['田中', '鈴木', '佐藤']
names.map do |name|
  "#{name}さん"
end.join('と') #=> "田中さんと鈴木さんと佐藤さん"

様々な繰り返し処理

timesメソッド
  • 配列を使わず、単純にn回処理を繰り返したい時はIntegerクラスのtimesメソッド
sum = 0
5.times { |n| sum += n }
sum #=> 10
  • 不要であればブロック引数は省略可能
sum = 0
5.times { sum += 1 }
sum #=> 5
upto、downtoメソッド
  • nからmまで数値を1つずつ増やしながら何か処理をしたい時はIntegerクラスのuptoメソッド(downtoは逆)
a = []
10.upto(14) { |n| a << n }
a #=> [10, 11, 12, 13, 14]
a = []
14.downto(10) { |n| a << n }
a#=> [14, 13, 12, 11]
stepメソッド
  • nからmまで数値をx個ずつ増やしながら何かを処理したい時はNumericクラスのstepメソッド

開始値.step(上限値, 1度に増減する大きさ)

a = []
1.step(10, 2) { |n| a << n }
a #=> [1, 3, 5, 7, 9]
while文とuntil文

while 条件式
繰り返したい処理
end

a = []
while a,size < 5
  a << 1
end
a #=> [1, 1, 1, 1, 1]
  • 条件式の後ろにdoを入れると1行で書くこともできる
    ruby
    a = []
    while a.size < 5 do a << 1 end
    a #=> [1, 1, 1, 1, 1]

  • 1行で書くのであれば修飾子としてwhile文を後ろに書いた方が可読性が高い(後置if文のように)

a = []
a << 1 while a.size < 5
a #=> [1, 1, 1, 1, 1]
  • どのような条件でも1回は実行したい時はbegin ... endで囲んでからwhile
a = []

while false
  #このコードは常に条件が偽になるので実行されない
  a << 1
end
a #=> []

# begin ... endで囲むと最低1回は実行される
begin
  a << 1
end while false
a #=> [1]
  • while文の逆で条件が偽である間、処理を繰り返すuntil文

until 条件式
繰り返したい処理
end

for文
  • eachメソッドをよく使い、for文は使わない
  • for文は配列の要素を受け取る変数や、for文の中で作成したローカル変数がfor文の外でも使える

for 変数 in 配列やハッシュ
繰り返し処理
end

loopメソッド

while true
#無限ループの処理
end

  • Kernelモジュールのloopメソッド

loop do
#無限ループの処理
end

  • 無限ループから脱出する場合はbreakを使う

配列に格納した5つの数値の中からランダムに数値を選び、5が出たタイミングで脱出する無限ループ

numers = [1, 2, 3, 4, 5]
loop do
  #sampleメソッドでランダムに要素を1つ取得する
  n = numbers.sample
  puts n
  break if n == 5
end
#=> 3
#   2
#   4
#   5

while文でも書ける

while true
  n = numbers.sample
  puts n
  break if n == 5
end
#=> 4
#   1
#   5
  • loopメソッドはブロックを使うので変数nがループの外で参照できない
  • whileはループの外でもnが参照できる

繰り返し処理用の制御構造

  • break
  • next

- redo

break
  • breakはeachメソッドやwhile文、for文でも使える
  • breakに引数を渡すと(break 123)、while文やfor文の戻り値になる
  • 引数を渡さない場合の戻り値はnil
  • 繰り返し処理が入れ子になっている場合は、一番内側の繰り返し処理を脱出する
fruits = ['apple', 'melon', 'orange']
numbers = [1, 2, 3]
fruits.each do |fruit|
  #配列の数字をランダムに入れ替え、3が出たらbreakする
  numbers.shuffle.each do |n|
    puts "#{fruit}, #{n}"
    #numbersのループは脱出するが、fruitsのループは継続する
    break if n == 3
  end
end
#=> apple, 1
#   apple, 3
#   melon, 2
#   melon, 3
#   orange, 2
#   orabge, 1
#   orange, 3
throwとcatchを使った大域脱出
  • 一気に外側のループまで脱出したい時は、Kernelモジュールのthrowメソッドとcatchメソッドを使う

catch タグ do
#繰り返し処理など
throw タグ
end

  • throw、catchというキーワードは他の言語では例外処理に使われる場合があるが、Rubyは例外処理とは関係ない
  • Rubyではraiseとrescueを例外処理で使う

"orange"と3の組み合わせが出たら全ての繰り返し処理を脱出する

fruits = ['sample', 'melon', 'orange']
numbers = [1, 2, 3]
catch :done do
  fruits.shuffle.each do |fruit|
    numbers.shuffle.each do |n|
      puts "#{fruit}, #{n}"
      if fruit == 'orange' && n ==3
        #全ての繰り返し処理を脱出する
        throw :done
      end
    end
  end
end 
#=> melon, 2
#   melon, 1
#   melon, 3
#   orange, 1
#   orange, 3
  • タグには通常シンボルを使用する
  • throwとcatchのタグが一致しない場合はエラーが発生
  • throwメソッドに第2引数を渡すとcatchメソッドの戻り値になる
ret =
  catch :done do
    throw :done
  end
ret #=> nil

ret =
  catch :done do
    throw :done, 123
  end
ret #=> 123
繰り返し処理で使うbreakとreturnの違い
  • breakは繰り返し処理からの脱出
  • returnはメソッドからの脱出

配列の中からランダムに1つの偶数を選び、その数を10倍して返すメソッド
break

def calc_with_break
  numbers = [1, 2, 3, 4, 5, 6]
  target = nil
  numbers.shuffle.each do |n|
    target = n
    #breakで脱出する
    break if n.even?
  end
  target n * 10
end
calc_with_break #=> 40

return
ruby
def calc_with_break
numbers = [1, 2, 3, 4, 5, 6]
target = nil
numbers.shuffle.each do |n|
target = n
#returnで脱出する
return if n.evn?
end
target n * 10
end
calc_with_return #=> nil

  • returnが呼ばれた瞬間にメソッド全体を脱出する
  • returnには引数を渡していないので、戻り値はnilになる
next
  • 繰り返し処理を途中で中断し、稚児の繰り返し処理に進める場合

偶数であれば処理を中断して次の繰り返し処理に進む

numbers = [1, 2, 3, 4, 5]
numbers.each do |n|
  #偶数であれば中断して次の処理に進む
  next if n.even?
  puts n
end
#=> 1
#   3
#   5
  • eachメソッドの中だけではなくwhile文やfor文の中でも使える
  • 入れ子になった繰り返し処理では一番内側のループだけが中断の対象になるのはbreakと同じ
redo
  • 繰り返し処理をやり直したい場合
  • 初回からやり直すのではなく、その回の繰り返し処理の最初に戻る

3つの野菜に対して「好きですか?」と問いかけ、ランダムに「はい」または「いいえ」を答える。
ただし、「はい」と答えるまで何度も同じ質問が続く

foods = ['ピーマン', 'トマト', 'セロリ']
foods.each do |food|
  print "#{food}は好きですか? => "
  #sampleは配列からランダムに1要素を取得するメソッド
  anser = ['はい', 'いいえ'].sample
  puts answer

  #はいと答えなければもう一度聞き直す
  redo unless answer == 'はい'
end
#=> ピーマンは好きですか? => いいえ
#   ピーマンは好きですか? => いいえ
#   ピーマンは好きですか? => いいえ
#   ピーマンは好きですか? => はい
#   トマトは好きですか? => はい
#   セロリは好きですか? => いいえ
#   セロリは好きですか? => はい
  • 状況によっては永遠にやり直しが続く
  • やり直しの回数を制限することを検討
foods = ['ピーマン', 'トマト', 'セロリ']
count = 0
foods.each do |food|
  print "#{food}は好きですか? => "
  #わざと「いいえ」しか答えないようにする
  anser = 'いいえ'
  puts answer

  count += 1
  #やり直しは2回までにする
  redo if answer != 'はい' && count < 2

  #カウントをリセット
  count = 0
end
#=> ピーマンは好きですか? => いいえ
#   ピーマンは好きですか? => いいえ
#   トマトは好きですか? => いいえ
#   トマトは好きですか? => いいえ
#   セロリは好きですか? => いいえ
#   セロリは好きですか? => いいえ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

heroku上でアセットが表示されない(route error?)

勉強を始めたばかりのビギナーです(ProgateでRuby on Railsのパスを終わった程度)
頓珍漢なご質問かもしれませんが、どうかご教示ください。
かなりググったり、いろんなことを数日間したのですが、どうしても解決できないため、質問させていただきます。

<やりたいこと>

既存のアプリ(以前にある会社に作ってもらったアプリ)を放置していたので、それを改めて開発・運用できる状況にしたい。(平行して、別途トレーニングアプリを自分で勉強しながら作り、ある程度力が付いたら既存アプリにも手を入れていきたい)

既存アプリ:ruby 2.3.4 / Rails 4.2.1

<現状>

まずはCSS周りだけでも触っていければと思いましたがRubyのバージョンも古く、rbenvでインストールできなかったり、herokuのCeder14に引っかかり、pushもできませんでした。
そのため、どうせならバージョンを上げようと思い、ローカルに下記のバージョンを準備し、git cloneでダウンロードしてきたApp上で作業しています。

ruby "2.7.1" / 'rails', '6.0.3'

heroku appを別途作り、Github経由してPushするようにしています。

<困っていること>

すっとばし過ぎなのかもしれませんが、ver-upの過程でいろんなエラーと格闘しながら、なんとかherokuへPushできるところまできました(deployは一応正常終了しています)
しかし、500エラーでトップページや他のページも表示されません。
エラーページは表示されているので、どうもアセットがちゃんとLoadで来てないのかなと思いながら、グーグル先生と格闘しましたが、どれをやってもうまくいきません。

ルートエラーも出ているで、routes.rbから辿ろうとしましたが、どこを直すべきかが判断付かない状況です。
いまでも稼働しているAPPなので、基本的にはプログラムは動くものだと思いますので、バージョンアップによってどこに手を付けていいのかわからないでいます。

<Tryしたこと>
・localでプリコンパイルする
・heroku側でdb:migrate/ restartする
・12factor対応としてproduction.rbに指示された記載をする
ほかにもいろいろ試したが、もう何をやったかも忘れるくらい悩み続けています・・・。

<各種ログ>*不足分は追加投稿いたします

heroku logs

2020-05-29T12:57:42.787544+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/executor.rb:14:in `call'
2020-05-29T12:57:42.787545+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:42.787545+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/static.rb:126:in `call'
2020-05-29T12:57:42.787545+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:42.787546+00:00 app[web.1]: rack (2.2.2) lib/rack/sendfile.rb:110:in `call'
2020-05-29T12:57:42.787546+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:42.787547+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/ssl.rb:74:in `call'
2020-05-29T12:57:42.787547+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:42.787547+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/host_authorization.rb:76:in `call'
2020-05-29T12:57:42.787548+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:42.787548+00:00 app[web.1]: railties (6.0.3) lib/rails/engine.rb:527:in `call'
2020-05-29T12:57:42.787548+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:42.787549+00:00 app[web.1]: puma (4.3.3) lib/puma/configuration.rb:228:in `call'
2020-05-29T12:57:42.787549+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:682:in `handle_request'
2020-05-29T12:57:42.787549+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:472:in `process_client'
2020-05-29T12:57:42.787550+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:328:in `block in run'
2020-05-29T12:57:42.787550+00:00 app[web.1]: puma (4.3.3) lib/puma/thread_pool.rb:134:in `block in spawn_thread'
2020-05-29T12:57:43.022016+00:00 heroku[router]: at=info method=GET path="/assets/semantic-ui/icons-aadc3580d2b64ff5a7e6f1425587db4e8b033efcbf8f5c332ca52a5ed580c87c.woff2" host=sengoku-staging.herokuapp.com request_id=dc4374fd-4bd7-40ce-b1db-39fb81cc3a99 fwd="119.104.39.83" dyno=web.1 connect=0ms service=6ms status=200 bytes=56989 protocol=https
2020-05-29T12:57:43.236139+00:00 heroku[router]: at=info method=GET path="/images/loading.gif" host=sengoku-staging.herokuapp.com request_id=a3070624-974d-4297-ae58-0aa1209d4e69 fwd="119.104.39.83" dyno=web.1 connect=1ms service=13ms status=200 bytes=8671 protocol=https
2020-05-29T12:57:43.230986+00:00 heroku[router]: at=info method=GET path="/images/close.png" host=sengoku-staging.herokuapp.com request_id=b8985e00-169a-4525-b44a-19e39c461b17 fwd="119.104.39.83" dyno=web.1 connect=0ms service=7ms status=200 bytes=474 protocol=https
2020-05-29T12:57:43.278205+00:00 heroku[router]: at=info method=GET path="/images/prev.png" host=sengoku-staging.herokuapp.com request_id=18d7ab1b-e71c-4b47-aec1-3fa92e8dd334 fwd="119.104.39.83" dyno=web.1 connect=0ms service=37ms status=200 bytes=1555 protocol=https
2020-05-29T12:57:43.270806+00:00 heroku[router]: at=info method=GET path="/images/next.png" host=sengoku-staging.herokuapp.com request_id=45a5fb25-0bef-4305-839b-93d70edaccef fwd="119.104.39.83" dyno=web.1 connect=0ms service=29ms status=200 bytes=1545 protocol=https
2020-05-29T12:57:43.232284+00:00 app[web.1]: Started GET "/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js" for 119.104.39.83 at 2020-05-29 12:57:43 +0000
2020-05-29T12:57:43.263458+00:00 app[web.1]:
2020-05-29T12:57:43.263461+00:00 app[web.1]: ActionController::RoutingError (No route matches [GET] "/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js"):
2020-05-29T12:57:43.263461+00:00 app[web.1]:
2020-05-29T12:57:43.263462+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call'
2020-05-29T12:57:43.263463+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263463+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
2020-05-29T12:57:43.263463+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263464+00:00 app[web.1]: railties (6.0.3) lib/rails/rack/logger.rb:37:in `call_app'
2020-05-29T12:57:43.263464+00:00 app[web.1]: railties (6.0.3) lib/rails/rack/logger.rb:26:in `block in call'
2020-05-29T12:57:43.263465+00:00 app[web.1]: activesupport (6.0.3) lib/active_support/tagged_logging.rb:80:in `block in tagged'
2020-05-29T12:57:43.263466+00:00 app[web.1]: activesupport (6.0.3) lib/active_support/tagged_logging.rb:28:in `tagged'
2020-05-29T12:57:43.263466+00:00 app[web.1]: activesupport (6.0.3) lib/active_support/tagged_logging.rb:80:in `tagged'
2020-05-29T12:57:43.263466+00:00 app[web.1]: railties (6.0.3) lib/rails/rack/logger.rb:26:in `call'
2020-05-29T12:57:43.263467+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263467+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/remote_ip.rb:81:in `call'
2020-05-29T12:57:43.263467+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263468+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/request_id.rb:27:in `call'
2020-05-29T12:57:43.263468+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263469+00:00 app[web.1]: rack (2.2.2) lib/rack/method_override.rb:24:in `call'
2020-05-29T12:57:43.263469+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263470+00:00 app[web.1]: rack (2.2.2) lib/rack/runtime.rb:22:in `call'
2020-05-29T12:57:43.263470+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263471+00:00 app[web.1]: activesupport (6.0.3) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'
2020-05-29T12:57:43.263471+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263471+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/executor.rb:14:in `call'
2020-05-29T12:57:43.263472+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263472+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/static.rb:126:in `call'
2020-05-29T12:57:43.263472+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263473+00:00 app[web.1]: rack (2.2.2) lib/rack/sendfile.rb:110:in `call'
2020-05-29T12:57:43.263473+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263474+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/ssl.rb:74:in `call'
2020-05-29T12:57:43.263474+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263474+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/host_authorization.rb:76:in `call'
2020-05-29T12:57:43.263475+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263475+00:00 app[web.1]: railties (6.0.3) lib/rails/engine.rb:527:in `call'
2020-05-29T12:57:43.263475+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263476+00:00 app[web.1]: puma (4.3.3) lib/puma/configuration.rb:228:in `call'
2020-05-29T12:57:43.263477+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:682:in `handle_request'
2020-05-29T12:57:43.263477+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:472:in `process_client'
2020-05-29T12:57:43.263478+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:328:in `block in run'
2020-05-29T12:57:43.263478+00:00 app[web.1]: puma (4.3.3) lib/puma/thread_pool.rb:134:in `block in spawn_thread'
2020-05-29T12:57:43.267499+00:00 heroku[router]: at=info method=GET path="/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js" host=sengoku-staging.herokuapp.com request_id=007e46bd-b355-4ff8-a6d0-60996f4956ec fwd="119.104.39.83" dyno=web.1 connect=0ms service=42ms status=404 bytes=6227 protocol=https
2020-05-29T12:57:43.609702+00:00 heroku[router]: at=info method=GET path="/assets/favicon-7874bfbf965313c5bbeeb2387c52d82beeebbb7aac8ff3a49ff15d40759611f3.ico" host=sengoku-staging.herokuapp.com request_id=a7918de0-9ba2-460c-8fac-f5f006405ef1 fwd="119.104.39.83" dyno=web.1 connect=0ms service=4ms status=200 bytes=4112 protocol=https
2020-05-29T13:13:45.000000+00:00 app[api]: Build started by user murakushu@gmail.com
2020-05-29T13:17:00.415597+00:00 app[api]: Release v87 created by user murakushu@gmail.com
2020-05-29T13:17:00.415597+00:00 app[api]: Deploy e6d1c9c1 by user murakushu@gmail.com
2020-05-29T13:17:01.131438+00:00 heroku[web.1]: Restarting
2020-05-29T13:17:01.134293+00:00 heroku[web.1]: State changed from up to starting
2020-05-29T13:17:02.446819+00:00 app[web.1]: - Gracefully stopping, waiting for requests to finish
2020-05-29T13:17:02.453010+00:00 app[web.1]: === puma shutdown: 2020-05-29 13:17:02 +0000 ===
2020-05-29T13:17:02.453406+00:00 app[web.1]: - Goodbye!
2020-05-29T13:17:14.000000+00:00 app[api]: Build succeeded
2020-05-29T13:17:16.124539+00:00 app[web.1]: Puma starting in single mode...
2020-05-29T13:17:16.124562+00:00 app[web.1]: * Version 4.3.3 (ruby 2.7.1-p83), codename: Mysterious Traveller
2020-05-29T13:17:16.124563+00:00 app[web.1]: * Min threads: 5, max threads: 5
2020-05-29T13:17:16.124563+00:00 app[web.1]: * Environment: production
2020-05-29T13:17:23.919066+00:00 app[web.1]: /app/vendor/bundle/ruby/2.7.0/gems/json-1.8.6/lib/json/common.rb:155: warning: Using the last argument as keyword parameters is deprecated
2020-05-29T13:17:24.138805+00:00 app[web.1]: * Listening on tcp://0.0.0.0:54194
2020-05-29T13:17:24.138984+00:00 app[web.1]: Use Ctrl-C to stop
2020-05-29T13:17:24.718991+00:00 heroku[web.1]: State changed from starting to up
2020-05-29T13:20:02.901454+00:00 heroku[router]: at=info method=GET path="/about/howto" host=sengoku-staging.herokuapp.com request_id=93a84717-2086-48eb-aa49-f2d45cd45908 fwd="119.104.39.83" dyno=web.1 connect=1ms service=74ms status=500 bytes=6271 protocol=https
2020-05-29T13:20:03.622488+00:00 heroku[router]: at=info method=GET path="/css/error_style.css" host=sengoku-staging.herokuapp.com request_id=1ce6702c-9063-4ef5-8942-c547f6c9a8b5 fwd="119.104.39.83" dyno=web.1 connect=1ms service=91ms status=200 bytes=1655818 protocol=https
2020-05-29T13:20:03.630004+00:00 heroku[router]: at=info method=GET path="/assets/main-0a988f8ddc9cacf19b0627458380a655ffc5830bf3b2249c6aa7b73e2c685548.js" host=sengoku-staging.herokuapp.com request_id=fa30b6b2-d099-4878-aff3-f4b1bdc7815e fwd="119.104.39.83" dyno=web.1 connect=1ms service=41ms status=404 bytes=6227 protocol=https
2020-05-29T13:20:03.788990+00:00 heroku[router]: at=info method=GET path="/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js" host=sengoku-staging.herokuapp.com request_id=72329eec-799b-42c4-a597-a1e3954e24ba fwd="119.104.39.83" dyno=web.1 connect=0ms service=30ms status=404 bytes=6227 protocol=https
2020-05-29T13:20:04.430528+00:00 heroku[router]: at=info method=GET path="/assets/logo_new-8914dbb872e125dc2a6172703a6eb87f509e66e4fd21149921d2b07a1163bdec.png" host=sengoku-staging.herokuapp.com request_id=0e430825-3029-4185-96bf-d65783f817ba fwd="119.104.39.83" dyno=web.1 connect=1ms service=12ms status=200 bytes=2542 protocol=https
2020-05-29T13:20:19.430002+00:00 heroku[router]: at=info method=GET path="/assets/murakushu_top_2-14ac461ec82a828e7d2566d6f7debca5c4022bcc9a3e409d7fb797f2269eb60f.jpg" host=sengoku-staging.herokuapp.com request_id=ef65292f-b455-436c-b6b2-d2b70664601a fwd="119.104.39.83" dyno=web.1 connect=1ms service=24ms status=404 bytes=6227 protocol=https
2020-05-29T13:20:19.836373+00:00 heroku[router]: at=info method=GET path="/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js" host=sengoku-staging.herokuapp.com request_id=4bf67e9d-0cb5-48c2-bda3-1b824973e4cd fwd="119.104.39.83" dyno=web.1 connect=1ms service=35ms status=404 bytes=6227 protocol=https
2020-05-29T13:20:20.069153+00:00 heroku[router]: at=info method=GET path="/images/close.png" host=sengoku-staging.herokuapp.com request_id=fab786b9-2f53-46a4-9c1b-eebfde2e92c5 fwd="119.104.39.83" dyno=web.1 connect=1ms service=3ms status=200 bytes=474 protocol=https
2020-05-29T13:20:20.327716+00:00 heroku[router]: at=info method=GET path="/images/prev.png" host=sengoku-staging.herokuapp.com request_id=f9f92ce7-89e2-4553-a4b5-9ff03bad4afe fwd="119.104.39.83" dyno=web.1 connect=1ms service=3ms status=200 bytes=1555 protocol=https
2020-05-29T13:20:20.336519+00:00 heroku[router]: at=info method=GET path="/images/next.png" host=sengoku-staging.herokuapp.com request_id=ff09639e-7214-4131-8882-e30a9b9d7978 fwd="119.104.39.83" dyno=web.1 connect=1ms service=2ms status=200 bytes=1545 protocol=https
2020-05-29T13:20:20.333109+00:00 heroku[router]: at=info method=GET path="/images/loading.gif" host=sengoku-staging.herokuapp.com request_id=d003b234-5ddb-4b7f-beec-cd7ff3491075 fwd="119.104.39.83" dyno=web.1 connect=1ms service=2ms status=200 bytes=8671 protocol=https
2020-05-29T13:20:20.732035+00:00 heroku[router]: at=info method=GET path="/assets/semantic-ui/icons-aadc3580d2b64ff5a7e6f1425587db4e8b033efcbf8f5c332ca52a5ed580c87c.woff2" host=sengoku-staging.herokuapp.com request_id=86ebc30e-d2d4-4ddf-b7a1-8c44b33ca4ce fwd="119.104.39.83" dyno=web.1 connect=1ms service=4ms status=200 bytes=56989 protocol=https
2020-05-29T13:56:45.874523+00:00 heroku[web.1]: Idling
2020-05-29T13:56:45.887746+00:00 heroku[web.1]: State changed from up to down
2020-05-29T13:56:47.115501+00:00 app[web.1]: - Gracefully stopping, waiting for requests to finish
2020-05-29T13:56:47.117055+00:00 app[web.1]: === puma shutdown: 2020-05-29 13:56:47 +0000 ===
2020-05-29T13:56:47.117056+00:00 app[web.1]: - Goodbye!

production.rb

(抜粋)
config.serve_static_files = true
config.assets.compile = false
config.assets.digest = true

routes.rb

Rails.application.routes.draw do

  mount Ckeditor::Engine => '/ckeditor'
  get 'mypage/index'
  get 'mypage/my_article'

  resources :articles do
    get 'list'
    resources :comments, :only => [:create, :destroy]
    resources :sub_images, :only => [:create]
    get 'manage'
    post 'publish',   action: 'publish',   as: 'publish'
  end
  delete 'delete_media', to: "sub_images#delete_media"
  post 'like/:article_id' => 'likes#like', as:'like'
  delete 'unlike/:article_id' => 'likes#unlike',as:'unlike'

  get 'privacy/index'
  get 'privacy/tokusho'

  get 'term/index'

  get 'about/grade'
  get 'about/premium'
  get 'about/howto'

  get 'premium/index'
  get 'premium/confirm'
  get 'premium/thanks'

  get 'inquiry/index'
  get 'inquiry/confirm'
  get 'inquiry/thanks'

  devise_for :users, controllers: {
    sessions:            'users/sessions',
    registrations:       'users/registrations',
    passwords:           'users/passwords',
    omniauth_callbacks:  'users/omniauth_callbacks',
    confirmations:       'users/confirmations'
  }
  devise_for :admins, controllers: {
    sessions:      'admins/sessions',
    passwords:     'admins/passwords',
    registrations: 'admins/registrations'
  }
  resources :sites, :only => [:create, :new, :index]

  resources :guides, :only => [:show]

  resources :profiles do
    # collection do
    #   get 'list'
    # end

    member do
      get "articles"
    end
  end

  namespace :manage do
    get 'tops' => 'tops#index'
    get 'download' => 'tops#download'
    resources :users, :only => [:edit, :update, :destroy, :index]do
    end
    resources :articles, :only => [:edit, :update, :destroy, :index] do
    end
    resources :sites, :only => [:edit, :update, :destroy, :index] do
    end
    resources :headlines
    resources :guides
    resources :banners
  end

  root 'articles#list'

  get '/sitemap' => redirect('https://s3-ap-northeast-1.amazonaws.com/sogoo/sitemaps/sitemap.xml.gz')


  match ':controller(/:action(/:id))', via: [:get, :post, :patch ]
end

<お願い>

1.ルートエラーの解消方法、正常にアセットをロードできる方法について、お気づきの点があればご指摘いただけると幸いです。

2.もしくは、既存のアプリを別のやり方でひとまず触れるようにできる方法があれば、それもご教示願えると嬉しいです。

情報足りないことがあるかと存じますが、何卒よろしくお願い申し上げます。

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

heroku上でアセットが表示されない

勉強を始めたばかりのビギナーです(ProgateでRuby on Railsのパスを終わった程度)
頓珍漢なご質問かもしれませんが、どうかご教示ください。
かなりググったり、いろんなことを数日間したのですが、どうしても解決できないため、質問させていただきます。

<やりたいこと>

既存のアプリ(以前にある会社に作ってもらったアプリ)を放置していたので、それを改めて開発・運用できる状況にしたい。(平行して、別途トレーニングアプリを自分で勉強しながら作り、ある程度力が付いたら既存アプリにも手を入れていきたい)

既存アプリ:ruby 2.3.4 / Rails 4.2.1

<現状>

まずはCSS周りだけでも触っていければと思いましたがRubyのバージョンも古く、rbenvでインストールできなかったり、herokuのCeder14に引っかかり、pushもできませんでした。
そのため、どうせならバージョンを上げようと思い、ローカルに下記のバージョンを準備し、git cloneでダウンロードしてきたApp上で作業しています。

ruby "2.7.1" / 'rails', '6.0.3'

heroku appを別途作り、Github経由してPushするようにしています。

<困っていること>

すっとばし過ぎなのかもしれませんが、ver-upの過程でいろんなエラーと格闘しながら、なんとかherokuへPushできるところまできました(deployは一応正常終了しています)
しかし、500エラーでトップページや他のページも表示されません。
エラーページは表示されているので、どうもアセットがちゃんとLoadで来てないのかなと思いながら、グーグル先生と格闘しましたが、どれをやってもうまくいきません。

ルートエラーも出ているで、routes.rbから辿ろうとしましたが、どこを直すべきかが判断付かない状況です。
いまでも稼働しているAPPなので、基本的にはプログラムは動くものだと思いますので、バージョンアップによってどこに手を付けていいのかわからないでいます。

<Tryしたこと>
・localでプリコンパイルする
・heroku側でdb:migrate/ restartする
・12factor対応としてproduction.rbに指示された記載をする
ほかにもいろいろ試したが、もう何をやったかも忘れるくらい悩み続けています・・・。

<各種ログ>*不足分は追加投稿いたします

heroku logs

2020-05-29T12:57:42.787544+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/executor.rb:14:in `call'
2020-05-29T12:57:42.787545+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:42.787545+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/static.rb:126:in `call'
2020-05-29T12:57:42.787545+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:42.787546+00:00 app[web.1]: rack (2.2.2) lib/rack/sendfile.rb:110:in `call'
2020-05-29T12:57:42.787546+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:42.787547+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/ssl.rb:74:in `call'
2020-05-29T12:57:42.787547+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:42.787547+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/host_authorization.rb:76:in `call'
2020-05-29T12:57:42.787548+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:42.787548+00:00 app[web.1]: railties (6.0.3) lib/rails/engine.rb:527:in `call'
2020-05-29T12:57:42.787548+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:42.787549+00:00 app[web.1]: puma (4.3.3) lib/puma/configuration.rb:228:in `call'
2020-05-29T12:57:42.787549+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:682:in `handle_request'
2020-05-29T12:57:42.787549+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:472:in `process_client'
2020-05-29T12:57:42.787550+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:328:in `block in run'
2020-05-29T12:57:42.787550+00:00 app[web.1]: puma (4.3.3) lib/puma/thread_pool.rb:134:in `block in spawn_thread'
2020-05-29T12:57:43.022016+00:00 heroku[router]: at=info method=GET path="/assets/semantic-ui/icons-aadc3580d2b64ff5a7e6f1425587db4e8b033efcbf8f5c332ca52a5ed580c87c.woff2" host=sengoku-staging.herokuapp.com request_id=dc4374fd-4bd7-40ce-b1db-39fb81cc3a99 fwd="119.104.39.83" dyno=web.1 connect=0ms service=6ms status=200 bytes=56989 protocol=https
2020-05-29T12:57:43.236139+00:00 heroku[router]: at=info method=GET path="/images/loading.gif" host=sengoku-staging.herokuapp.com request_id=a3070624-974d-4297-ae58-0aa1209d4e69 fwd="119.104.39.83" dyno=web.1 connect=1ms service=13ms status=200 bytes=8671 protocol=https
2020-05-29T12:57:43.230986+00:00 heroku[router]: at=info method=GET path="/images/close.png" host=sengoku-staging.herokuapp.com request_id=b8985e00-169a-4525-b44a-19e39c461b17 fwd="119.104.39.83" dyno=web.1 connect=0ms service=7ms status=200 bytes=474 protocol=https
2020-05-29T12:57:43.278205+00:00 heroku[router]: at=info method=GET path="/images/prev.png" host=sengoku-staging.herokuapp.com request_id=18d7ab1b-e71c-4b47-aec1-3fa92e8dd334 fwd="119.104.39.83" dyno=web.1 connect=0ms service=37ms status=200 bytes=1555 protocol=https
2020-05-29T12:57:43.270806+00:00 heroku[router]: at=info method=GET path="/images/next.png" host=sengoku-staging.herokuapp.com request_id=45a5fb25-0bef-4305-839b-93d70edaccef fwd="119.104.39.83" dyno=web.1 connect=0ms service=29ms status=200 bytes=1545 protocol=https
2020-05-29T12:57:43.232284+00:00 app[web.1]: Started GET "/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js" for 119.104.39.83 at 2020-05-29 12:57:43 +0000
2020-05-29T12:57:43.263458+00:00 app[web.1]:
2020-05-29T12:57:43.263461+00:00 app[web.1]: ActionController::RoutingError (No route matches [GET] "/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js"):
2020-05-29T12:57:43.263461+00:00 app[web.1]:
2020-05-29T12:57:43.263462+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call'
2020-05-29T12:57:43.263463+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263463+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
2020-05-29T12:57:43.263463+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263464+00:00 app[web.1]: railties (6.0.3) lib/rails/rack/logger.rb:37:in `call_app'
2020-05-29T12:57:43.263464+00:00 app[web.1]: railties (6.0.3) lib/rails/rack/logger.rb:26:in `block in call'
2020-05-29T12:57:43.263465+00:00 app[web.1]: activesupport (6.0.3) lib/active_support/tagged_logging.rb:80:in `block in tagged'
2020-05-29T12:57:43.263466+00:00 app[web.1]: activesupport (6.0.3) lib/active_support/tagged_logging.rb:28:in `tagged'
2020-05-29T12:57:43.263466+00:00 app[web.1]: activesupport (6.0.3) lib/active_support/tagged_logging.rb:80:in `tagged'
2020-05-29T12:57:43.263466+00:00 app[web.1]: railties (6.0.3) lib/rails/rack/logger.rb:26:in `call'
2020-05-29T12:57:43.263467+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263467+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/remote_ip.rb:81:in `call'
2020-05-29T12:57:43.263467+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263468+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/request_id.rb:27:in `call'
2020-05-29T12:57:43.263468+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263469+00:00 app[web.1]: rack (2.2.2) lib/rack/method_override.rb:24:in `call'
2020-05-29T12:57:43.263469+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263470+00:00 app[web.1]: rack (2.2.2) lib/rack/runtime.rb:22:in `call'
2020-05-29T12:57:43.263470+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263471+00:00 app[web.1]: activesupport (6.0.3) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'
2020-05-29T12:57:43.263471+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263471+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/executor.rb:14:in `call'
2020-05-29T12:57:43.263472+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263472+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/static.rb:126:in `call'
2020-05-29T12:57:43.263472+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263473+00:00 app[web.1]: rack (2.2.2) lib/rack/sendfile.rb:110:in `call'
2020-05-29T12:57:43.263473+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263474+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/ssl.rb:74:in `call'
2020-05-29T12:57:43.263474+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263474+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/host_authorization.rb:76:in `call'
2020-05-29T12:57:43.263475+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263475+00:00 app[web.1]: railties (6.0.3) lib/rails/engine.rb:527:in `call'
2020-05-29T12:57:43.263475+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263476+00:00 app[web.1]: puma (4.3.3) lib/puma/configuration.rb:228:in `call'
2020-05-29T12:57:43.263477+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:682:in `handle_request'
2020-05-29T12:57:43.263477+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:472:in `process_client'
2020-05-29T12:57:43.263478+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:328:in `block in run'
2020-05-29T12:57:43.263478+00:00 app[web.1]: puma (4.3.3) lib/puma/thread_pool.rb:134:in `block in spawn_thread'
2020-05-29T12:57:43.267499+00:00 heroku[router]: at=info method=GET path="/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js" host=sengoku-staging.herokuapp.com request_id=007e46bd-b355-4ff8-a6d0-60996f4956ec fwd="119.104.39.83" dyno=web.1 connect=0ms service=42ms status=404 bytes=6227 protocol=https
2020-05-29T12:57:43.609702+00:00 heroku[router]: at=info method=GET path="/assets/favicon-7874bfbf965313c5bbeeb2387c52d82beeebbb7aac8ff3a49ff15d40759611f3.ico" host=sengoku-staging.herokuapp.com request_id=a7918de0-9ba2-460c-8fac-f5f006405ef1 fwd="119.104.39.83" dyno=web.1 connect=0ms service=4ms status=200 bytes=4112 protocol=https
2020-05-29T13:13:45.000000+00:00 app[api]: Build started by user murakushu@gmail.com
2020-05-29T13:17:00.415597+00:00 app[api]: Release v87 created by user murakushu@gmail.com
2020-05-29T13:17:00.415597+00:00 app[api]: Deploy e6d1c9c1 by user murakushu@gmail.com
2020-05-29T13:17:01.131438+00:00 heroku[web.1]: Restarting
2020-05-29T13:17:01.134293+00:00 heroku[web.1]: State changed from up to starting
2020-05-29T13:17:02.446819+00:00 app[web.1]: - Gracefully stopping, waiting for requests to finish
2020-05-29T13:17:02.453010+00:00 app[web.1]: === puma shutdown: 2020-05-29 13:17:02 +0000 ===
2020-05-29T13:17:02.453406+00:00 app[web.1]: - Goodbye!
2020-05-29T13:17:14.000000+00:00 app[api]: Build succeeded
2020-05-29T13:17:16.124539+00:00 app[web.1]: Puma starting in single mode...
2020-05-29T13:17:16.124562+00:00 app[web.1]: * Version 4.3.3 (ruby 2.7.1-p83), codename: Mysterious Traveller
2020-05-29T13:17:16.124563+00:00 app[web.1]: * Min threads: 5, max threads: 5
2020-05-29T13:17:16.124563+00:00 app[web.1]: * Environment: production
2020-05-29T13:17:23.919066+00:00 app[web.1]: /app/vendor/bundle/ruby/2.7.0/gems/json-1.8.6/lib/json/common.rb:155: warning: Using the last argument as keyword parameters is deprecated
2020-05-29T13:17:24.138805+00:00 app[web.1]: * Listening on tcp://0.0.0.0:54194
2020-05-29T13:17:24.138984+00:00 app[web.1]: Use Ctrl-C to stop
2020-05-29T13:17:24.718991+00:00 heroku[web.1]: State changed from starting to up
2020-05-29T13:20:02.901454+00:00 heroku[router]: at=info method=GET path="/about/howto" host=sengoku-staging.herokuapp.com request_id=93a84717-2086-48eb-aa49-f2d45cd45908 fwd="119.104.39.83" dyno=web.1 connect=1ms service=74ms status=500 bytes=6271 protocol=https
2020-05-29T13:20:03.622488+00:00 heroku[router]: at=info method=GET path="/css/error_style.css" host=sengoku-staging.herokuapp.com request_id=1ce6702c-9063-4ef5-8942-c547f6c9a8b5 fwd="119.104.39.83" dyno=web.1 connect=1ms service=91ms status=200 bytes=1655818 protocol=https
2020-05-29T13:20:03.630004+00:00 heroku[router]: at=info method=GET path="/assets/main-0a988f8ddc9cacf19b0627458380a655ffc5830bf3b2249c6aa7b73e2c685548.js" host=sengoku-staging.herokuapp.com request_id=fa30b6b2-d099-4878-aff3-f4b1bdc7815e fwd="119.104.39.83" dyno=web.1 connect=1ms service=41ms status=404 bytes=6227 protocol=https
2020-05-29T13:20:03.788990+00:00 heroku[router]: at=info method=GET path="/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js" host=sengoku-staging.herokuapp.com request_id=72329eec-799b-42c4-a597-a1e3954e24ba fwd="119.104.39.83" dyno=web.1 connect=0ms service=30ms status=404 bytes=6227 protocol=https
2020-05-29T13:20:04.430528+00:00 heroku[router]: at=info method=GET path="/assets/logo_new-8914dbb872e125dc2a6172703a6eb87f509e66e4fd21149921d2b07a1163bdec.png" host=sengoku-staging.herokuapp.com request_id=0e430825-3029-4185-96bf-d65783f817ba fwd="119.104.39.83" dyno=web.1 connect=1ms service=12ms status=200 bytes=2542 protocol=https
2020-05-29T13:20:19.430002+00:00 heroku[router]: at=info method=GET path="/assets/murakushu_top_2-14ac461ec82a828e7d2566d6f7debca5c4022bcc9a3e409d7fb797f2269eb60f.jpg" host=sengoku-staging.herokuapp.com request_id=ef65292f-b455-436c-b6b2-d2b70664601a fwd="119.104.39.83" dyno=web.1 connect=1ms service=24ms status=404 bytes=6227 protocol=https
2020-05-29T13:20:19.836373+00:00 heroku[router]: at=info method=GET path="/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js" host=sengoku-staging.herokuapp.com request_id=4bf67e9d-0cb5-48c2-bda3-1b824973e4cd fwd="119.104.39.83" dyno=web.1 connect=1ms service=35ms status=404 bytes=6227 protocol=https
2020-05-29T13:20:20.069153+00:00 heroku[router]: at=info method=GET path="/images/close.png" host=sengoku-staging.herokuapp.com request_id=fab786b9-2f53-46a4-9c1b-eebfde2e92c5 fwd="119.104.39.83" dyno=web.1 connect=1ms service=3ms status=200 bytes=474 protocol=https
2020-05-29T13:20:20.327716+00:00 heroku[router]: at=info method=GET path="/images/prev.png" host=sengoku-staging.herokuapp.com request_id=f9f92ce7-89e2-4553-a4b5-9ff03bad4afe fwd="119.104.39.83" dyno=web.1 connect=1ms service=3ms status=200 bytes=1555 protocol=https
2020-05-29T13:20:20.336519+00:00 heroku[router]: at=info method=GET path="/images/next.png" host=sengoku-staging.herokuapp.com request_id=ff09639e-7214-4131-8882-e30a9b9d7978 fwd="119.104.39.83" dyno=web.1 connect=1ms service=2ms status=200 bytes=1545 protocol=https
2020-05-29T13:20:20.333109+00:00 heroku[router]: at=info method=GET path="/images/loading.gif" host=sengoku-staging.herokuapp.com request_id=d003b234-5ddb-4b7f-beec-cd7ff3491075 fwd="119.104.39.83" dyno=web.1 connect=1ms service=2ms status=200 bytes=8671 protocol=https
2020-05-29T13:20:20.732035+00:00 heroku[router]: at=info method=GET path="/assets/semantic-ui/icons-aadc3580d2b64ff5a7e6f1425587db4e8b033efcbf8f5c332ca52a5ed580c87c.woff2" host=sengoku-staging.herokuapp.com request_id=86ebc30e-d2d4-4ddf-b7a1-8c44b33ca4ce fwd="119.104.39.83" dyno=web.1 connect=1ms service=4ms status=200 bytes=56989 protocol=https
2020-05-29T13:56:45.874523+00:00 heroku[web.1]: Idling
2020-05-29T13:56:45.887746+00:00 heroku[web.1]: State changed from up to down
2020-05-29T13:56:47.115501+00:00 app[web.1]: - Gracefully stopping, waiting for requests to finish
2020-05-29T13:56:47.117055+00:00 app[web.1]: === puma shutdown: 2020-05-29 13:56:47 +0000 ===
2020-05-29T13:56:47.117056+00:00 app[web.1]: - Goodbye!

production.rb

(抜粋)
config.serve_static_files = true
config.assets.compile = false
config.assets.digest = true

routes.rb

Rails.application.routes.draw do

  mount Ckeditor::Engine => '/ckeditor'
  get 'mypage/index'
  get 'mypage/my_article'

  resources :articles do
    get 'list'
    resources :comments, :only => [:create, :destroy]
    resources :sub_images, :only => [:create]
    get 'manage'
    post 'publish',   action: 'publish',   as: 'publish'
  end
  delete 'delete_media', to: "sub_images#delete_media"
  post 'like/:article_id' => 'likes#like', as:'like'
  delete 'unlike/:article_id' => 'likes#unlike',as:'unlike'

  get 'privacy/index'
  get 'privacy/tokusho'

  get 'term/index'

  get 'about/grade'
  get 'about/premium'
  get 'about/howto'

  get 'premium/index'
  get 'premium/confirm'
  get 'premium/thanks'

  get 'inquiry/index'
  get 'inquiry/confirm'
  get 'inquiry/thanks'

  devise_for :users, controllers: {
    sessions:            'users/sessions',
    registrations:       'users/registrations',
    passwords:           'users/passwords',
    omniauth_callbacks:  'users/omniauth_callbacks',
    confirmations:       'users/confirmations'
  }
  devise_for :admins, controllers: {
    sessions:      'admins/sessions',
    passwords:     'admins/passwords',
    registrations: 'admins/registrations'
  }
  resources :sites, :only => [:create, :new, :index]

  resources :guides, :only => [:show]

  resources :profiles do
    # collection do
    #   get 'list'
    # end

    member do
      get "articles"
    end
  end

  namespace :manage do
    get 'tops' => 'tops#index'
    get 'download' => 'tops#download'
    resources :users, :only => [:edit, :update, :destroy, :index]do
    end
    resources :articles, :only => [:edit, :update, :destroy, :index] do
    end
    resources :sites, :only => [:edit, :update, :destroy, :index] do
    end
    resources :headlines
    resources :guides
    resources :banners
  end

  root 'articles#list'

  get '/sitemap' => redirect('https://s3-ap-northeast-1.amazonaws.com/sogoo/sitemaps/sitemap.xml.gz')


  match ':controller(/:action(/:id))', via: [:get, :post, :patch ]
end

<お願い>

1.ルートエラーの解消方法、正常にアセットをロードできる方法について、お気づきの点があればご指摘いただけると幸いです。

2.もしくは、既存のアプリを別のやり方でひとまず触れるようにできる方法があれば、それもご教示願えると嬉しいです。

情報足りないことがあるかと存じますが、何卒よろしくお願い申し上げます。

(投稿後の進捗)

2020/5/30 18:24
production.rbの記述を”config.public_file_server.enabled = true” と変更するとルーティングエラーはログ上からは消えましたが、500エラーは解消されず。(heroku logを見ると特にエラーらしきものが無い状態です)

heroku logs

2020-05-29T12:57:43.263470+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263471+00:00 app[web.1]: activesupport (6.0.3) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'
2020-05-29T12:57:43.263471+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263471+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/executor.rb:14:in `call'
2020-05-29T12:57:43.263472+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263472+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/static.rb:126:in `call'
2020-05-29T12:57:43.263472+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263473+00:00 app[web.1]: rack (2.2.2) lib/rack/sendfile.rb:110:in `call'
2020-05-29T12:57:43.263473+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263474+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/ssl.rb:74:in `call'
2020-05-29T12:57:43.263474+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263474+00:00 app[web.1]: actionpack (6.0.3) lib/action_dispatch/middleware/host_authorization.rb:76:in `call'
2020-05-29T12:57:43.263475+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263475+00:00 app[web.1]: railties (6.0.3) lib/rails/engine.rb:527:in `call'
2020-05-29T12:57:43.263475+00:00 app[web.1]: newrelic_rpm (6.10.0.364) lib/new_relic/agent/instrumentation/middleware_tracing.rb:99:in `call'
2020-05-29T12:57:43.263476+00:00 app[web.1]: puma (4.3.3) lib/puma/configuration.rb:228:in `call'
2020-05-29T12:57:43.263477+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:682:in `handle_request'
2020-05-29T12:57:43.263477+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:472:in `process_client'
2020-05-29T12:57:43.263478+00:00 app[web.1]: puma (4.3.3) lib/puma/server.rb:328:in `block in run'
2020-05-29T12:57:43.263478+00:00 app[web.1]: puma (4.3.3) lib/puma/thread_pool.rb:134:in `block in spawn_thread'
2020-05-29T12:57:43.267499+00:00 heroku[router]: at=info method=GET path="/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js" host=sengoku-staging.herokuapp.com request_id=007e46bd-b355-4ff8-a6d0-60996f4956ec fwd="119.104.39.83" dyno=web.1 connect=0ms service=42ms status=404 bytes=6227 protocol=https
2020-05-29T12:57:43.609702+00:00 heroku[router]: at=info method=GET path="/assets/favicon-7874bfbf965313c5bbeeb2387c52d82beeebbb7aac8ff3a49ff15d40759611f3.ico" host=sengoku-staging.herokuapp.com request_id=a7918de0-9ba2-460c-8fac-f5f006405ef1 fwd="119.104.39.83" dyno=web.1 connect=0ms service=4ms status=200 bytes=4112 protocol=https
2020-05-29T13:13:45.000000+00:00 app[api]: Build started by user murakushu@gmail.com
2020-05-29T13:17:00.415597+00:00 app[api]: Release v87 created by user murakushu@gmail.com
2020-05-29T13:17:00.415597+00:00 app[api]: Deploy e6d1c9c1 by user murakushu@gmail.com
2020-05-29T13:17:01.131438+00:00 heroku[web.1]: Restarting
2020-05-29T13:17:01.134293+00:00 heroku[web.1]: State changed from up to starting
2020-05-29T13:17:02.446819+00:00 app[web.1]: - Gracefully stopping, waiting for requests to finish
2020-05-29T13:17:02.453010+00:00 app[web.1]: === puma shutdown: 2020-05-29 13:17:02 +0000 ===
2020-05-29T13:17:02.453406+00:00 app[web.1]: - Goodbye!
2020-05-29T13:17:14.000000+00:00 app[api]: Build succeeded
2020-05-29T13:17:16.124539+00:00 app[web.1]: Puma starting in single mode...
2020-05-29T13:17:16.124562+00:00 app[web.1]: * Version 4.3.3 (ruby 2.7.1-p83), codename: Mysterious Traveller
2020-05-29T13:17:16.124563+00:00 app[web.1]: * Min threads: 5, max threads: 5
2020-05-29T13:17:16.124563+00:00 app[web.1]: * Environment: production
2020-05-29T13:17:23.919066+00:00 app[web.1]: /app/vendor/bundle/ruby/2.7.0/gems/json-1.8.6/lib/json/common.rb:155: warning: Using the last argument as keyword parameters is deprecated
2020-05-29T13:17:24.138805+00:00 app[web.1]: * Listening on tcp://0.0.0.0:54194
2020-05-29T13:17:24.138984+00:00 app[web.1]: Use Ctrl-C to stop
2020-05-29T13:17:24.718991+00:00 heroku[web.1]: State changed from starting to up
2020-05-29T13:20:02.901454+00:00 heroku[router]: at=info method=GET path="/about/howto" host=sengoku-staging.herokuapp.com request_id=93a84717-2086-48eb-aa49-f2d45cd45908 fwd="119.104.39.83" dyno=web.1 connect=1ms service=74ms status=500 bytes=6271 protocol=https
2020-05-29T13:20:03.622488+00:00 heroku[router]: at=info method=GET path="/css/error_style.css" host=sengoku-staging.herokuapp.com request_id=1ce6702c-9063-4ef5-8942-c547f6c9a8b5 fwd="119.104.39.83" dyno=web.1 connect=1ms service=91ms status=200 bytes=1655818 protocol=https
2020-05-29T13:20:03.630004+00:00 heroku[router]: at=info method=GET path="/assets/main-0a988f8ddc9cacf19b0627458380a655ffc5830bf3b2249c6aa7b73e2c685548.js" host=sengoku-staging.herokuapp.com request_id=fa30b6b2-d099-4878-aff3-f4b1bdc7815e fwd="119.104.39.83" dyno=web.1 connect=1ms service=41ms status=404 bytes=6227 protocol=https
2020-05-29T13:20:03.788990+00:00 heroku[router]: at=info method=GET path="/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js" host=sengoku-staging.herokuapp.com request_id=72329eec-799b-42c4-a597-a1e3954e24ba fwd="119.104.39.83" dyno=web.1 connect=0ms service=30ms status=404 bytes=6227 protocol=https
2020-05-29T13:20:04.430528+00:00 heroku[router]: at=info method=GET path="/assets/logo_new-8914dbb872e125dc2a6172703a6eb87f509e66e4fd21149921d2b07a1163bdec.png" host=sengoku-staging.herokuapp.com request_id=0e430825-3029-4185-96bf-d65783f817ba fwd="119.104.39.83" dyno=web.1 connect=1ms service=12ms status=200 bytes=2542 protocol=https
2020-05-29T13:20:19.430002+00:00 heroku[router]: at=info method=GET path="/assets/murakushu_top_2-14ac461ec82a828e7d2566d6f7debca5c4022bcc9a3e409d7fb797f2269eb60f.jpg" host=sengoku-staging.herokuapp.com request_id=ef65292f-b455-436c-b6b2-d2b70664601a fwd="119.104.39.83" dyno=web.1 connect=1ms service=24ms status=404 bytes=6227 protocol=https
2020-05-29T13:20:19.836373+00:00 heroku[router]: at=info method=GET path="/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js" host=sengoku-staging.herokuapp.com request_id=4bf67e9d-0cb5-48c2-bda3-1b824973e4cd fwd="119.104.39.83" dyno=web.1 connect=1ms service=35ms status=404 bytes=6227 protocol=https
2020-05-29T13:20:20.069153+00:00 heroku[router]: at=info method=GET path="/images/close.png" host=sengoku-staging.herokuapp.com request_id=fab786b9-2f53-46a4-9c1b-eebfde2e92c5 fwd="119.104.39.83" dyno=web.1 connect=1ms service=3ms status=200 bytes=474 protocol=https
2020-05-29T13:20:20.327716+00:00 heroku[router]: at=info method=GET path="/images/prev.png" host=sengoku-staging.herokuapp.com request_id=f9f92ce7-89e2-4553-a4b5-9ff03bad4afe fwd="119.104.39.83" dyno=web.1 connect=1ms service=3ms status=200 bytes=1555 protocol=https
2020-05-29T13:20:20.336519+00:00 heroku[router]: at=info method=GET path="/images/next.png" host=sengoku-staging.herokuapp.com request_id=ff09639e-7214-4131-8882-e30a9b9d7978 fwd="119.104.39.83" dyno=web.1 connect=1ms service=2ms status=200 bytes=1545 protocol=https
2020-05-29T13:20:20.333109+00:00 heroku[router]: at=info method=GET path="/images/loading.gif" host=sengoku-staging.herokuapp.com request_id=d003b234-5ddb-4b7f-beec-cd7ff3491075 fwd="119.104.39.83" dyno=web.1 connect=1ms service=2ms status=200 bytes=8671 protocol=https
2020-05-29T13:20:20.732035+00:00 heroku[router]: at=info method=GET path="/assets/semantic-ui/icons-aadc3580d2b64ff5a7e6f1425587db4e8b033efcbf8f5c332ca52a5ed580c87c.woff2" host=sengoku-staging.herokuapp.com request_id=86ebc30e-d2d4-4ddf-b7a1-8c44b33ca4ce fwd="119.104.39.83" dyno=web.1 connect=1ms service=4ms status=200 bytes=56989 protocol=https
2020-05-29T13:56:45.874523+00:00 heroku[web.1]: Idling
2020-05-29T13:56:45.887746+00:00 heroku[web.1]: State changed from up to down
2020-05-29T13:56:47.115501+00:00 app[web.1]: - Gracefully stopping, waiting for requests to finish
2020-05-29T13:56:47.117055+00:00 app[web.1]: === puma shutdown: 2020-05-29 13:56:47 +0000 ===
2020-05-29T13:56:47.117056+00:00 app[web.1]: - Goodbye!
2020-05-30T05:12:56.871635+00:00 heroku[web.1]: Unidling
2020-05-30T05:12:56.887081+00:00 heroku[web.1]: State changed from down to starting
2020-05-30T05:13:09.270184+00:00 app[web.1]: Puma starting in single mode...
2020-05-30T05:13:09.270208+00:00 app[web.1]: * Version 4.3.3 (ruby 2.7.1-p83), codename: Mysterious Traveller
2020-05-30T05:13:09.270209+00:00 app[web.1]: * Min threads: 5, max threads: 5
2020-05-30T05:13:09.270210+00:00 app[web.1]: * Environment: production
2020-05-30T05:13:15.619674+00:00 app[web.1]: /app/vendor/bundle/ruby/2.7.0/gems/json-1.8.6/lib/json/common.rb:155: warning: Using the last argument as keyword parameters is deprecated
2020-05-30T05:13:15.826609+00:00 app[web.1]: * Listening on tcp://0.0.0.0:45890
2020-05-30T05:13:15.826710+00:00 app[web.1]: Use Ctrl-C to stop
2020-05-30T05:13:16.040038+00:00 heroku[web.1]: State changed from starting to up
2020-05-30T05:13:16.965247+00:00 heroku[router]: at=info method=GET path="/robots.txt" host=sengoku-staging.herokuapp.com request_id=efb70e3d-19ed-41cf-a238-e56fd7e90ef0 fwd="88.99.195.201" dyno=web.1 connect=1ms service=25ms status=301 bytes=171 protocol=http
2020-05-30T05:13:17.513895+00:00 heroku[router]: at=info method=GET path="/robots.txt" host=sengoku-staging.herokuapp.com request_id=06a7db95-5a72-4fa3-97fe-ad78a72df3b0 fwd="88.99.195.201" dyno=web.1 connect=0ms service=7ms status=200 bytes=341 protocol=https
2020-05-30T05:13:17.712859+00:00 heroku[router]: at=info method=GET path="/" host=sengoku-staging.herokuapp.com request_id=a95eaaf9-346e-45ff-8946-070daadee1b6 fwd="88.99.195.201" dyno=web.1 connect=0ms service=2ms status=301 bytes=161 protocol=http
2020-05-30T05:13:18.299895+00:00 heroku[router]: at=info method=GET path="/" host=sengoku-staging.herokuapp.com request_id=64abbe61-94c2-4b8e-8ade-5fad991a9d84 fwd="94.130.167.95" dyno=web.1 connect=0ms service=9ms status=500 bytes=6271 protocol=https
2020-05-30T05:46:31.889981+00:00 heroku[web.1]: Idling
2020-05-30T05:46:31.892926+00:00 heroku[web.1]: State changed from up to down
2020-05-30T05:46:35.456931+00:00 app[web.1]: - Gracefully stopping, waiting for requests to finish
2020-05-30T05:46:35.458005+00:00 app[web.1]: === puma shutdown: 2020-05-30 05:46:35 +0000 ===
2020-05-30T05:46:35.458008+00:00 app[web.1]: - Goodbye!
2020-05-30T09:14:21.000000+00:00 app[api]: Build started by user murakushu@gmail.com
2020-05-30T09:17:28.932910+00:00 heroku[web.1]: State changed from down to starting
2020-05-30T09:17:28.759072+00:00 app[api]: Deploy 758ecc25 by user murakushu@gmail.com
2020-05-30T09:17:28.759072+00:00 app[api]: Release v88 created by user murakushu@gmail.com
2020-05-30T09:17:44.417636+00:00 app[web.1]: Puma starting in single mode...
2020-05-30T09:17:44.417744+00:00 app[web.1]: * Version 4.3.3 (ruby 2.7.1-p83), codename: Mysterious Traveller
2020-05-30T09:17:44.417745+00:00 app[web.1]: * Min threads: 5, max threads: 5
2020-05-30T09:17:44.417746+00:00 app[web.1]: * Environment: production
2020-05-30T09:17:44.000000+00:00 app[api]: Build succeeded
2020-05-30T09:17:51.647639+00:00 app[web.1]: /app/vendor/bundle/ruby/2.7.0/gems/json-1.8.6/lib/json/common.rb:155: warning: Using the last argument as keyword parameters is deprecated
2020-05-30T09:17:52.120700+00:00 heroku[web.1]: State changed from starting to up
2020-05-30T09:17:51.906127+00:00 app[web.1]: * Listening on tcp://0.0.0.0:43015
2020-05-30T09:17:51.906223+00:00 app[web.1]: Use Ctrl-C to stop
2020-05-30T09:17:52.843656+00:00 heroku[router]: at=info method=GET path="/" host=sengoku-staging.herokuapp.com request_id=c42fdb3c-7803-4286-9699-e1dc2aca4145 fwd="119.104.33.16" dyno=web.1 connect=0ms service=38ms status=500 bytes=6271 protocol=https
2020-05-30T09:17:53.174298+00:00 heroku[router]: at=info method=GET path="/assets/logo_new-8914dbb872e125dc2a6172703a6eb87f509e66e4fd21149921d2b07a1163bdec.png" host=sengoku-staging.herokuapp.com request_id=7696f6cd-602b-4366-9818-58e80851dad1 fwd="119.104.33.16" dyno=web.1 connect=0ms service=6ms status=200 bytes=2542 protocol=https
2020-05-30T09:17:53.440387+00:00 heroku[router]: at=info method=GET path="/css/error_style.css" host=sengoku-staging.herokuapp.com request_id=b5d9e381-b063-42cd-abde-aea2c1f9de25 fwd="119.104.33.16" dyno=web.1 connect=1ms service=35ms status=200 bytes=1655818 protocol=https
2020-05-30T09:17:53.660926+00:00 heroku[router]: at=info method=GET path="/assets/main-0a988f8ddc9cacf19b0627458380a655ffc5830bf3b2249c6aa7b73e2c685548.js" host=sengoku-staging.herokuapp.com request_id=26ecbc8f-81de-4088-a747-80195ee844e8 fwd="119.104.33.16" dyno=web.1 connect=0ms service=27ms status=404 bytes=6227 protocol=https
2020-05-30T09:17:53.661559+00:00 heroku[router]: at=info method=GET path="/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js" host=sengoku-staging.herokuapp.com request_id=5bd31680-1a3c-4128-8cd5-092c8985d65e fwd="119.104.33.16" dyno=web.1 connect=1ms service=24ms status=404 bytes=6227 protocol=https
2020-05-30T09:18:06.620404+00:00 heroku[router]: at=info method=GET path="/assets/jquery.narrows-ff44ce77e70cdab26e8833daeab09f751a16662d35eb8354d9d8de313d1570b9.js" host=sengoku-staging.herokuapp.com request_id=a6549d03-cff3-4c43-aba2-8898455f63f0 fwd="119.104.33.16" dyno=web.1 connect=1ms service=12ms status=404 bytes=6227 protocol=https
2020-05-30T09:18:06.841521+00:00 heroku[router]: at=info method=GET path="/assets/semantic-ui/icons-aadc3580d2b64ff5a7e6f1425587db4e8b033efcbf8f5c332ca52a5ed580c87c.woff2" host=sengoku-staging.herokuapp.com request_id=f837dd13-d62b-4b78-a0f9-6f89118731b1 fwd="119.104.33.16" dyno=web.1 connect=0ms service=3ms status=200 bytes=56989 protocol=https
2020-05-30T09:18:07.077611+00:00 heroku[router]: at=info method=GET path="/assets/murakushu_top_2-14ac461ec82a828e7d2566d6f7debca5c4022bcc9a3e409d7fb797f2269eb60f.jpg" host=sengoku-staging.herokuapp.com request_id=b420780a-bf77-4253-9b42-fa612d09eb6a fwd="119.104.33.16" dyno=web.1 connect=1ms service=17ms status=404 bytes=6227 protocol=https
2020-05-30T09:18:07.271364+00:00 heroku[router]: at=info method=GET path="/images/next.png" host=sengoku-staging.herokuapp.com request_id=a253791d-fcb3-41ed-8274-34324f718090 fwd="119.104.33.16" dyno=web.1 connect=0ms service=5ms status=200 bytes=1545 protocol=https
2020-05-30T09:18:07.263543+00:00 heroku[router]: at=info method=GET path="/images/loading.gif" host=sengoku-staging.herokuapp.com request_id=8e8ff87b-eae7-45a8-a80d-6d50fa4a2242 fwd="119.104.33.16" dyno=web.1 connect=0ms service=3ms status=200 bytes=8671 protocol=https
2020-05-30T09:18:07.265476+00:00 heroku[router]: at=info method=GET path="/images/close.png" host=sengoku-staging.herokuapp.com request_id=ff314517-2c9c-4792-b8bd-c2725607ddfe fwd="119.104.33.16" dyno=web.1 connect=1ms service=2ms status=200 bytes=474 protocol=https
2020-05-30T09:18:07.271679+00:00 heroku[router]: at=info method=GET path="/images/prev.png" host=sengoku-staging.herokuapp.com request_id=9b7f35c2-1c9a-4916-b5d9-ced2d0ba95d0 fwd="119.104.33.16" dyno=web.1 connect=0ms service=4ms status=200 bytes=1555 protocol=https
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ABC086C - Traveling の嘘解法

問題

https://atcoder.jp/contests/abs/tasks/arc089_a (300 点)

概要
2次元平面上で旅行をしようとしています。旅行プランでは時刻 0点(0,0)を出発し、1以上N以下の各 i に対し、時刻Ti点(Xi,Yi)を訪れる予定です。
時刻Tに点(X,Y)にいる時、時刻T+1には(X+1,Y),(X-1,Y),(X,Y+1),(X,Y-1)のいずれかに存在することができます(つまりその場にとどまれないということです)
旅行プランが実行可能かどうか判定してください。

入力

N
T1 X1 Y1
T2 X2 Y2
:
Tn Xn Yn

#例題
2
3 1 2
6 1 1

解法

以下の嘘解法を多く見かけました。

n = gets.strip.to_i
n.times do
  t, x, y = gets.strip.split.map(&:to_i)
  if t < x + y || (t + x + y) % 2 != 0
    puts 'No'
    exit
  end
end
puts 'Yes'

例題でいうと

2
3 1 2
6 1 1

に対して点(0,0)からの最短到着時刻(最短距離)と、時刻Tと点(X,Y)の偶奇が一致するかを見ています。

それぞれ見てみましょう。まずは、

最短到着時刻(最短距離)

if t < x + y

これは左上の点(0,0)、右下の点(1,1)とした場合

(x,y) (0,0) (0,1)
- (1, 0) (1, 1)

点(0,0)*から上下右左のいずれかに移動できる場合の最短到着時刻はX+Yで求めることができます。((0,0)→(1,1)には最短2歩でいける)

(x+y) 0 1
- 1 2

よって点(0,0)から出発し、到着したい時刻(T) < 最短到着時刻(X+Y)の場合は、必要時刻が足らずに到着できません。次に、

偶奇の一致

(t + x + y) % 2

これはT(予定時刻)X+Y(最短到着時刻)の偶数と奇数が一致するかを見ています。X+Y(最短到着時刻)の座標が偶数の場合、その座標にはどんなに遠回りしても偶数になる性質があります。(奇数の場合も一緒)
よってT(予定時刻)X+Y(最短到着時刻)の偶奇が一致する場合、上記のコードは必ず0を返します。

なぜ嘘解法なのか?

確かにACの判定が出ているコードですが、
以下の様な入力例を考えてみます。

テストケース

2
4 2 2
6 0 0
(x+y),(T) 0,(6) 1 2
- 1 2 3
- 2 3 4,(4)

といった感じの平面座標です。
一つ目の入力(4 2 2)は予定時刻と最短到着時刻が一致しているため、行けることがわかります。

では二つ目の入力(6 0 0)はどうでしょうか...

  if t < x + y || (t + x + y) % 2 != 0
    puts 'No'
    exit
  end

この条件式は左右の条件式のどちらかがtrueだった場合は、旅行不可のため"No"を出力します。
よって二つ目の入力はどちらの条件式もFalse返すため、Yesを出力します。(つまり旅行可能)

しかし、点(2,2)(一つ目の入力地点)から(0,0)までの最短距離を出してみましょう。

(最短距離),(T) 4,(6) 3 2
- 3 2 1
- 2 1 0(4)

6 - 4 (ふたつ目の入力時刻 - ひとつ目の入力時刻) = 2(歩分)しか旅人は移動できないはずですが、上記の表だと2歩で到着できるのは点(1,1)までの様です。
しかし上記のコードでは、"Yes"と判定されてしまいます。

しい解法

正しくは、以下の条件を確認する必要があります。

  1. TとX+Yの偶奇の一致
  2. (現在の地点)から(入力された目標地点(X,Y))までの最短到着時刻移動可能な距離圏内(入力時刻-現時刻)である

上記1つは実行済みなので、条件2のコードを追加します。
ちなみに最短到着時刻(最短距離)で記載したコードは、上記の条件2と同義なので添削します。

これをコードで表現すると以下のようになりました。

n = gets.strip.to_i

t_old = x_old = y_old = 0
n.times do
  t, x, y = gets.strip.split.map(&:to_i)

#移動可能時刻と最短到着時刻を計算
  abel = (t_old - t).abs
  dist = (x_old - x).abs + (y_old - y).abs

  if (t + x + y) % 2 != 0 || abel < dist #<-条件追加
    puts 'No'
    exit
  end

#今回入力された値を記録
  t_old = t
  x_old = x
  y_old = y
end
puts 'Yes'

解説

  1. 入力された(t,x,y)をそれぞれ(t_old,x_old,y_old)として保存し、次のループで使います。
  2. 次の回で入力された値と保存された値を比べて旅人の移動可能時刻と、前地点から今地点の最短距離を出します。
    • 移動可能距離 = (今回の到着時刻 - 前回の到着時刻)の絶対値
    • 最短到着時刻 = (前回のX地点-今回のX地点)の絶対値 + (前回のY地点-今回のY地点)の絶対値
  3. 最短到着時刻(dist)が移動可能距離(abel)圏内か判定します。←3つめの追加条件

これを提出すると正しくACとなります。
先ほどの1: 4 2 2 , 2: 6 0 0のケースにおいても正しく "No"と判定します。

以上でこの問題の嘘解法についての投稿は終了します。

ここからは記事を書いた感想文です。(笑)

初投稿

初めまして!今回がQiita初投稿となりました!:pray:
僕自身は、競技プログラミングを始めてようやくAOJ(AizuOnlineJadge)のITP1を解き切れたレベルです:sweat_smile:
しかし競技プログラミングは非常に楽しいので、今後も粘り強く続けていきます:relaxed:

実はこの嘘解法との出会いは、解き方が分からずに他の人のコードを閲覧したことがきっかけでした。(笑)
コード長の項目が1番少ない物のが1番スマートでしょ!!という安易な理由でたどり着いたのが今回の嘘解法です。
閲覧したコードを解釈したときには、こんなに簡単なコードで成り立っているのか!全くもってスマートだぜ!!と唸りました。
しかし何か引っかかるものがありました。他の方が書いたコードに疑いを持ったのは初めてでした。

そうして見つけた嘘解法...とても興奮しました...!
その勢いのまま投稿をした次第です(笑)
今見返しても、とてもスマートではない記事とコードですが、この楽しみを味わえたことには、非常に満足しています:star2:

そして、
もっともっとスマートなコード・記事を書ける猛者はたくさんいると思うので、
この記事を閲覧してくれた際は、是非ともご教示いただければ幸いです。
よろしくお願いします:grin:

追記

  • コードと記事の内容を修正しました(@superrino130さん、ご教示いただきありがとうございました!)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby と Python で解く AtCoder CODE FESTIVAL 2016 qual C B 優先度付きキュー

はじめに

AtCoder Problems の Recommendation を利用して、過去の問題を解いています。
AtCoder さん、AtCoder Problems さん、ありがとうございます。

今回のお題

AtCoder CODE FESTIVAL 2016 qual C B - K個のケーキ
Difficulty: 905

今回のテーマ、優先度付きキュー

以前解きました Ruby と Python と Java で解く AtCoder ABC141 D 優先度付きキュー の応用版といった問題です。

与えられた配列の最大値と2番目の最大値をキューから取り出し、1を減算してキューに戻します。これを繰返しキューの中身が1つ以下になったところで、ループを抜け出します。

尚、数学的解法もあります。

Ruby 数学的解法

ruby.rb
k = gets.to_i
a = gets.split.map(&:to_i).max
puts [0, 2 * a - k - 1].max

最大値に注目して解きますが、スッキリ感は凄いですね。

Ruby Heap

ruby.rb
class Heap
  attr_reader :size
  def initialize(up: false)
    @up = up
    @heap = []
    @size = 0
  end
  def sum
    x = 0
    @size.times do |i|
      x += @heap[i]
    end
    x
  end
  def push(n)
    n = -n if @up
    i = @size
    while i > 0
      pid = (i - 1) / 2
      break if n >= @heap[pid]
      @heap[i] = @heap[pid]
      i = pid
    end
    @heap[i] = n
    @size += 1
  end
  def pop
    return nil if @size == 0
    top = @heap[0]
    @size -= 1
    n = @heap[@size]
    i = 0
    while i * 2 + 1 < @size
      cid1 = i * 2 + 1
      cid2 = cid1 + 1
      if cid2 < @size && @heap[cid2] < @heap[cid1]
        cid1 = cid2
      end
      break if @heap[cid1] >= n
      @heap[i] = @heap[cid1]
      i = cid1
    end
    @heap[i] = n
    if @up
      -top
    else
      top
    end
  end
end

gets
a = gets.split.map(&:to_i)
h = Heap.new(up: true)
a.each do |x|
  h.push(x)
end
while h.size > 1
  u = h.pop
  v = h.pop
  h.push(u - 1) if u - 1 > 0
  h.push(v - 1) if v - 1 > 0
end
if h.size == 0
  puts "0"
else
  puts h.pop - 1
end
up.rb
  def initialize(up: false)
    @up = up

前回 のクラスを改良して、最大値と最小値をフラグで切り替えられるようにしました。

heap.rb
while h.size > 1
  u = h.pop
  v = h.pop
  h.push(u - 1) if u - 1 > 0
  h.push(v - 1) if v - 1 > 0
end

2つ取り出して減算した後、1以上であればキューに戻します。

Python 数学的解法

python.py
from sys import stdin

def main():
    input = stdin.readline
    k, _ = map(int, input().split())
    a = max(map(int, input().split()))
    print(max(0, 2 * a - k - 1))
main()

Python heapq

python.py
from sys import stdin

def main():
    import heapq
    input = stdin.readline
    input()
    a = [-1 * int(i) for i in input().split()]
    heapq.heapify(a)
    while len(a) > 1:
        u = heapq.heappop(a)
        v = heapq.heappop(a)
        if u + 1 < 0:
            heapq.heappush(a, u + 1)
        if v + 1 < 0:
            heapq.heappush(a, v + 1)
    if len(a) == 0:
        print(0)
    else:
        print(abs(a[0] + 1))
main()

演算が複雑な場合、符号変換用の関数を準備した方がいいと思われます。

Ruby 数学的解法 Ruby Heap Python 数学的解法 Python heapq
コード長 (Byte) 76 1120 184 458
実行時間 (ms) 7 26 17 22
メモリ (KB) 1788 1788 2940 3064

まとめ

  • AtCoder CODE FESTIVAL 2016 qual C B を解いた
  • Ruby に詳しくなった
  • Python に詳しくなった

参照したサイト
[競プロ用]優先度付きキュー(heapq)まとめ

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

Ruby ボーナスドリンク問題 解いてみた(解答例あり)

はじめに

『プロを目指す人のためのRuby入門』通称チェリー本を学習後のプログラミング初心者です。
インプットしたものを手を動かして実践してみたいなと思ったら、作者の記事を見つけました。
「アウトプットのネタに困ったらこれ!?Ruby初心者向けのプログラミング問題を集めてみた(全10問)」

この3つ目の問題を解いてみました。

他の問題はこちら
一問目:カレンダー作成問題(たのしいRuby 練習問題)
二問目:カラオケマシン作成問題
三問目:ビンゴカード作成問題

問題

詳細はここから

ある駄菓子屋で飲み物を買うと、空き瓶3本で新しい飲み物を1本プレゼントしてくれる。
最初に購入した飲み物の本数から、トータルで飲める本数を算出するプログラムを作成せよ。
また、最初に100本購入した場合、トータルで何本飲めるか。

備考
この問題は小学校3年生の算数の問題をベースとしている。

購入した本数 飲める本数
0 0
1 1
3 4
11 16
100 (プログラムで算出する)

解答例

こんな感じになりました

class BonusDrink
  def self.total_count_for(amount)
    if amount.zero?
      0
    elsif amount.odd?
      3 * amount / 2
    elsif amount.even?
      3 * (amount - 1) / 2 + 1
    end
  end
end

テストコードはこちらです。

require_relative '../Bonus_drink/drink'

RSpec.describe BonusDrink do
  it "total_count_for" do
    expect(BonusDrink.total_count_for(0)).to eq 0
    expect(BonusDrink.total_count_for(1)).to eq 1
    expect(BonusDrink.total_count_for(3)).to eq 4
    expect(BonusDrink.total_count_for(11)).to eq 16
  end
end

解説

プログラムとして成立してるとは思うのですが、プログラミングの問題の趣旨としてこの解き方で良かったのかなという感じは若干あります。
(ほとんど紙とペンを動かして算数的に解きました。)

まず

問題にもある通り、「空き瓶3本と新たな1本を交換」してもらえます。
交換して受け取った新たな瓶1本も交換対象になるのがこの問題の肝です。

ですので

購入した本数:3 新たに交換した本数:1  合計で飲んだ本数:4

となりますが、受け取った瓶を追加して3本に達するとさらにもう一本交換できるのです。
例えば、

購入した本数:5  新たに交換した本数:1  さらに追加で交換した本:1 合計で飲んだ本数:7

ということになります。
したがって購入した本数が増えれば増えるほど、追加の交換が何度も繰り返されます。

これをプログラミングで表現して合計の瓶の本数・交換済みの本数などを繰り返し代入して最大の合計本数に近づけていく問題なのかと思ったのですが、思い付かず、諦めました。
何か解答例あればコメントで教えていただけますと幸いです。

ここから解説

ではどう解いたかというと、購入した本数と飲める本数をとりあえず少し書き出してみました。

購入した本数 飲める本数
0 0
1 1
2 2
3 4
4 5
5 7
6 8
7 10
8 11
9 13
10 14
11 16

そうすると、「購入した本数nが奇数の時、(n * 3)の値はnの時の飲める本数と(n+1)の時の飲める本数の合計と一致する」ということがわかりました。
ちなみに0の時は成立しません。
(他にも色々法則性はありますが、今回はこの方程式を使います。)
具体的には、

購入した本数 飲める本数
5 7
6 8
5 * 3 = 15
7 + 8 = 15

なりますね!
つまり「nを3倍にしたものを2で割ると飲める本数を求められる」ことがわかりました。
これをプログラミングに直すと、

# 奇数の時
3 * amount / 2
# 偶数の時は1つ前の奇数から求めました
3 * (amount - 1) / 2 + 1

以上の内容をまとめたものが、上記の解答例です。

さいごに

こういう法則が成り立つな、というのは分かったのですが、「なんでこうなるのか」がいまいちわからないです。
わかりやすく説明していただける方いましたらお恥ずかしながらご教授お願いしたいです。

よろしくお願いします!

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

【grover】RailsでPDFの生成をする【2020年度版】

Rails で PDF を生成する際はどうされていますか?
少し調べると wicked_pdf や prawn などの gem が出てきます。

今回は grover という Puppeteer/Chromium を使って HTML から PDF や画像を生成する gem の存在を知り、試してみたのでメモを残します。

手順

puppeteer をインストール

npm install puppeteer

Gemfile に記述

gem 'grover'

config/initializers/grover.rb に以下を追加。

# frozen_string_literal: true

Grover.configure do |config|
  config.options = {
    format: 'A4',
    margin: {
      top: '5px',
      bottom: '10cm'
    },
    viewport: {
      width: 640,
      height: 480
    },
    prefer_css_page_size: true,
    emulate_media: 'screen',
    cache: false,
    timeout: 0, # Timeout in ms. A value of `0` means 'no timeout'
    launch_args: ['--font-render-hinting=medium', '--lang=ja'], # 日本語表示のため --lang=ja を追加
    wait_until: 'domcontentloaded'
  }
end

controllers/api/sample_controller.rb に以下を記述。
ルーティングの記述も忘れずに。

# frozen_string_literal: true

module Api
  class SampleController < ApplicationController
    include ActionController::MimeResponds # API モードで respond_to を使うために必要

    def show
      controller = ActionController::Base.new
      html = controller.render_to_string(template: 'api/hoges/show', layout: 'pdf')
      pdf = Grover.new(html).to_pdf
      respond_to do |format|
        format.html
        format.pdf do
          send_data(pdf, filename: 'your_filename.pdf', type: 'application/pdf')
        end
      end
    end

pdf 生成用の layout ファイルを作成

views/layouts/pdf.html.erb

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <style>
    </style>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

api/sample/show.html.erb

<p>請求書</p>

<style>
p { font-size: 20px; }
</style>

結果

スクリーンショット 2020-05-30 11.48.01.png

最後に

簡単に pdf を生成することができました。
処理時間も気にならないレベルです。 puppeteer を入れればフォントの設定など不要そうなので良さそうです。

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

Ruby on Rails 日英対応 i18n

環境

  • rails 5.2
  • ruby 2.5

方針

  • できるだけ手軽に日英対応

概要

  1. 言語切り替えボタンをヘッダーに設置
  2. コントローラー内でセッション変数session[:locale]などにロケール情報を格納
  3. before_action で各ページ読み込み前に言語設定
  4. ymlファイルへ訳文記入
  5. Viewなどで使用

実装

1. 言語切り替えボタンをヘッダーに設置

簡単に切り替えができるようにヘッダーにボタン設置しました。

application.html.erb
ヘッダーに以下を設置
<% if session[:locale] == "en" || session[:locale].blank? %>
   <%= link_to "/home/change_language/ja" do %>
    日本語
   <% end %>
<% elsif session[:locale] == "ja" %>
   <%= link_to "/home/change_language/en" do %>
    English
   <% end %>
<% end %>

今回は日英のみ対応なので

  • session[:locale]に"ja"が入っている場合は「English」
  • session[:locale]に"en"が入っている場合は「日本語」

を表示するようにしました。

2. コントローラー内でsession変数格納

routes.rb
get "/home/change_language/:language" => "home#change_language"
home_controller.rb
  def change_language
    session[:locale] = params[:language]
    redirect_back(fallback_location: "/")
  end

3. 各ページ読み込み前にbefore_actionでI18nにlocale設定

before_action に設定することでどのページにアクセスしても先に言語情報が設定されます。
※ユーザーのログイン状態のようなにします

application.rb
before_action :set_locale

private
  def set_locale
    if %w(ja en).include?(session[:locale])
      I18n.locale = session[:locale]
    end
  end

4. app>config>locales>

  • ja.yml
  • en.yml

に分けて訳文を記入しました。
例)英語はjaをenに変えるだけです。

ja.yml
ja:
  #共通で使う言葉
  dictionary:
    words:
      hello: "こんにちは"
  activerecord:
    #Model名
    model:
      user: "ユーザー"
    #カラム名
    attributes:
      user_name: "ユーザーネーム"
      password: "パスワード"
  #viewごとに設定
  users:
    show:
      profile: "プロフィール"
      picture: "写真"
    form:
      required_field: "必須項目"

5. Viewなどで使用する

show.html.erb
 <%= t("dictionary.words.hello") %>  => こんにちは
 <%= t("activerecord.attributes.user_name") %>  => ユーザーネーム

# Viewがusers show の場合以下のように省略して記載可能
 <%= t(".profile") %>  => プロフィール
 <%= t(".picture") %>  => 写真

# カラム名を設定しておけばフォームヘルパーでも言語により自動で切り替え
<%= form_with scope: @user ,,,,,%>
<%= form.label :user_name %> =>ユーザーネーム
<% end %>
_form.html.erb
# パーシャルはアンダーバーを省き利用Viewがusers _form.html.erbの場合
 <%= t(".required_field") %>  => 必須項目

# このパーシャルがusers show.html.erbで利用されていたとしても
 <%= t(".picture") %>  => × これは利用できません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】request specでsigned/encrypted cookiesにアクセスする

環境

  • Rails 5.2.3
  • RSpec 3.9.0

問題/やりたいこと

そのままでは request spec 内で signed cookie 及び encrypted cookie にアクセス出来ません。
具合的には、以下のようなエラーが出ます。

     NoMethodError:
       undefined method `signed' for #<Rack::Test::CookieJar:0x00007fbc6751fa38>

これは、request spec 内で使われている cookies オブジェクトが ActionDispatch::Cookies::CookieJar ではなく Rack::Test::CookieJar のインスタンスであり、signedencrypted メソッドを実装していないためです。

解決方法

it do
  get some_url
  expect(response).to have_http_status(:success)

  jar = ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash)
  expect(jar.signed['your_cookie_comers_here']).to eq('something')
  expect(jar.encrypted['another_cookie_comers_here']).to eq('something_else')
end

補足

ただこれだけでは、secure: true(httpsサーバにだけcookieを送信する設定)の場合には動きません。(jar.signed['your_cookie_comers_here']の部分が nil になってしまいます)

    cookies.signed[:your_key_comers_here] = {
      value: 'your_value_comers_here',
      expires: 1.day.from_now,
      secure: true,  # この設定
      httponly: true
    }

rspec を ssl モードで動かす必要がある気がしますが、↓に書かれているように protocol: 'https://'protocol: :https' を試しても駄目でした。
ArgumentError: unknown keyword: protocol
と言われるので、そもそも protocol が rspec でサポートされていないようです。

https://stackoverflow.com/questions/6785261/test-an-https-ssl-request-in-rspec-rails

元ネタ

こちらの記事を参考にさせてもらいました。
https://philna.sh/blog/2020/01/15/test-signed-cookies-in-rails/

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

link_toメソッドの使い方

link_toメソッドはerbに埋め込みます。
アセットパイプラインにより、erbファイルをhtmlに変換する事でlink_toからaタグに変わります。
このように、Rubyのコードを自動で変換する機能がついているのがRuby on Railsです。
えへへ

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

【Ruby】メソッドの定義まとめ

【Ruby】メソッドの定義まとめ

rubyのメソッド定義に関する記述のまとめ。

メソッドの定義方法は基本的にpythonと類似。
Rubyは1行目文末に「:」不要。終わりに「end」が必要。

「:」を使ったキーワード引数で、引数名で値を渡すことができる。

目次

  1. メソッド定義
  2. メソッドの呼び出し
  3. 引数を渡す
  4. 戻り値
    1. メソッドで戻り値を使う
    2. 戻り値で真偽値を返す
    3. returnでメソッドが終了する
    4. 戻り値とif文(戻り値をif文に渡す)
    5. 戻り値とif文メソッド内でreturnとif文を使う
  5. キーワード引数

メソッド定義

def メソッド名
 処理
end

└ end必須

メソッドの呼び出し

メソッド名
 └ ( )不要 ※引数なしの場合

メソッド実例
def hello
 puts "こんにちは"
end

hello

#出力
こんにちは

引数を渡す

def メソッド名(引数名1,引数名2,,)
 処理
end

メソッド名(引数名1,引数名2,,)

 └ 引数なしでは呼び出せない(ないとエラー)  
 └ 引数の数は合わせること(合わないとエラー)
 └ 引数名が使えるのは定義したメソッドの中のみ(スコープ)

戻り値

return 値
 └ 値をメソッドと置き換える。
 └ 値は文字列、数式など

メソッドで戻り値を使う

def メソッド名
 return 
end
戻り値の例
def divide(a,b)
  return a/b
end

puts add(10,5)

value=add(10,5)
puts "割り算結果は#{value}です"

#出力
2
割り算結果は2です

戻り値で真偽値を返す

return 条件式
 └ 条件式の結果がtrue/falseで返される

メソッド名?(引数名)
 └ 真偽値(true/false)を返すメソッドには「?」をつける
 └ 慣習として

0以上かを真偽値で返すメソッド
def positive?(value)
 return value > 0
end

puts positive?(10)
puts positive?(-3)

#出力
true
false

returnでメソッドが終了する

メソッド内のreturn以降の処理は実行されない。

def divide(a,b)
  return a/b
  puts "割り算しました"
end

divide(10,5)

#出力
2

※returnのすぐ下の「puts "割り算しました"」は実行されない。

戻り値とif文(戻り値をif文に渡す)

メソッドの戻り値に真偽値を設定し、if文の条件式でメソッドを呼び出す。

#真偽値を返すメソッド
def discount?(price)
 return price >= 1000
end


price=800

if discount?(price)
  puts "10%値引きします。価格は#{price*0.9}です。"
else 
  puts "あと#{1000-price}円で10%割引です"
end


#出力
あと200円で10%割引です

戻り値とif文(メソッド内でreturnとif文を使う)

メソッドの中でif文と戻り値を使う。

#1000以上で消費税を割り引く
def total_value(price)
 if price >= 1000
   return price
 end 
 return 1000*1.1

end

puts "お支払い金額は#{total_value(800)}円です"

#出力
お支払い金額は880円です

キーワード引数

引数の名前で値を指定する。
def メソッド名(引数名A:,引数名B:,,,)
 └ 引数名の後ろに「:」をつける
 └ 処理は変更なし

メソッド名(引数名B:値,引数名A:値,,,)
 └ メソッド内で定義した引数名と合わせる
 └ 引数名の後に「:」をつける

def user(name:, age:, gender:, word:)
  puts "#{name}さんの年齢は#{age}です"
  puts "性別は#{gender}です"
  puts "口癖は「#{word}」です"
end

user(gender:"male", name:"JoJo", age:"17", word:"オラオラオラオラオラオラ")

#出力
JoJoさんの年齢は17です"
性別はmaleです"
口癖は「オラオラオラオラオラオラ」です"


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

【Rails】APIモードでcookieを使う

環境

  • Rails 5.2.3

前提

または

  • ActionController::APIを継承して API用のcontrollerを使っている。

問題/やりたいこと

そのままでは cookiesにアクセスできないので、アクセスできるようにしていきます。

やり方

ここでは後者(ActionController::APIを継承して API用のcontrollerを使っている)前提とします。

ActionController::Cookies を include する

ActionController::APIを継承しているベースコントローラー、または実際にcookiesにアクセスしたいコントローラーでinclude ActionController::Cookiesします。

つまり

  class YourApiBaseController < ActionController::API
    include ActionController::Cookies

または

  class YourApiController < YourApiBaseController
    include ActionController::Cookies

アプリケーションで ActionDispatch::Cookies を使えるようにする

config/application.rb
module YourApi
  class Application < Rails::Application
    config.middleware.use ActionDispatch::Cookies

その他

別途 session_storecredentials.yml.enc (旧バージョンの場合は secrets.yml* )の設定はされている必要があります。

config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store, key: 'your-cookie-key-comes-here'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】基本コード一覧。実例で基本をおさえる

【Ruby】基本コード一覧。実例で基本をおさえる

忘備録用のrubyの基本事項の一覧。

puts、ハッシュ、シンボル、each文の書き方、if文のelsifを覚えておけば、あとは他の言語とほぼ共通。

目次

  1. 拡張子(.rb)
  2. 出力(putsメソッド)
  3. コメントアウト(#)
  4. 文字をつなげる(+)
  5. 変数定義(=)
  6. 代入演算子
  7. 変数展開("#{ }")
  8. if文(条件分岐)
  9. 比較演算子
  10. 論理演算子
  11. 配列
  12. each文(繰り返し処理)
  13. シンボル(:値)
  14. ハッシュ
    1. ハッシュ値の更新
    2. ハッシュに要素追加
    3. キーにシンボルを使う
    4. シンボルを使った省略形
  15. nil
    1. nilとハッシュ
    2. nilとif文
  16. 配列とハッシュ
    1. 配列の中のハッシュの値を取り出す
    2. each文を使ってハッシュの値を取り出す
    3. ハッシュ・each文・if文の組み合わせ


拡張子

.rb

出力(putsメソッド)

puts "AAA"
 └ 「'」でもOK
 └ putsの後に半角スペース必須

コメントアウト(#)

#

文字をつなげる(+)

puts 'AAA' + 'BBB'
 └ 数値と文字列の連結は不可
 └ 型は統一する(もしくは変数展開)

変数定義

変数名 = 数値
変数名 = '文字列'

代入演算子

x=x+10x+=10
x=x-10x-=10
x=x*10x*=10
x=x/10x/=10
x=x%10x%=10

変数展開

puts "#{ }"
 └ ダブルクオテーション
 └ ※シングルクオテーションはそのまま文字列として出力

hello = "こんにちは"
name = "AAA"

puts "#{hello}#{name}さん"

if文(条件分岐)

if 条件式1
 処理
elsif 条件式2
 処理
else
 処理
end

 └ elsif (python:elif, php: else if)
 └ 条件式は true/falseに置き換わる
 └ falseの場合は処理をスキップ

比較演算子

== 等しい
!= 等しくない
> より大きい
< より小さい
>= 以上
<= 以下

論理演算子

&& かつ
|| または

配列

[要素1,要素2,要素3,,,]

array = ["AAA","BBB","CCC"]
puts array[0]
puts "インデックス番号1の要素は#{array[1]}です。"

#出力
AAA
インデックス番号1の要素はBBBです。

each文(繰り返し処理)

配列.each do |変数名|
  処理
end

 └ .each do| |
 └ 最後にend必須
 └ 定義した変数名が使えるのはeach文の中のみ

使用例
elements = ["AAA","BBB","CCC"]
elements.each do |element|

puts "配列の中身は#{element}です。" 

#出力
配列の中身はAAAです。
配列の中身はBBBです。
配列の中身はCCCです。

シンボル

:値
 └ シンボル型
 └ 文字列の代替として使える
 └ ハッシュのキーに使われることが多い

puts "AAA"
puts :AAA

#出力
AAA
AAA

ハッシュ

各値に名前(キー)をつけた配列の呼び名。
{キー1=>値1,キー2=>値2, キー3=>値3,,,}
 └ 文字列は「"」で囲む(キー、値)
 └ 波括弧:{ }
 └ 配列にハッシュを代入する

ハッシュ実例
elements = {"key1"=>"A","key2"=>"B", "key3"=>"C"}
puts elements["key2"]

#出力
B

▼他の言語
・pythonの辞書型
 {キー1:値1,キー2:値2, キー3:値3,,,}
・phpの連想配列
  (キー1=>値1,キー2=>値2, キー3=>値3,,,);

ハッシュ値の更新

ハッシュ["キー名"] = 変更したい値

ハッシュ実例
elements = {"key1"=>"A","key2"=>"B", "key3"=>"C"}
elements["key2"]=222

puts elements["key2"]

#出力
222

ハッシュに要素追加

elements = {"key1"=>"A","key2"=>"B", "key3"=>"C"}
elements["key4"]="D"

puts elements

#出力
{"key1"=>"A","key2"=>"B", "key3"=>"C", "key4"=>"D"}

キーにシンボルを使う

キー名を文字列ではなく、「:」をつけて表せる。
:キー名=>値
 └ 取得 配列[:キー名]

elements = {:key1=>"A",:key2=>"B", :key3=>"C"}
puts elements[:key2]

#出力
B

シンボルを使った省略形

:キー名=>値キー名:値
 └ ※取得時はシンボルを使う
 └ 取得 配列[:キー名]

シンボル(省略形)
elements = {key1:"A",key2:"B", key3:"C"}
puts elements[:key2]

#出力
B

nil

nil(ニル)
 └ 空の要素
 └ nil自体を出力すると空文字になる

nilとハッシュ

  • 存在しないキーを指定すると、値はnilになる
  • エラーにならない
nil
elements = {key1:"A",key2:"B", key3:"C"}
puts elements[:key4]
puts nil

#出力


nilとif文

if文は条件式がtrueとfalse以外に、nilかそれ以外も使える。

・nil → false
・nil以外 → true

elements = {key1:"A",key2:"B", key3:"C"}

if elements[:key4]
 puts "key4の値は#{elements[:key4]}です"
else
 puts "key4はありません"
end

#出力
key4はありません

配列とハッシュ

配列の要素にハッシュを入れる

配列の要素にハッシュを入れることができる。

elements = [{order:"A",number:1}, {order:"B",number:2}, {order:"C",number:3}]
改行で整える
elements = [
  {order:"A",number:1}, 
  {order:"B",number:2}, 
  {order:"C",number:3}
]

puts elements[0]

#出力
{order:"A",number:1}

配列の中のハッシュの値を取り出す

elements = [
  {order:"A",number:1}, 
  {order:"B",number:2}, 
  {order:"C",number:3}
]

#変数に代入して取り出す
element = elements[0]
puts element[:order]

#1行で取り出す
puts elements[0][:order]

#出力
A
A

each文を使ってハッシュの値を取り出す

each文
配列.each do |変数名|
  処理
end
each文とハッシュ
elements = [
  {order:"A",number:1}, 
  {order:"B",number:2}, 
  {order:"C",number:3}
]

elements.each do |element|
  puts "#{element[:order]}の数字は#{element[:number]}です"
end

#出力
Aの数字は1です
Bの数字は2です
Cの数字は3です

ハッシュ・each文・if文の組み合わせ

elements = [
  {order:"A",number:1}, 
  {order:"B"}, 
  {order:"C",number:3},
  {order:"D"}
]

elements.each do |element|
 #数字があるかないかを判断
  if element[:number]
    puts "#{element[:order]}の数字は#{element[:number]}です"
  else
    puts "#{element[:order]}の数字はありません"
  end
end

#出力
Aの数字は1です
Bの数字はありません
Cの数字は3です
Dの数字はありません



each文の書き方が独特でおもしろい。
doをつけたり変数名をパイプで囲むメリットがなぞ。

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

Rubyプログラム内で別のコマンドを起動する方法

Rubyプログラム内で別のコマンドを起動する方法について.
主に popenについて書く

方法1: system()関数

system()関数にコマンドを文字列で与える.
最も簡単。

例1 引数無し

sys0.rb
system("ls")

実行すると

ruby sys0.rb

a.txt b.txt c.txt sys0.rb sys1.rb sys2.rb

例2 引数あり

sys1.rb
system("ls *.txt")

実行すると

ruby sys1.rb

a.txt b.txt c.txt

例3 終了ステータスの取得

Exit status (終了ステータス, 脱出ステータス)は $? で取得できる

sys2.rb
system("ls abc")
print $?, "\n"

実行すると

ruby sys2.rb

ls: cannot access abc: No such file or directory
pid 13715 exit 2

lsは失敗し終了ステータス2が得られている.

方法2 popen

実行したコマンド(子プロセス)の出力を得たり,入力したりできる.
要は,標準入出力にread/writeできる.

例1 結果を得る.標準出力からread

popen0.rb
IO.popen("ls","r") do | io |
        while io.gets do
                print
        end
end

実行すると

ruby popen0.rb

a.txt
b.txt
c.txt
popen0.rb
sys0.rb
sys1.rb
sys2.rb

例2 標準入出力にread/write

popen1.rb
IO.popen("grep e","r+") do | io |
        io.print "Hello,\n"
        io.print "World!\n"
        io.close_write
        while io.gets do
                print
        end
end

実行すると

ruby popen1.rb

Hello,

方法3 fork and exec

例1 forkのみ

fork0.rb
print "Start!\n"
child_pid = fork do
  #以下 子プロセスのみが実行
  print "I am a child. pid=", Process.pid, "\n"
  sleep(1)
  #以上 子プロセスのみが実行
end
#以下 親プロセスのみが実行
print "I am a parent. my pid=", Process.pid, ", my child's pid=", child_pid, "\n"
Process.waitpid(child_pid) #子プロセスの終了を待つ

実行すると

ruby fork0.rb

Start!
I am a parent. my pid=25527, my child's pid=25529
I am a child. pid=25529

例2 fork and exec

fork1.rb
print "Start!\n"
child_pid = fork do
  #以下 子プロセスのみが実行
  exec("ls")
  #以上 子プロセスのみが実行
  #以下 実行されない
  abc()
  #以上 実行されない
end
#以下 親プロセスのみが実行
print "I am a parent. my pid=", Process.pid, ", my child's pid=", child_pid, "\n"
Process.waitpid(child_pid) #子プロセスの終了を待つ

実行すると

ruby fork1.rb

Start!
I am a parent. my pid=25801, my child's pid=25803
a.txt b.txt c.txt fork0.rb fork1.rb popen0.rb popen1.rb sys0.rb sys1.rb sys2.rb

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