20200405のRubyに関する記事は15件です。

【Ruby】puts,p,printメソッドの違い

puts、p、printメソッドについて、違いが分からなかったので調べてみました。


putsメソッド

-改行あり
-引数のオブジェクトを文字列に変換して出力

putsメソッド
puts "あけまして、そらじろう"
puts 20200405
ターミナル
あけまして、そらじろう
20200405

pメソッド
-改行あり
-引数と引数の文字型(文字列や数値)も出力

pメソッド
p "あけまして、そらじろう"
p 20200405
ターミナル
"あけまして、そらじろう"
20200405

printメソッド
-改行なし
-数のオブジェクトを文字列に変換し出力

printメソッド
print "あけまして、そらじろう"
print 20200405
ターミナル
あけまして、そらじろう20200405

追記
putsメソッドとpメソッドの違いは、文字列として識別できるかどうか
printメソッドは、改行のないputsメソッド

ちなみにpメソッドは、デバッグでよく使うみたいです。

<https://gihyo.jp/book/2018/978-4-297-10123-7
<https://rurema.clear-code.com/

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

Dockerを使ってrailsのAPIモードの環境構築

はじめに

今回は、Dockerを使ってRailsのAPIモードの環境構築をしていく記事です。
コマンド、設定などを詳細に書いていきます。

記事の最後までの所要時間は30分以内です。意外と簡単でした。

APIモードとは

APIモードとはRails5から追加された機能で、APIのようなRailsアプリケーションを作れる機能だそうです。
MVCのうち、モデルとコントローラーのみが作成されます。
APIモードで作成されたアプリのURLにリクエストを送ると、json形式のデータがレスポンスとして返ってきます。

環境構築

では、環境構築をしていきます。

$ mkdir sample_app
$ cd sample_app
$ docker pull ruby:2.5.1
$ docker run --rm -v "$PWD":/usr/src/sample_app -w /usr/src/sample_app ruby:2.5.1 bundle init
$ docker build -t developer_name/sample_app .

ここで、Dockerfiledocker-compose.ymlGemfileGemfile.lockの4つのファイルが必要となるので、sample_app以下にそれぞれ作っていきます。

$ touch 各ファイル

Dockerfilesample_appはそれぞれのディレクトリ名に変更してください。

Dockerfile
# Debianがベースのrubyイメージを指定
FROM ruby:2.5.1

# 必要なものをインストール
RUN apt-get update -qq && apt-get -y install \
    build-essential \
    libpq-dev \
    nodejs \
    mysql-client

# rails用のディレクトリを作成
RUN mkdir /sample_app

# ローカルマシン(Mac)からコンテナの中にファイルをコピー
COPY Gemfile /API_sample
COPY Gemfile.lock /sample_app

# 作業ディレクトリを指定
WORKDIR /sample_app

# 上でコピーしたGemfileに従ってGemをインストール
RUN gem install bundler && bundle install

続いて、docker-compose.ymlです。
MYSQL_ROOT_PASSWORDは、後で作成するdatabase.ymlのパスワードに合わせます。

docker-compose.yml
version: '3'
services:
  web:
    build: .
    ports:
      - "3000:3000"
    depends_on:
      - db
    volumes:
      - .:/API_sample
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
  db:
    image: mysql:5.7
    volumes:
      - mysql_data:/var/lib/mysql/
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"
volumes:
  mysql_data:

Railsアプリの作成

新規アプリ作成時に、後ろに--apiをつけるとAPIモードでアプリが作成されます。

$ docker-compose run web rails new . --force --database=mysql --api
$ docker-compose run --rm web rails generate scaffold User name:string
$ docker-compose run --rm web rails db:create
$ docker-compose run --rm web rails db:migrate

scaffoldでUserを作ったら、ファイルを見てみましょう。
普通のrailsアプリで作られるassetsviewが無いことが確認できると思います。

ページを開く

それでは、アプリをlocalhostで開いてみましょう。
http://localhost:3000/users

真っ白なページに、[]とだけ表示されているはずです。
これは、データが空であるということを表しています。

では、curlコマンドでデータのリクエストを送ってみます。

curlはURLシンタックスを用いてファイルを送信または受信するコマンドラインツールである。(Wikipediaより)

下記の例を簡単に説明すると、指定したURLに対して、POSTメソッドを使用してターミナルからjsonファイルを送信しているということです。

$ curl -X POST -H "Content-Type: application/json" -d '{"name": "hoge"}' http://localhost:3000/users

そして、http://localhost:3000/users に接続すると
[{"id":1,"name":"hoge","created_at":"","updated_at":""}]というデータが表示されると思います。(日付はカットしました)

これで完了です!意外とあっさりできますよね。
他にも、GET、PUT、DELETEも使えるので色々と試してみてください。

感想

APIモードと聞いて最初はよくわからないな、怖いなと思っていました。
ですが、やってみると簡単ですし、今まで深く考えていなかったHTTPについて調べたり知ったりするきっかけになったので良かったです。

駆け出しエンジニアにとっても簡単なのでぜひやってみてはいかがでしょうか?

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

[Rails]画像選択時にプレビュー表示

本記事投稿のいきさつ

アプリの中で画像投稿機能を実装したとき、ただフォームを作成するだけでは画像を選択しても表示がされず何を選んだのか確認をすることが出来ません。
そこで、画像を選択した時点でプレビュー表示することができればいいなと思い、実際に機プレビュー機能を作成したため、記録として残したいと思います。
今回はユーザーのプロフィール画像の編集画面を想定します。
そのため、既に登録されている画像は最初からプレビューさせた状態で表示をさせます。

前提

  • 変種画面はprofile_edit.html.haml
  • 画像の保存先は Usersテーブル: imageカラム
  • file_field本体は隠し、画像が選択されていない時はiconを、 選択されている時はプレビュー画像をクリックすることで画像選択できるようにします。
  • jQueryを使用しますが、必要なGemのインストール等は既に出来ているとします。
  • ユーザー機能はDeviseを使用しています。
  • cssは今回の内容に含まれません。

フォーム作成

まず、今回はhamlでビューのフォームを作成します。

profile_edit.html.haml
= form_for current_user, url: {action: 'profile_update'} do |f|

    .form-group 
      .image_form
        .image_form__contents
          -# ラベルでfile_fieldとicon、プレビュー画像を紐付けます
          = f.label :image, class: 'image_label' do
            .prev-contents
              -# 既に登録されている画像があれば表示をさせます
              - if current_user.image.present?
                .prev-content
                  = image_tag current_user.image.url, alt: "preview", class: "prev-image"
              -# 既に登録されている画像がなければiconを表示させます
              - else
                = icon('fas', 'image', class: 'photo-icon')
            -# file_fieldはdisplay: none;で隠します
            = f.file_field :image, class: 'image_form__contents__field hidden_file'

FileReader

今回はFileReaderを使用します。
FileReaderとはHTML5世代の機能でユーザーのPC内にあるファイルやバッファ上の生データに対して、読み取りアクセスを行えるオブジェクトです。

jsファイルの編集

今回はimage_preview.jsを作成して、そこに記述していきます。

image_preview.js
$(document).on('turbolinks:load', function () {
  $(function () {
    // 画像をプレビュー表示させる.prev-contentを作成
    function buildHTML(image) {
      var html =
        `
        <div class="prev-content">
          <img src="${image}", alt="preview" class="prev-image">
        </div>
        `
      return html;
    }

    // 画像が選択された時に発火します
    $(document).on('change', '.hidden_file', function () {
      // .file_filedからデータを取得して変数fileに代入します
      var file = this.files[0];
      // FileReaderオブジェクトを作成します
      var reader = new FileReader();
      // DataURIScheme文字列を取得します
      reader.readAsDataURL(file);
      // 読み込みが完了したら処理が実行されます
      reader.onload = function () {
        // 読み込んだファイルの内容を取得して変数imageに代入します
        var image = this.result;
        // プレビュー画像がなければ処理を実行します
        if ($('.prev-content').length == 0) {
          // 読み込んだ画像ファイルをbuildHTMLに渡します
          var html = buildHTML(image)
          // 作成した.prev-contentをiconの代わりに表示させます
          $('.prev-contents').prepend(html);
          // 画像が表示されるのでiconを隠します
          $('.photo-icon').hide();
        } else {
          // もし既に画像がプレビューされていれば画像データのみを入れ替えます
          $('.prev-content .prev-image').attr({ src: image });
        }
      }
    });
  });
});

上記で出てくるDataURISchemeとは、
簡単にいうと、画像やらJavascriptやらそういったHTMLのコンテンツを文字列として定義出来るものです。
以上でで、画像が表示されます。

終わり

最後まで見ていただきありがとうございました。

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

gem annotate の README を翻訳しました

概要

gem annotateREADME を翻訳しました。

Annotate (aka AnnotateModels)

下記のファイルの上部あるいは下部に現在の DB スキーマを要約したコメントを追加します。

  • ActiveRecord models
  • Fixture files
  • Tests and Specs
  • Object Daddy exemplars
  • Machinist blueprints
  • Fabrication fabricators
  • Thoughtbot's factory_bot factories, i.e. the (spec|test)/factories/<model>_factory.rb files
  • routes.rb file (for Rails projects)

スキーマコメントは下記のように記載されます。

# == Schema Info
#
# Table name: line_items
#
#  id                  :integer(11)    not null, primary key
#  quantity            :integer(11)    not null
#  product_id          :integer(11)    not null
#  unit_price          :float
#  order_id            :integer(11)
#

class LineItem < ActiveRecord::Base
  belongs_to :product
  . . .

また、SpatialAdapter , PostgisAdapter , PostGISAdapter のいずれかを使用したときは、geom タイプや srid タイプのような幾何学的なカラムにも注釈をつけます。

# == Schema Info
#
# Table name: trips
#
#  local           :geometry        point, 4326
#  path            :geometry        line_string, 4326

また、-r オプションを渡すと、rake routes の出力を routes.rb にコメントとして追加します。

3.Xにアップグレードするとモデルへの注釈が動作しない?

バージョン 2.7.X では、引数が何も渡されない場合は、gem annotate はデフォルトでモデルに注釈を追加していました。
デフォルトでは、gem annotate は routes とモデルが一緒に注釈を追加されることを許可していません。
#647 で変更が追加されました。
詳細はこちらでご覧ください。

この問題を修正する方法はいくつかあります。

  • CLI を使用している場合は、--models を使用してモデルフラグを明示的に渡してください。

あるいは

a) rails g annotate:install を実行し、デフォルト設定が models オプション 'true' になるように上書きします。

b) lib/tasks/auto_annotate_models.rake において models のキーと値を追加します。

    Annotate.set_defaults(
      ...
      'models'                      => 'true',
      ...

インストール

rubygems.org から 追加

group :development do
  gem 'annotate'
end

Github から 追加

group :development do
  gem 'annotate', git: 'https://github.com/ctran/annotate_models.git'
end

rubygems.org からインストール

gem install annotate

Github のチェックアウトからインストール

git clone https://github.com/ctran/annotate_models.git annotate_models
cd annotate_models
rake build
gem install pkg/annotate-*.gem

利用方法

もし Gemfile 経由でインストールをしているなら、bundle exec を下記コマンドにつけてください。

Rails での利用方法

すべてのモデル、テスト、フィクスチャ、ファクトリに注釈をつける。

cd /path/to/app
annotate

モデル、テスト、ファクトリのみに注釈をつける。

annotate --models --exclude fixtures

モデルのみに注釈をつける。

annotate --models

routes.rb に注釈をつける。

annotate --routes

モデル、テスト、フィクスチャ、ファクトリ、シリアライザの注釈を削除する。

annotate --delete

routes.rb の注釈を削除する。

annotate --routes --delete

db:migrate を実行するたびに自動的に注釈をつけるためには、
rails g annotate:install を実行するか、RakefileAnnotate.load_tasks を追加してください。

詳細は Rails における設定 をご覧ください。

Rails の外での利用方法

--routes オプションが意味をなさないことを除けば Rails 外の利用でも上述のすべてが適用されます。明示的に1つあるいは複数の --require オプションと --model-dir オプションを設定することで、annotate にプロジェクトの構造を知らせ、プロジェクトが起動し関連のあるコードを読み込むことを助ける必要があります。

設定

もし特定のモデルで注釈を常に飛ばしたいときは、モデルファイルの任意の場所に下記の文字列を追加してください。

# -*- SkipSchemaAnnotations

Rails における設定

設定ファイル(.rake ファイル形式)を生成して、デフォルトオプションを設定するためには下記のコマンドを実行してください。

rails g annotate:install

このファイルを編集することで、注釈がファイルの上下どちらに追加されるかや、どのタイプのファイルに注釈が記載されるかといったような、出力形式などを制御します。

生成される rake ファイルである lib/tasks/auto_annotate_models.rakeAnnotate.load_tasks も含みます。この rake ファイルはコマンドラインの機能と重複するいくつかの rake タスクを追加します。

rake annotate_models                          # Add schema information (as comments) to model and fixture files
rake annotate_routes                          # Adds the route map to routes.rb
rake remove_annotation                        # Remove schema information from model and fixture files

デフォルトでは、一旦設定ファイルを生成したあとは、rake db:migrate が実行されるたびに annotate が実行されます(development 環境のみ)。
この挙動を永続的に無効にしたいときは、.rake ファイルを編集し、下記のように変更してください。

    'skip_on_db_migrate'   => 'false',

上記の記述から下記の記述へ変更する。

    'skip_on_db_migrate'   => 'true',

1回だけ annotate なしで rake db:migrate を実行したいときは、.rake ファイルを編集する代わりにシンプルな環境変数をつけることでそれを実現できます。

ANNOTATE_SKIP_ON_DB_MIGRATE=1 rake db:migrate

オプション

  • --additional-file-patterns

コンマで区切った注釈を行う追加のファイルパスあるいは glob(例: /foo/bar/%model_name%/*.rb,/baz/%model_name%.rb)を記載する。

  • -d, --delete

すべてのモデルファイルあるいは routes.rb ファイルから注釈を削除する。

  • -p [before|top|after|bottom], --position

model/test/fixture/factory/route/serializer ファイルの上部(前)あるいは下部(後)に注釈を置く。

  • --pc, --position-in-class [before|top|after|bottom]

model ファイルの上部(前)あるいは下部(後)に注釈を置く。

  • --pf, --position-in-factory [before|top|after|bottom]

factory ファイルの上部(前)あるいは下部(後)に注釈を置く。

  • --px, --position-in-fixture [before|top|after|bottom]

fixture ファイルの上部(前)あるいは下部(後)に注釈を置く。

  • --pt, --position-in-test [before|top|after|bottom]

test ファイルの上部(前)あるいは下部(後)に注釈を置く。

  • --pr, --position-in-routes [before|top|after|bottom]

routes.rb ファイルの上部(前)あるいは下部(後)に注釈を置く。

  • --ps, --position-in-serializer [before|top|after|bottom]

serializer ファイルの上部(前)あるいは下部(後)に注釈を置く。

  • --w, --wrapper STR

パラメーターとして渡されたテキストで注釈をラップします。
--w が使用された場合は、同じテキストが始端と終端に使用されます。

  • --wo, --wrapper-open STR

注釈の始端をラップするテキスト。

  • --wc, --wrapper-close STR

注釈の終端をラップするテキスト。

  • -r, --routes

'rake routes' の出力で routes.rb に注釈を付ける。

  • --models

ActiveRecord モデルに注釈を付ける。

  • -a, --active-admin

active_admin モデルに注釈を付ける。

  • -v, --version

この gem の現在のバージョンを表示する。

  • -m, --show-migration

注釈にマイグレーションのバージョン番号を含める。

  • -k, --show-foreign-keys

注釈にテーブルの外部キー制約の一覧を載せる。

  • --ck, --complete-foreign-keys

注釈に完全な外部キーの名前を載せる。

  • -i, --show-indexes

注釈にテーブルのインデックスの一覧を載せる。

  • -s, --simple-indexes

注釈内のカラムの関連したインデックスを結合する。

  • --model-dir dir

app/models ではないディレクトリに保存されたモデルファイルに注釈を付ける。ディレクトリ名はカンマで区切る。

  • --root-dir dir

ルートディレクトリのプロジェクト内に保存されたファイルに注釈を付ける。ディレクトリ名はカンマで区切る。

  • --ignore-model-subdirects

モデルディレクトリのサブディレクトリを無視する。

  • --sort

作成順ではなくアルファベット順でカラムをソートする。

  • --classified-sort

アルファベット順にカラムをソートする。ただし、一番目には id 、その次が残りのカラム、タイムスタンプカラム、関連付けカラムの順となる。

  • -R, --require path

モデルを読み込む前に必要とする追加のファイル。このファイルは複数回使用される。

  • -e [tests,fixtures,factories,serializers], --exclude

tests,fixtures,factories,serializers ファイルに注釈を付けないようにする。

  • -f [bare|rdoc|yard|markdown], --format

プレインテキスト/RDoc/YARD/Markdown としてスキーマ情報を記述する。

  • --force

変更がない場合でも新しい注釈を強制的に記述する。

  • --frozen

注釈の変更を許可しない。ファイルに変更がある場合は、ゼロ以外の値で終了します。

  • --timestamp

注釈にタイムスタンプを含ませる。

  • --trace

ファイルに注釈を付けられない場合は、例外メッセージだけでなく、スタックトレース全体を表示する。

  • -I, --ignore-columns REGEX

与えられた正規表現に合致するカラムに注釈をつけないようにする(例: annotate -I '^(id|updated_at|created_at)')。

  • --ignore-routes REGEX

与えられた正規表現に合致する routes に注釈をつけないようにする(例: annotate -I '(mobile|resque|pghero)')。

  • --hide-limit-column-types VALUES

与えられたカラムタイプに limit 値を表示させない。カラムタイプはコンマで区切る(例: integer,boolean,text)。

  • --hide-default-column-types VALUES

与えられたカラムタイプに default 値を表示させない。カラムタイプはコンマで区切る(例: json,jsonb,hstore)。

  • --ignore-unknown-models

不正なモデルファイルに対して警告を表示させない。

  • --with-comment

モデルの注釈にデータベースコメントを含める。

オプション: additional_file_patterns

CLI: --additional-file-patterns

Ruby: :additional_file_patterns

注釈を行うために追加のパスを提供します。このパスは glob を含むことができます。絶対パスを使用することを推奨します。下記に例を記載します。

  • /app/lib/decorates/%MODEL_NAME%/*.rb
  • /app/lib/forms/%PLURALIZED_MODEL_NAME%/**/*.rb
  • /app/lib/forms/%TABLE_NAME%/*.rb

適切なモデルは %*% 構文を用いて推論され、一致するファイルに注釈が付けられます。これは既存のファイル名解決とともに動作します(annotate_models.rbresolve_filename メソッドの中で発見されるオプション)。

Rails の設定の中で使用するときは、下記を使用できます。

File.join(Rails.application.root,
'app/lib/forms/%PLURALIZED_MODEL_NAME%/***/**.rb')

ソート

デフォルトでは、カラムはデータベース順でソートされます(つまりマイグレーションが行われた順番)。

もしアルファベット順にソートしてマイグレーションを実行した順番とは関係なく注釈の結果を一致させたいときは、--sort オプションを使用してください。

マークダウン

生成されるフォーマットは実際には MultiMarkdown で、テーブルのための構文拡張機能を利用しています。もしこのフォーマットを使用したいときはパーサーとして kramdown を使用することを推奨しています。もしドキュメントを生成するために yard を使用している場合は、.yardopts ファイルに kramdown を追加することでプロバイダとして kramdown をマークダウンのフォーマットに指定してください。

--markup markdown
--markup-provider kramdown

Gemfile にも同様に kramdown を追加するようにしてください。

gem 'kramdown', groups => [:development], require => false

警告

自動作成されたコメントブロックの後にテキストを追加しないでください。annotate によって以前にコメントブロックが追加された可能性があるとき、annotate はモデル内の始端・終端のコメントブロックを削除することがあります。

annotate が行った変更を必ず確認するようにしてください。Git を使用している場合は、annotate を実行した後にプロジェクトのステータスを確認することができます。

$ git status

VCS(Git や Subversion など)を使っていない人は、特に注意して annotate を扱い、1つの VCS の利用を検討してください。

リンク集

ライセンス

Ruby と同じライセンスでリリースしています。サポートと保証はありません。

作者

AUTHORS.md をご覧ください。

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

gem annotate のドキュメントを翻訳しました

概要

gem annotateREADME を翻訳しました。

Annotate (aka AnnotateModels)

下記のファイルの上部あるいは下部に現在の DB スキーマを要約したコメントを追加します。

  • ActiveRecord models
  • Fixture files
  • Tests and Specs
  • Object Daddy exemplars
  • Machinist blueprints
  • Fabrication fabricators
  • Thoughtbot's factory_bot factories, i.e. the (spec|test)/factories/<model>_factory.rb files
  • routes.rb file (for Rails projects)

スキーマコメントは下記のように記載されます。

# == Schema Info
#
# Table name: line_items
#
#  id                  :integer(11)    not null, primary key
#  quantity            :integer(11)    not null
#  product_id          :integer(11)    not null
#  unit_price          :float
#  order_id            :integer(11)
#

class LineItem < ActiveRecord::Base
  belongs_to :product
  . . .

また、SpatialAdapter , PostgisAdapter , PostGISAdapter のいずれかを使用したときは、geom タイプや srid タイプのような幾何学的なカラムにも注釈をつけます。

# == Schema Info
#
# Table name: trips
#
#  local           :geometry        point, 4326
#  path            :geometry        line_string, 4326

また、-r オプションを渡すと、rake routes の出力を routes.rb にコメントとして追加します。

3.Xにアップグレードするとモデルへの注釈が動作しない?

バージョン 2.7.X では、引数が何も渡されない場合は、gem annotate はデフォルトでモデルに注釈を追加していました。
デフォルトでは、gem annotate は routes とモデルが一緒に注釈を追加されることを許可していません。
#647 で変更が追加されました。
詳細はこちらでご覧ください。

この問題を修正する方法はいくつかあります。

  • CLI を使用している場合は、--models を使用してモデルフラグを明示的に渡してください。

あるいは

a) rails g annotate:install を実行し、デフォルト設定が models オプション 'true' になるように上書きします。

b) lib/tasks/auto_annotate_models.rake において models のキーと値を追加します。

    Annotate.set_defaults(
      ...
      'models'                      => 'true',
      ...

インストール

rubygems.org から 追加

group :development do
  gem 'annotate'
end

Github から 追加

group :development do
  gem 'annotate', git: 'https://github.com/ctran/annotate_models.git'
end

rubygems.org からインストール

gem install annotate

Github のチェックアウトからインストール

git clone https://github.com/ctran/annotate_models.git annotate_models
cd annotate_models
rake build
gem install pkg/annotate-*.gem

利用方法

もし Gemfile 経由でインストールをしているなら、bundle exec を下記コマンドにつけてください。

Rails での利用方法

すべてのモデル、テスト、フィクスチャ、ファクトリに注釈をつける。

cd /path/to/app
annotate

モデル、テスト、ファクトリのみに注釈をつける。

annotate --models --exclude fixtures

モデルのみに注釈をつける。

annotate --models

routes.rb に注釈をつける。

annotate --routes

モデル、テスト、フィクスチャ、ファクトリ、シリアライザの注釈を削除する。

annotate --delete

routes.rb の注釈を削除する。

annotate --routes --delete

db:migrate を実行するたびに自動的に注釈をつけるためには、
rails g annotate:install を実行するか、RakefileAnnotate.load_tasks を追加してください。

詳細は Rails における設定 をご覧ください。

Rails の外での利用方法

--routes オプションが意味をなさないことを除けば Rails 外の利用でも上述のすべてが適用されます。明示的に1つあるいは複数の --require オプションと --model-dir オプションを設定することで、annotate にプロジェクトの構造を知らせ、プロジェクトが起動し関連のあるコードを読み込むことを助ける必要があります。

設定

もし特定のモデルで注釈を常に飛ばしたいときは、モデルファイルの任意の場所に下記の文字列を追加してください。

# -*- SkipSchemaAnnotations

Rails における設定

設定ファイル(.rake ファイル形式)を生成して、デフォルトオプションを設定するためには下記のコマンドを実行してください。

rails g annotate:install

このファイルを編集することで、注釈がファイルの上下どちらに追加されるかや、どのタイプのファイルに注釈が記載されるかといったような、出力形式などを制御します。

生成される rake ファイルである lib/tasks/auto_annotate_models.rakeAnnotate.load_tasks も含みます。この rake ファイルはコマンドラインの機能と重複するいくつかの rake タスクを追加します。

rake annotate_models                          # Add schema information (as comments) to model and fixture files
rake annotate_routes                          # Adds the route map to routes.rb
rake remove_annotation                        # Remove schema information from model and fixture files

デフォルトでは、一旦設定ファイルを生成したあとは、rake db:migrate が実行されるたびに annotate が実行されます(development 環境のみ)。
この挙動を永続的に無効にしたいときは、.rake ファイルを編集し、下記のように変更してください。

    'skip_on_db_migrate'   => 'false',

上記の記述から下記の記述へ変更する。

    'skip_on_db_migrate'   => 'true',

1回だけ annotate なしで rake db:migrate を実行したいときは、.rake ファイルを編集する代わりにシンプルな環境変数をつけることでそれを実現できます。

ANNOTATE_SKIP_ON_DB_MIGRATE=1 rake db:migrate

オプション

  • --additional-file-patterns

コンマで区切った注釈を行う追加のファイルパスあるいは glob(例: /foo/bar/%model_name%/*.rb,/baz/%model_name%.rb)を記載する。

  • -d, --delete

すべてのモデルファイルあるいは routes.rb ファイルから注釈を削除する。

  • -p [before|top|after|bottom], --position

model/test/fixture/factory/route/serializer ファイルの上部(前)あるいは下部(後)に注釈を置く。

  • --pc, --position-in-class [before|top|after|bottom]

model ファイルの上部(前)あるいは下部(後)に注釈を置く。

  • --pf, --position-in-factory [before|top|after|bottom]

factory ファイルの上部(前)あるいは下部(後)に注釈を置く。

  • --px, --position-in-fixture [before|top|after|bottom]

fixture ファイルの上部(前)あるいは下部(後)に注釈を置く。

  • --pt, --position-in-test [before|top|after|bottom]

test ファイルの上部(前)あるいは下部(後)に注釈を置く。

  • --pr, --position-in-routes [before|top|after|bottom]

routes.rb ファイルの上部(前)あるいは下部(後)に注釈を置く。

  • --ps, --position-in-serializer [before|top|after|bottom]

serializer ファイルの上部(前)あるいは下部(後)に注釈を置く。

  • --w, --wrapper STR

パラメーターとして渡されたテキストで注釈をラップします。
--w が使用された場合は、同じテキストが始端と終端に使用されます。

  • --wo, --wrapper-open STR

注釈の始端をラップするテキスト。

  • --wc, --wrapper-close STR

注釈の終端をラップするテキスト。

  • -r, --routes

'rake routes' の出力で routes.rb に注釈を付ける。

  • --models

ActiveRecord モデルに注釈を付ける。

  • -a, --active-admin

active_admin モデルに注釈を付ける。

  • -v, --version

この gem の現在のバージョンを表示する。

  • -m, --show-migration

注釈にマイグレーションのバージョン番号を含める。

  • -k, --show-foreign-keys

注釈にテーブルの外部キー制約の一覧を載せる。

  • --ck, --complete-foreign-keys

注釈に完全な外部キーの名前を載せる。

  • -i, --show-indexes

注釈にテーブルのインデックスの一覧を載せる。

  • -s, --simple-indexes

注釈内のカラムの関連したインデックスを結合する。

  • --model-dir dir

app/models ではないディレクトリに保存されたモデルファイルに注釈を付ける。ディレクトリ名はカンマで区切る。

  • --root-dir dir

ルートディレクトリのプロジェクト内に保存されたファイルに注釈を付ける。ディレクトリ名はカンマで区切る。

  • --ignore-model-subdirects

モデルディレクトリのサブディレクトリを無視する。

  • --sort

作成順ではなくアルファベット順でカラムをソートする。

  • --classified-sort

アルファベット順にカラムをソートする。ただし、一番目には id 、その次が残りのカラム、タイムスタンプカラム、関連付けカラムの順となる。

  • -R, --require path

モデルを読み込む前に必要とする追加のファイル。このファイルは複数回使用される。

  • -e [tests,fixtures,factories,serializers], --exclude

tests,fixtures,factories,serializers ファイルに注釈を付けないようにする。

  • -f [bare|rdoc|yard|markdown], --format

プレインテキスト/RDoc/YARD/Markdown としてスキーマ情報を記述する。

  • --force

変更がない場合でも新しい注釈を強制的に記述する。

  • --frozen

注釈の変更を許可しない。ファイルに変更がある場合は、ゼロ以外の値で終了します。

  • --timestamp

注釈にタイムスタンプを含ませる。

  • --trace

ファイルに注釈を付けられない場合は、例外メッセージだけでなく、スタックトレース全体を表示する。

  • -I, --ignore-columns REGEX

与えられた正規表現に合致するカラムに注釈をつけないようにする(例: annotate -I '^(id|updated_at|created_at)')。

  • --ignore-routes REGEX

与えられた正規表現に合致する routes に注釈をつけないようにする(例: annotate -I '(mobile|resque|pghero)')。

  • --hide-limit-column-types VALUES

与えられたカラムタイプに limit 値を表示させない。カラムタイプはコンマで区切る(例: integer,boolean,text)。

  • --hide-default-column-types VALUES

与えられたカラムタイプに default 値を表示させない。カラムタイプはコンマで区切る(例: json,jsonb,hstore)。

  • --ignore-unknown-models

不正なモデルファイルに対して警告を表示させない。

  • --with-comment

モデルの注釈にデータベースコメントを含める。

オプション: additional_file_patterns

CLI: --additional-file-patterns

Ruby: :additional_file_patterns

注釈を行うために追加のパスを提供します。このパスは glob を含むことができます。絶対パスを使用することを推奨します。下記に例を記載します。

  • /app/lib/decorates/%MODEL_NAME%/*.rb
  • /app/lib/forms/%PLURALIZED_MODEL_NAME%/**/*.rb
  • /app/lib/forms/%TABLE_NAME%/*.rb

適切なモデルは %*% 構文を用いて推論され、一致するファイルに注釈が付けられます。これは既存のファイル名解決とともに動作します(annotate_models.rbresolve_filename メソッドの中で発見されるオプション)。

Rails の設定の中で使用するときは、下記を使用できます。

File.join(Rails.application.root,
'app/lib/forms/%PLURALIZED_MODEL_NAME%/***/**.rb')

ソート

デフォルトでは、カラムはデータベース順でソートされます(つまりマイグレーションが行われた順番)。

もしアルファベット順にソートしてマイグレーションを実行した順番とは関係なく注釈の結果を一致させたいときは、--sort オプションを使用してください。

マークダウン

生成されるフォーマットは実際には MultiMarkdown で、テーブルのための構文拡張機能を利用しています。もしこのフォーマットを使用したいときはパーサーとして kramdown を使用することを推奨しています。もしドキュメントを生成するために yard を使用している場合は、.yardopts ファイルに kramdown を追加することでプロバイダとして kramdown をマークダウンのフォーマットに指定してください。

--markup markdown
--markup-provider kramdown

Gemfile にも同様に kramdown を追加するようにしてください。

gem 'kramdown', groups => [:development], require => false

警告

自動作成されたコメントブロックの後にテキストを追加しないでください。annotate によって以前にコメントブロックが追加された可能性があるとき、annotate はモデル内の始端・終端のコメントブロックを削除することがあります。

annotate が行った変更を必ず確認するようにしてください。Git を使用している場合は、annotate を実行した後にプロジェクトのステータスを確認することができます。

$ git status

VCS(Git や Subversion など)を使っていない人は、特に注意して annotate を扱い、1つの VCS の利用を検討してください。

リンク集

ライセンス

Ruby と同じライセンスでリリースしています。サポートと保証はありません。

作者

AUTHORS.md をご覧ください。

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

undefined method 'page' for # <Array:0x000........>が出たときの対処法

はじめに

gem kaminariについて、新しい発見があったので、記録として残す。

今回のエラー

controller.rb
インスタンス変数 = オブジェクト.page(:params[:page]).per(10)
index.html.haml
= paginate インスタンス変数

と定義したところ、エラーが出た。

エラー文

undefined method 'page' for # <Array:0x000........>

なぜ???

実は、、、

私の解釈では、、

controller.rb
インスタンス変数 = オブジェクト.page(:params[:page]).per(10)

のつもりだったのが、

controller.rb
インスタンス変数 = 配列.page(:params[:page]).per(10)

の間違いだった。

解決策

kaminariの公式ページを読めばわかるのだが、

通常.pageメソッドはActive Recordのオブジェクトにしか適用されないらしい。

今回はそうではなく、配列(Array)だったのでエラーが出ていたようだ。

そこでkaminariでは、配列にも.pageメソッドを使用できるやり方がある。

controller.rb
インスタンス変数 = Kaminari.paginate_array(配列).page(params[:page]).per(10)

これで解決した。

おわりに

あまり深く理解せず使用していたジェムがたくさんあるが、こういうエラーがきっかけで、
改めて公式レファレンスを読むことの重要性を感じた。

公式レファレンスを読み進めていくと、新しい発見もたくさんあるので非常に楽しいので、皆さんもいろんなgemの公式レファレンスを読んでみてほしい。

参考記事

公式レファレンス
https://github.com/kaminari/kaminari/blob/master/README.md

kaminariでundefined method `page' for #<Array:0x000xxxxxxと出た
https://haayaaa.hatenablog.com/entry/2019/03/11/215042

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

rails newするとCould not load command "rails/commands/server/server_command". Error: uninitialized constant URI::Generic.と表示される時の対処法

Vagrantを使って環境構築をしている際、エラーに遭遇したので記録を残しておきます。

構築環境

Ruby 2.5.0
Ruby on rails 5.1.7
Vagrant 2.2.7
Virtual Box 6.1.4
rbenv 1.1.2

発生したエラー

$ rails new
#省略
[WARNING] Could not load command "rails/commands/server/server_command". Error:
uninitialized constant URI::Generic.

解決した方法

Rubyのバージョンを2.5.7に上げるとこのエラーは解消されました。

#バージョン2.5.7のRubyをインストール
$ rbenv install 2.5.7
#省略

#使用する全体のrubyのバージョンを指定
$rbenv global 2.5.7

#Rubyのバージョンを確認
$ruby -v
ruby 2.5.7p206 (2019-10-01 revision 67816) [x86_64-linux]

$rails new
#うまくいきました

https://stackoverflow.com/questions/59961343/failing-to-start-up-default-rails-server
この質問を参考にしてバージョンを変えてみたんですが、よく読むと2.5.7でも同じ現象が起こったって書かれてますね...。原因が分かる方は教えていただきたいです。

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

アカウントの有効化〜Railsチュートリアル11章〜

いよいよRailsチュートリアル11章に入っていきます。11章の体感としてはProgateの内容でも少し触った内容であったので割とスムーズにすすめることができました。
それではまとめていきます。

アカウントの有効化

現時点でのApplicationは新規登録したユーザーははじめからすべての機能にアクセスできるようになっている。
ここではアカウントを有効化するステップを新規登録の途中に差し込むことで本当にそのメールアドレスの持ち主なのかどうかを確認できるようにする。
大まかな流れ
(1)有効化トークンやダイジェストを関連付けておいた状態で
(2)有効化トークンを含めたリンクをユーザーにメールで送信し
(3)ユーザーがそのリンクをクリックすると有効化できるようにする
というものである。

基本的な手順

1.ユーザーの初期状態は「有効化されていない」にしておく
2.ユーザー登録が行われたときに有効化トークンとそれに対応する有効化ダイジェストを生成する。
3.有効化ダイジェストはデータベースに保存しておき、有効化トークンはメールアドレスと一緒にユーザーに送信する有効化用メールのリンクに仕込んでおく
4.ユーザーがメールのリンクをクリックしたらApplicationはメールアドレスをキーにしてユーザーを探し、データベース内に保存しておいた有効化ダイジェストと比較することでトークンを認証する。
5.ユーザーが認証できたら、ユーザーのステータスを「有効化されていない」から「有効化済み」に変更する。

AccountActivationsリソース

Session機能を使ってアカウントの有効化という作業を「リソース」としてモデル化する。アカウントの有効化リソースはActive Recordのモデルとは関係ないので両者を関連付けることはしない。その代わりにこの作業に必要なデータ(有効化トークンや有効化ステータス)をUserモデルに追加する。

普段のリソースとは異なる点
有効化用のリンクにアクセスして有効化のステータスを変更する部分では、RESTのルールに従うとPATCHリクエストとUpdateアクションになるべきである。しかし有効化リンクはメールでユーザーに送られる。ユーザーがこのリンクをクリックすればそれはブラウザで普通にクリックしたときと同じであり、その場合ブラウザから発行されるのはGETリクエストになってしまう。このためユーザーからのGETリクエストを受けるためにEDITアクションに変更して使っていく。

アカウント有効化に使うリソースを追加する

Rails.application.routes.draw do
resources :account_activations, only: [:edit]
↑
#有効化のメールにedit_account_activation_url
(activation_token, ...)
を使用したいのでeditアクションへ名前付きルートが必要になる。
そこで上記のResourcesを追加する。

AccountActivationのデータモデル

有効化のメールには一意の有効化トークンが必要であるも送信メールとデータベースのそれぞれに同じ文字列をおいておく方法だと情報漏えいが起こった際多大な被害につながる。
そこでデータベースに仮想的な属性をつけてハッシュ化した文字列をデータベースに保存するようにする。具体的には仮想属性の有効化トークンにアクセスし、user.activation_tokenでユーザーを認識できるようにする。

$ rails generate migration add_activation_to_users \
> activation_digest:string activated:boolean activated_at:datetime
↑をデータベースにmigrateする。

アクティブトークンのコールバック

ユーザーが新しい登録を完了するためには必ずアカウントの有効化が必要になるので有効化トークンや有効化ダイジェストはユーザーオブジェクトが作成される前に作成しておく必要がある。
そこでbefore_createコールバックが必要となる。

before_create :create_activation_digest

上のコードはメソッド参照と呼ばれるもので、こうするとRailsはcreate_activation_digestというメソッドを探し、ユーザーを作成する前に実行するようになる。create_activation_digestメソッド自体はUserモデル内でしか使わないので、外部に公開する必要はない。
よってprivateキーワードを指定してこのメソッドを隠蔽する。

self.activation_token  = User.new_token
self.activation_digest = User.digest(activation_token)

このコードを9章で作成した永続Sessionのためのユーザー登録した時と比べる。
9章では記憶トークンとダイジェストはすでにデータベースにいるユーザーのために作成されるのに対し、before_createコールバックはユーザーが作成される前に呼び出されることなので更新される属性がまだない。
このコールバックがあることでUser.newで新しいユーザーが定義されるとactivation_token属性やactivation_digest属性が得られるようになる。

class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save   :downcase_email
  before_create :create_activation_digest
  validates :name,  presence: true, length: { maximum: 50 }
  .
  .
  .
  private

    # メールアドレスをすべて小文字にする
    def downcase_email
      self.email = email.downcase
    end

    # 有効化トークンとダイジェストを作成および代入する
    def create_activation_digest
      self.activation_token  = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
end

アカウント有効化のメール送信

データのモデル化が終わったのでアカウント有効化メールの送信に必要なコードを追加する。このメソッドではActionMailerライブラリを使ってUserのメイラーを追加する。メイラーは、モデルやコントローラと同様にrails generateで生成できる。メイラーの構成はコントローラのアクションとよく似ている。テンプレートはビューと同じようなもの。このテンプレートの中に有効化トークンとメールアドレス (= 有効にするアカウントのアドレス) のリンクを含め、使っていく。

Userメイラーで
account_activationメソッドと、第12章で必要となるpassword_resetメソッドを生成する。
生成したメイラーごとにビューのテンプレートが2つずつ生成される。
1つはテキストメール用のテンプレート
1つはHTML用のテンプレートである。

生成されたApplicationメイラーにはデフォルトのformアドレスがある。

最初に生成されたテンプレートをカスタマイズして実際に有効化メールで使えるようにする。

app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: "noreply@example.com"
  layout 'mailer'
end

次にユーザーを含むインスタンス変数を作成してビューで使えるようにし、user.emailにメール送信を行う。subjectキーはmailの件名に当たる。

class UserMailer < ApplicationMailer

  def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
  end

edit_account_activation_url(@user.activation_token, ...)
ここで思い出してみる。

edit_user_url(user)
上のメソッドは、次の形式のURLを生成します。

http://www.example.com/users/1/edit

これに対応するアカウント有効化リンクのベースURLは次のようになります。

http://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit

クエリパラメータを使って、このURLにメールアドレスもうまく組み込んでみましょう。クエリパラメータとは、URLの末尾で疑問符「?」に続けてキーと値のペアを記述したものです。

account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com

このとき、メールアドレスの「@」記号がURLでは「%40」となっている点に注目してください。これは「エスケープ」と呼ばれる手法で、通常URLでは扱えない文字を扱えるようにするために変換されています。Railsでクエリパラメータを設定するには、名前付きルートに対して次のようなハッシュを追加します。

edit_account_activation_url(@user.activation_token, email: @user.email)

送信メールのプレビュー

Railsでは、特殊なURLにアクセスするとメールのメッセージをその場でプレビューすることができます。

 development環境のメール設定
config/environments/development.rb
Rails.application.configure do
  .
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :test
  host = 'example.com' # 自分のクラウドIDEのリンクを貼る
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }
  .
end
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview

  # Preview this email at
  # http://localhost:3000/rails/mailers/user_mailer/account_activation
  def account_activation
    user = User.first
    user.activation_token = User.new_token
    UserMailer.account_activation(user)
  end

  # Preview this email at
  # http://localhost:3000/rails/mailers/user_mailer/password_reset
  def password_reset
    UserMailer.password_reset
  end
end

メールtestの実装

test/mailers/user_mailer_test.rb
require 'test_helper'

class UserMailerTest < ActionMailer::TestCase

  test "account_activation" do
    user = users(:michael)
    user.activation_token = User.new_token
    mail = UserMailer.account_activation(user)
    assert_equal "Account activation", mail.subject
    assert_equal [user.email], mail.to
    assert_equal ["noreply@example.com"], mail.from
    assert_match user.name,               mail.body.encoded
    assert_match user.activation_token,   mail.body.encoded
    assert_match CGI.escape(user.email),  mail.body.encoded
  end
end

アカウントの有効化

AccountActivationsコントローラのeditアクションを書いていく。

authenticated?メソッドの抽象化

app/models/user.rb

class User < ApplicationRecord
  .
  .
  .
  # トークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
  .
  .
  .
end
module SessionsHelper
  .
  .
  .
  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(:remember, cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end``

アカウントを有効化するeditアクション

app/controllers/account_activations_controller.rb
class AccountActivationsController < ApplicationController

  def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.update_attribute(:activated,    true)
      user.update_attribute(:activated_at, Time.zone.now)
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
  end
end

本日はここまで

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

[Rails][data-vocabulary.org スキーマのサポートは終了します。]gem Gretelをschema.orgに対応させる方法。

data-vocabulary.org スキーマのサポートは終了します。

スクリーンショット 2020-03-10 11.47.24.png

2020年4月6日からGoogleで「data-vocabulary.org」を利用した構造化データがリッチリザルトとしてサポートされなくなるので、引き続きリッチリザルトを利用したい場合は「schema.org」を利用した構造化データに移行する必要があります。

パンくずリスト  |  Google 検索デベロッパー ガイド  |  Google Developers
https://developers.google.com/search/docs/data-types/breadcrumb?hl=ja

Gem Gretelとは

Gretel( https://rubygems.org/gems/gretel/versions/3.0.7 )は設定ファイルを書くことで、簡単にパンクズリストを出力することができるようになるGemです。
パンクズの出力のオプションでsemantic: trueをつけることによりリッチリザルトにも対応していました(data-vocabulary.org)。

しかし、このGemはメンテナンスが長いこと止まっており、Googleの「schema.org」を利用するにはパッチを当てる必要がります。
自分で検索した所同じような対応をしている人が散見されたので、僕の方法を示して行こうと思います。

※ 本来は別のGemなどに移行することをオススメしますが、昔から使っている人がお手軽に対応できる方法を本記事にしています。

モンキーパッチを当てる

Gretel Gemを使っているRailsプロジェクトで config/initializers/gretel.rb を作成して

ruby
# frozen_string_literal: true

module Gretel
  module ViewHelpers
    delegate :breadcrumbs_json_ld, to: :gretel_renderer
  end

  class Renderer
    # rubocop:disable Rails/OutputSafety
    def breadcrumbs_json_ld
      {
        "@context": 'http://schema.org/',
        "@type": 'BreadcrumbList',
        "itemListElement": links.map.with_index do |link, i|
          {
            '@type': 'ListItem',
            'position': i + 1,
            'item': {
              '@id': "#{root_url.chop}#{link}",
              'name': link.text
            }
          }
        end
      }.to_json.html_safe
    end
    # rubocop:enable Rails/OutputSafety
  end
end

Yamitake gist gretel.rb https://gist.github.com/yamitake/3659b9d87404ad975f8a881b05971e33

使い方

breadcrumbs_json_ldを宣言したので、viewファイル側で下記のように宣言することにより、schema.orgに対応したjsonLDが出力されます。

.breadcrumbs
  == breadcrumbs
  = tag.script(breadcrumbs_json_ld, type: 'application/ld+json')

おわりに

今までGretelで開発してきた人が暫定対応としては上記の方法でお手軽に対応できますが、gretelはメンテナンスが長いことされていないので別のGemを探すか自前でパンクズの実装をした方がいいと思います。

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

Ruby で Google Spreadsheets を機械が読み書き

TL; DR

  • Google Spreadsheets の表を Ruby プログラムを使って無人操作する方法を書きました。
  • ほぼ 先日書いた Python の記事 の Ruby 版です。
  • 公式 Quick Start は人間が介在する OAuth2 のやり方だけど、ようはこれの無人版です。
  • サービスアカウントというものを作って、それに必要な権限を与え、そのアカウントの秘密鍵を使ってアクセスします。

スプレッドシートの準備

これは何も特別なことはありません。Google Drive に適当なシートを作成して、ファイルの ID だけ控えておいてください。

プロジェクトとサービスアカウント

各種ドキュメントを操作するには当然権限が必要です。プログラムから操作する場合は、大雑把に言って2とおりの権限獲得の手法があります。

  • 一時的に人間の許可を得て、その人間のアカウントで操作
  • 権限を与えられた 機械ようのアカウント で操作

ざっくり言うと前者はインタラクティヴなソフトで使う方法で、後者は自動化システムで使う方法です。今回は後者の方法を使います。ここで「機械ようのアカウント」を サービスアカウントと言います。つまり、まずはサービスアカウントを作り、必要な権限を与える必要がります。だいたい以下の手順です。

  1. GCPのコンソール に行って、プロジェクトを作ります。
  2. GCPの左上のハンバーガーメニュー > IAMと管理 > サービスアカウント > サービスアカウントを作成
  3. 入力は必須項目だけで良いと思います。name@project.iam.gserviceaccount.com というアカウントができます。
  4. アカウントを作ると秘密鍵 (private key) をダウンロードできると思います。JSON形式でダウンロードしてください。
  5. このアカウントに対して、操作したいファイル(スプレッドシート)の操作権限を与えてください(=共有してください)。与え方は通常の人間向けの権限操作といっしょです。
  6. ハンバーガーメニュー > APIとサービス > ダッシュボード > +APIとサービスを有効化 と進み、Google Sheets API を有効化してください。

パッケージのインストール

たぶんこれだけでいいはず。

$ gem install google-api-client

コード

シートの 1 行目を読み出し、最終行に don't, panic, 42 と追記するコードです。

require "google/apis/sheets_v4"

Sheet_id = "*******"  # スプレッドシートの(ファイルの)ID

service = Google::Apis::SheetsV4::SheetsService.new
service.authorization = Google::Auth::ServiceAccountCredentials.make_creds(
  json_key_io: File.open("ダウンロードした秘密鍵.json"),
  scope: Google::Apis::SheetsV4::AUTH_SPREADSHEETS
)

# データの読み出し例
resp = service.get_spreadsheet_values(Sheet_id, "シート1!a1:z1")
p resp.values

# 行の追記例
service.append_spreadsheet_value(
  Sheet_id, "シート1!a:c",
  {"values": [["don't", "panic", 42]]},
  value_input_option: "RAW"      # 式を入れたいときは "USER_ENTERED"
)

終わりに

append_spreadsheet_value はIoT機器ぽいものでログ等をどんどん追記する用途には便利だと思います。

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

Ruby と Perl で解くAtCoder ABC 161 D 幅優先探索

はじめに

AtCoder の AtCoder Beginner Contest 161 の D問題が解けなかったので、復習を兼ねて投稿しました。

お礼

オフィシャルの解説やネット上の解説・解答を参照して理解を進めております。
AtCoder さん、競技プロプレイヤーさん、ありがとうございます。

幅優先探索

幅優先探索といえば、次のようなマップ・迷路問題で使用されます。

#...#.#
..#...#
.#..#..

所謂、ある地点での上下左右において、. だったらキューに追加、# だったらキューに追加しない、というふうな解法を行います。

今回の D問題はそれを応用させ、末尾の数字 (仮に 3とします) のルンルン数 (2, 3, 4) をキューに追加することにより解いていきます。

また、マップ・迷路問題ではゴールにたどり着いた時点もしくは、全マップを探索した時点でループを終了しますが、今回は K番目に到達した時点でループを終了します。

Ruby

AtCoder Beginner Contest D - Lunlun Number

Ruby.rb
k = gets.chomp.to_i
cnt = 0
que = []
(1..9).each do |i|
  que.push(i)
  cnt += 1
  if cnt == k
    puts i
    exit
  end
end
while que.size > 0 do
  n = que.shift
  if n % 10 == 0
    cnt += 1
    if cnt == k
      puts n * 10
      exit
    end
    que.push(n * 10)
    cnt += 1
    if cnt == k
      puts n * 10 + 1
      exit
    end
    que.push(n * 10 + 1)
  end
  if n % 10 != 0 && n % 10 != 9
    cnt += 1
    if cnt == k
      puts n * 10 + n % 10 - 1
      exit
    end
    que.push(n * 10 + n % 10 - 1)
    cnt += 1
    if cnt == k
      puts n * 10 + n % 10
      exit
    end
    que.push(n * 10 + n % 10)
    cnt += 1
    if cnt == k
      puts n * 10 + n % 10 + 1
      exit
    end
    que.push(n * 10 + n % 10 + 1)
  end
  if n % 10 == 9
    cnt += 1
    if cnt == k
      puts n * 10 + 8
      exit
    end
    que.push(n * 10 + 8)
    cnt += 1
    if cnt == k
      puts n * 10 + 9
      exit
    end
    que.push(n * 10 + 9)
  end
end

キューに push して while で回して shift で取り出す要領で、幅優先探索を実行します。

Perl

Perl.pl
use v5.18; # strict say state
use warnings;
use List::Util qw(reduce first max min sum0);

chomp (my $k = <STDIN>);
my @que;
my $cnt;
for my $i (1..9) {
  push @que, $i;
  $cnt++;
  if ($cnt == $k) {
    say $i;
    exit;
  }
}
while (@que) {
  my $i = shift @que;
  if ($i % 10 == 0) {
    $cnt++;
    if ($cnt == $k) {
      say $i.'0';
      exit;
    }
    push @que, $i.'0';
    $cnt++;
    if ($cnt == $k) {
      say $i.'1';
      exit;
    }
    push @que, $i.'1';
  } elsif ($i % 10 != 0 && $i % 10 != 9) {
    $cnt++;
    if ($cnt == $k) {
      say $i.($i % 10 - 1);
      exit;
    }
    push @que, $i.($i % 10 - 1);
    $cnt++;
    if ($cnt == $k) {
      say $i.($i % 10);
      exit;
    }
    push @que, $i.($i % 10);
    $cnt++;
    if ($cnt == $k) {
      say $i.($i % 10 + 1);
      exit;
    }
    push @que, $i.($i % 10 + 1);
  } else {
    $cnt++;
    if ($cnt == $k) {
      say $i.'8';
      exit;
    }
    push @que, $i.'8';
    $cnt++;
    if ($cnt == $k) {
      say $i.'9';
      exit;
    }
    push @que, $i.'9';
  }
}

Perl の方は、数字を文字としても扱う様なコーディングにしています。

Perl(文字) Perl(数字) Ruby
実行時間 56 ms 42 ms 25 ms

ここでは、Ruby が速い結果となりました。

まとめ

  • ABC 161 D を解いた
  • 幅優先探索の応用ができるようになった

参照したサイト
ABC 161 解説
D言語で解く AtCoder Beginner Contest 161 (A〜D)
shift, unshift, pop, pushまとめ

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

Twitterユーザーの性別を機械学習で予測する

性別がタグ付けされたTwitterユーザー 2万人分のデータがあったので、このデータを使ってTwitterユーザーの性別予測を行ってみた。テキスト処理にはRuby、機械学習にはPythonを使っている。

先に結論

Twitterのプロフィールを用いた単純な機械学習による性別予測は、約60%の精度しかでなかった。

今回用いたデータは外国語のデータであり、日本語のプロフィールだと違った結果になるが、精度は同じようにあまりでないと思われる。こう思う理由は、「Twitterユーザーのデータは、そもそも人が見ても性別の判定が難しい」ため。

Twitterユーザー性別判定の手順

手順1〜5にはRuby、手順6にはPythonを使っている。

  1. プロフィールに含まれる単語をリストアップする
  2. 各単語の出現回数を記録する
  3. 極端に出現回数が少ない、もしくは多すぎる単語は除去する
  4. 単語数を次元数とみなしてユーザーのプロフィールをベクトルで表現する
  5. 正解データのラベルを作成する
  6. 機械学習を適用する

Rubyでテキストの事前処理を行う

前述の手順1〜5を行うRubyコードは下記の通り。今回のような手法だと、このテキスト処理の部分に性能が大きく左右される。やり方は無数にあり、このコードでは本当に最小限のテキスト処理しか行っていない。

# https://www.kaggle.com/crowdflower/twitter-user-gender-classification
def parse_kaggle_data
  str = File.read('gender-classifier-DFE-791531.csv', encoding: 'ISO-8859-1:UTF-8')
  lines = str.split("\r").map { |l| l.split(',') }
  header = lines[0]
  users = lines.drop(1).map { |l| header.map.with_index { |h, i| [h, l[i]] }.to_h }
  users = users.select { |u| %w(female male).include?(u['gender']) && u['gender:confidence'] == '1' }
  [users.map { |u| u['description'] }, users.map { |u| u['gender'] }]
end

def split_to_words(text_array)
  text_array.map { |d| d.split(/([\s"]|__REP__)/) }.flatten.
      map { |w| w.gsub(/^#/, '') }.
      map { |w| w.gsub(/[^.]\.+$/, '') }.
      map { |w| w.gsub(/[^!]!+$/, '') }.
      map { |w| w.gsub(/^\(/, '') }.
      map { |w| w.gsub(/^\)/, '') }.
      delete_if { |w| w.length < 2 }.
      map(&:downcase).sort.uniq
end

def count_words(text_array, word_array)
  words_count = Hash.new(0)
  text_array.each do |d|
    word_array.each do |w|
      if d.include?(w)
        words_count[w] += 1
      end
    end
  end
  words_count
end

descriptions, genders = parse_kaggle_data

desc_words = split_to_words(descriptions)
desc_words_count = count_words(descriptions, desc_words)
filtered_desc_words = desc_words.select { |w| desc_words_count[w] > 2 && desc_words_count[w] < 500 }
desc_vectors = descriptions.map { |d| filtered_desc_words.map { |w| d.include?(w) ? 1 : 0 } }
File.write('data/description_vectors.txt', desc_vectors.map { |v| v.join(' ') }.join("\n"))

labels = genders.map do |g|
  case g
  when '';        0
  when 'brand';   1
  when 'female';  2
  when 'male';    3
  when 'unknown'; 4
  end
end
File.write('data/labels.txt', labels.join("\n"))

Pythonで機械学習を行う

ナイーブベイズ、ロジスティック回帰、ランダムフォレスト、サポートベクターマシンを試した結果、どれも似たような結果になっている。

手法 精度
ナイーブベイズ(正規分布) 0.5493
ナイーブベイズ(ベルヌーイ) 0.6367
ロジスティック回帰 0.6151
ランダムフォレスト 0.6339
サポートベクターマシン 0.6303

それぞれの手法には元データに対する暗黙の仮定があるが、今回はそれは考慮せず単純に結果を比較している点に注意が必要。

# sudo yum install -y python3
# sudo pip3 install -U pip numpy sklearn ipython

import numpy as np
from sklearn.naive_bayes import GaussianNB
from sklearn.naive_bayes import BernoulliNB
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, accuracy_score
from sklearn.metrics import confusion_matrix
import pickle

description_vectors = np.loadtxt('data/description_vectors.txt')
labels = np.loadtxt('data/labels.txt')

(x_train, x_test, y_train, y_test) = train_test_split(description_vectors, labels)

clf = GaussianNB().fit(x_train, y_train)
clf = BernoulliNB().fit(x_train, y_train)
clf = LogisticRegression().fit(x_train, y_train)
clf = RandomForestClassifier().fit(x_train, y_train)
clf = SVC(C = 1.0).fit(x_train, y_train)

y_pred = clf.predict(x_test)
np.mean(y_test == y_pred)

# Grid search

# best params: {'C': 1.0, 'gamma': 'scale', 'kernel': 'rbf'}
parameters = [{'kernel': ['linear', 'rbf', 'poly', 'sigmoid'], 'C': np.logspace(-2, 2, 5), 'gamma': ['scale']}]
clf = GridSearchCV(SVC(), parameters, verbose = True, n_jobs = -1)
clf.fit(x_train, y_train)

# best params: {'max_depth': 100, 'n_estimators': 300}
parameters = [{'n_estimators': [30, 50, 100, 300], 'max_depth': [25, 30, 40, 50, 100]}]
clf = GridSearchCV(RandomForestClassifier(), parameters, verbose = True, n_jobs = -1)
clf.fit(x_train, y_train)

print(clf.best_params_)
print(clf.best_score_)
print(clf.best_estimator_)

print(classification_report(y_test, y_pred))
print(accuracy_score(y_test, y_pred))
print(confusion_matrix(y_test, y_pred))

# Model persistence

pickle.dump(clf, open('model.sav', 'wb'))
clf = pickle.load(open('model.sav', 'rb'))

関連リンク

Twitter User Gender Classification | Kaggle
Using machine learning to predict gender

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

ユーザーの更新・表示・削除機能を追加します〜Railsチュートリアル10章〜

この章ではユーザー機能を充実させてRESTアクションを完成させます。
この章で行うことは
プロフィールの更新、認可モデルの実装。ユーザー一覧の追加。ユーザーの削除機能を追加していきます。

ユーザーを更新する

ユーザー情報を編集するパターンは新規ユーザーの作成と似通っている。
ユーザーを編集するためのeditアクションを作成する。
patchリクエストに応答するUpdateアクションを作成する。
またユーザー情報を更新できるのはそのユーザー自身だけであるよう設定する。

まずeditに対応するアクションとビューを追加する。

アクション

def edit
    @user = User.find(params[:id])
  end

ビュー

<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= f.label :name %>
      <%= f.text_field :name, class: 'form-control' %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit "Save changes", class: "btn btn-primary" %>
    <% end %>

    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails" target="_blank">change</a>
    </div>
  </div>
</div>

WebブラウザはネイティブではPATCHリクエスト (RESTの慣習として要求されている) を送信できないので、RailsはPOSTリクエストと隠しinputフィールドを利用してPATCHリクエストを「偽造」しています
ユーザーのeditビューで使われているtarget="_blank"ですが、これを使うとリンク先を新しいタブ(またはウィンドウ)で開くようになるので、別の Webサイトへリンクするときなどに便利です。

target="_blank"で新しいページを開くと、フィッシングサイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。対処方法は、リンク用のaタグのrel(relationship)属性に、"noopener"と設定します。

編集の失敗

編集に失敗した場合について扱う。まずupdateアクションの作成から始める。update_attributesを使って送信されたparamsハッシュに基いてユーザーを更新します。無効な情報が送信された場合、更新の結果としてfalseが返され、elseに分岐して編集ページをレンダリングします。

  def create
    @user = User.new(user_params)
    if @user.save
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  def edit
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      # 更新に成功した場合を扱う。
    else
      render 'edit'
    end
  end

編集失敗のtest

require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "unsuccessful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    patch user_path(@user), params: { user: { name:  "",
                                              email: "foo@invalid",
                                              password:              "foo",
                                              password_confirmation: "bar" } }

    assert_template 'users/edit'
  end
end

test内容
1 そのユーザーにとっての編集画面を開ける
2 user/editのビューが表示される
3 編集した内容(無効な情報)をpatchリクエストとして送る
4 user/editのビューが表示される

編集成功のtest

require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "successful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name:  name,
                                              email: email,
                                              password:              "",
                                              password_confirmation: "" } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end
end

このままでは長さに対するバリデーションが有効となっているためREDとなってしまう。パスワードのバリデーションに対して例外処理を加える。

ユーザーmodelsへ

validates :password, presence: true, length: { minimum: 6 }, allow_nil: true

を追加する、これは新規ユーザー登録時には有効とならない処理である。

認可

認証 (authentication) はサイトのユーザーを識別することであり、認可 (authorization) はそのユーザーが実行可能な操作を管理すること。
editアクションとupdateアクションはセキュリティ上欠陥がある。それはどのユーザーでもあらゆるアクションにアクセスできるため、誰でも (ログインしていないユーザーでも) ユーザー情報を編集できてしまう。そこでユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御する(こういったセキュリティ上の制御機構をセキュリティモデルと呼ぶ)。

まずログインしたユーザーが保護されたページへアクセスしようとした際にログインページへ転送する方法と許可されていないページに対しアクセスするログイン済みのユーザーにはルートURLにリダイレクトさせるようにする。

ユーザーに対しログインを要求する

Usersコントローラの中でbeforeフィルターを使い、転送させる仕組みを作る。

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]

 # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

デフォルトでは、beforeフィルターはコントローラ内のすべてのアクションに適用されるので、ここでは適切な:onlyオプション (ハッシュ) を渡すことで、:editと:updateアクションだけにこのフィルタが適用されるように制限をかけている。unlessは条件が偽の時に対応する処理が作動する。

editとupdateアクションの保護に対するtest

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "should redirect edit when not logged in" do
    get edit_user_path(@user)
    assert_not flash.empty?
    assert_redirected_to login_url
  end

  test "should redirect update when not logged in" do
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert_not flash.empty?
    assert_redirected_to login_url
  end
end

正しいユーザーを要求する

ログインを要求するだけでは不十分であり、ユーザーが自分の情報だけを編集できるようにする必要がある。そこでUserコントローラのtestを補完するようにtestを追加する。

まずfixtureファイルに2人目のユーザーを追加する。
次に9章で定義したlog_in_asメソッドを使ってeditアクションとupdateアクションをtestする。

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect edit when logged in as wrong user" do
    log_in_as(@other_user)
    get edit_user_path(@user)
    assert flash.empty?
    assert_redirected_to root_url
  end

  test "should redirect update when logged in as wrong user" do
    log_in_as(@other_user)
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert flash.empty?
    assert_redirected_to root_url
  end
end

@other_user(二人目のユーザー)を定義。
上段のtestは2人目のユーザーが1人目のユーザーのedit_user_pathに入った場合、フラッシュがでるか。ルートURLへリダイレクトするのかを確認している。
下段のtestは二人目のユーザーが一人目のユーザーの情報を変更する内容のpatchリクエストを送信した場合にフラッシュがでてルートURLへ値ダイレクトされるかを確認している。

下段のtestをパスするため、correct_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出せるようにする。

before_action :correct_user,   only: [:edit, :update]


    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless @user == current_user
    end

フレンドリーフォワーディング

これまでは保護されたページにアクセスしようとすると問答無用で自分のプロフィールページに移動させられてしまう。別の言い方をすればログインしていないユーザーが編集ページにアクセス使用としていたならば、ユーザーがログインした後にはその編集ページにリダイレクトされるようにするのが望ましい動作である。

フレンドドリーフォワーディングのtest

require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user)
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name:  name,
                                              email: email,
                                              password:              "",
                                              password_confirmation: "" } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end
end

編集URLへアクセス。ログインした時、編集ページにリダイレクトされているか?
失敗するテストが書けたので、ようやくフレンドリーフォワーディングを実装する準備ができた。ユーザーを希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要があります。この動作をstore_locationとredirect_back_orの2つのメソッドを使って実現する。これらのメソッドはSessionsヘルパーで定義しています

module SessionsHelper
  .
  .
  .
  # 記憶したURL (もしくはデフォルト値) にリダイレクト
  def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)
    session.delete(:forwarding_url)
  end

  # アクセスしようとしたURLを覚えておく
  def store_location
    session[:forwarding_url] = request.original_url if request.get?
  end
end

転送先のURLを保存する仕組みは、ユーザーをログインさせたときと同じで、session変数を使います。requestオブジェクトも使っています (request.original_urlでリクエスト先が取得できます)。store_locationメソッドでは、 リクエストが送られたURLをsession変数の:forwarding_urlキーに格納しています。ただし、GETリクエストが送られたときだけ格納するようにしておきます。これによって、例えばログインしていないユーザーがフォームを使って送信した場合、転送先のURLを保存させないようにできる。

ログインユーザー用beforeフィルターにstore_locationを追加する。

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  def edit
  end
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end
end

フォワーディング自体を実装するには、redirect_back_orメソッドを使います。リクエストされたURLが存在する場合はそこにリダイレクトし、ない場合は何らかのデフォルトのURLにリダイレクトします。デフォルトのURLは、Sessionコントローラのcreateアクションに追加し、サインイン成功後にリダイレクトします このコードは、値がnilでなければsession[:forwarding_url]を評価し、そうでなければデフォルトのURLを使っています。
またリストではsession.delete(:forwarding_url) という行を通して転送用のURLを削除している点にも注意してください。

すべてのユーザーを表示する

indexアクションを追加していく。ここではすべてのユーザーの一覧表示を行っていく。
データベースにサンプルデータを追加する方法
ユーザー出力のページネーション用リンクの追加をする

ユーザーの一覧ページを実装するためにまずはセキュリティモデルについて考える。ユーザーのshowページは今後もサイトを訪れた全てのユーザーから見えるようにするがindexページはログインしたユーザーにしか見せないようにし、未登録のユーザーがデフォルトで表示できるページを制限する。

indexページを不正なアクセスから守るためにindexアクションが正しくリダイレクトするか検証するtestを書いてみる。

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end

  test "should redirect index when not logged in" do
    get users_path
    assert_redirected_to login_url
  end
  .
  .
  .
end

次にbeforeフィルターのlogged_in_userにindexアクションを追加してこのアクションを保護する。

そしてすべてのユーザーを表示するために、全ユーザーが格納された変数を作成し、順々に表示するindexビューを実装します。

<% provide(:title, 'All users') %>
<h1>All users</h1>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

そしてユーザー一覧ページのリンクをheaderに更新して動かせるようにする。

サンプルユーザーの追加

今のままでは一人しかユーザー一覧がないため、ここからはサンプルのユーザーを追加する。
サンプルのユーザーを作成するにはGemfileにFakergemを追加する。

データベース上にサンプルユーザーを生成するRailsタスク

db/seeds.rb
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar")

99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password)
end

Example Userという名前とメールアドレスを持つ1人のユーザと、それらしい名前とメールアドレスを持つ99人のユーザーを作成します。

ページネーション

これで、最初のユーザーにも仲間ができたが、今度は逆に1つのページに大量のユーザーが表示されてしまっている。これを解決するのがページネーション (pagination) というもので、この場合は、例えば1つのページに一度に30人だけユーザーを表示するというものです。
これを使うためには、Gemfileにwill_paginate gem とbootstrap-will_paginate gemを両方含め、Bootstrapのページネーションスタイルを使ってwill_paginateを構成する必要があります。

gem 'will_paginate',           '3.1.6'
gem 'bootstrap-will_paginate', '1.0.0'

indexアクションにあるUser.allを、ページネーションを理解できるオブジェクトに置き換える必要もあります。まずは、ビューに特殊なwill_paginateメソッドを追加します。

app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

<%= will_paginate %>

indexアクションでUsersをページネートする。

  def index
    @users = User.paginate(page: params[:page])
  end

ユーザー一覧のtest

今回のテストでは、ログイン、indexページにアクセス、最初のページにユーザーがいることを確認、ページネーションのリンクがあることを確認、といった順でテストしていく。
まずFixtureで30人のユーザーを追加する。

次にページネーションを含めたUserIndexのtestを追加

test/integration/users_index_test.rb
require 'test_helper'

class UsersIndexTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "index including pagination" do
    log_in_as(@user)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    User.paginate(page: 1).each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
    end
  end
end

ユーザーの削除

ここではユーザーを削除するためのリンクを追加する。また削除に必要なdestroyアクションも実装する。しかしその前に、削除を実行できる権限をもつ管理(Admin)ユーザーのクラスを作成する。
まず特権を持つ管理ユーザーを識別するために論理値をとるadmin属性をUserモデルに追加する。こうすると自動的にadmin?メソッドも使えるようになるのでこれを使って管理ユーザーの状態をtestする。

destroyアクション

Userリソースの最後の仕上げとしてdestroyアクションへのリンクを追加する。まずユーザーindexページの各ユーザーに削除用のリンクを追加し続いて管理ユーザーへのアクセスを制限する。これにより現在のユーザーが管理者のときに限り[delete]リンクが表示されるようになる。

ユーザー削除用リンクの実装

app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, method: :delete,
                                  data: { confirm: "You sure?" } %>
  <% end %>
</li>

この削除リンクが動作するためには、destroyアクションを追加する必要があります。このアクションでは、該当するユーザーを見つけてActive Recordのdestroyメソッドを使って削除し、最後にユーザーindexに移動します。ユーザーを削除するためにはログインしていなくてはならないので、で:destroyアクションもlogged_in_userフィルターに追加しています。

destroyアクションでは、findメソッドとdestroyメソッドを1行で書くために2つのメソッドを連結 (chain) している。

結果として、管理者だけがユーザーを削除できるようになります (より具体的には、削除リンクが見えているユーザーのみ削除できる)。しかし、実はまだ大きなセキュリティホールがあります。ある程度の腕前を持つ攻撃者なら、コマンドラインでDELETEリクエストを直接発行するという方法でサイトの全ユーザーを削除してしまうことができるでしょう。サイトを正しく防衛するには、destroyアクションにもアクセス制御を行う必要があります。これを実装してようやく、管理者だけがユーザーを削除できるようにします。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  before_action :admin_user,     only: :destroy
  .
  .
  .
  private
    .
    .
    .
    # 管理者かどうか確認
    def admin_user
      redirect_to(root_url) unless current_user.admin?
    end
end

ユーザー削除のtest

Usersコントローラをテストするために、アクション単位でアクセス制御をテストします。削除をテストするために、DELETEリクエストを発行してdestroyアクションを直接動作させます。このとき2つのケースをチェックします。

・ログインしていないユーザーであれば、ログイン画面にリダイレクトされること。
・ログイン済みではあっても管理者でなければ、ホーム画面にリダイレクトされること。

管理者権限の制御をアクションレベルでテストする green
test/controllers/users_controller_test.rb

require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect destroy when not logged in" do
    assert_no_difference 'User.count' do
      delete user_path(@user)
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy when logged in as a non-admin" do
    log_in_as(@other_user)
    assert_no_difference 'User.count' do
      delete user_path(@user)
    end
    assert_redirected_to root_url
  end
end

assert_no_differenceメソッドを使って、ユーザー数が変化しないことを確認している点に注目してください。

このテストでは、管理者ではないユーザーの振る舞いについて検証していますが、管理者ユーザーの振る舞いと一緒に確認できるとよさそうです。そこで、管理者であればユーザー一覧画面に削除リンクが表示される仕様を利用して今回のテストを追加していくことにします。

assert_difference 'User.count', -1 do
  delete user_path(@other_user)
end

assert_differenceメソッドを使ってユーザーが作成されたことを確認しましたが、今回は同じメソッドを使ってユーザーが削除されたことを確認しています。具体的には、DELETEリクエストを適切なURLに向けて発行し、User.countを使ってユーザー数が
1
減ったかどうかを確認しています。

したがって、管理者や一般ユーザーのテスト、そしてページネーションや削除リンクのテストをすべてまとめると、次のようになります。

削除リンクとユーザー削除に対する統合テスト green

test/integration/users_index_test.rb
require 'test_helper'

class UsersIndexTest < ActionDispatch::IntegrationTest

  def setup
    @admin     = users(:michael)
    @non_admin = users(:archer)
  end

  test "index as admin including pagination and delete links" do
    log_in_as(@admin)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    first_page_of_users = User.paginate(page: 1)
    first_page_of_users.each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
      unless user == @admin
        assert_select 'a[href=?]', user_path(user), text: 'delete'
      end
    end
    assert_difference 'User.count', -1 do
      delete user_path(@non_admin)
    end
  end

  test "index as non-admin" do
    log_in_as(@non_admin)
    get users_path
    assert_select 'a', text: 'delete', count: 0
  end
end

となります。
本日はここまで。

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

Uglifier::Error: Unexpected character '`' Herokuデプロイ時のエラー解消方法

はじめに

今年の1月末からプログラミングの学習を開始したばかりの初心者です。
今回作成した個人アプリをHerokuでデプロイしてみたのですが、途中で発生したエラー解決について自分用の備忘録として初投稿してみました。

※今回はあくまで発生したエラーの解消法のみをシンプルにまとめています。
Herokuでのデプロイ方法については参考にした記事を最下部に貼っているので気になる方はそちらをご確認ください。

※開発環境
・Ruby 2.5.1
・Rails 5.2.4.2
・heroku/7.39.2 darwin-x64 node-v12.13.0

エラー内容


Herokuでデプロイ中に以下のようなエラー文言が表示されデプロイに失敗しました。

ターミナル
remote:  !
remote:  !     Precompiling assets failed.
remote:  !
remote:  !     Push rejected, failed to compile Ruby app.
remote: 
remote:  !     Push failed
remote: Verifying deploy...
remote: 
remote: !       Push rejected to (設定したアプリ名).
remote: 
To https://git.heroku.com/(設定したアプリ名).git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'https://git.heroku.com/(設定したアプリ名).git'

ターミナルを遡って見てみると、エラー原因について以下のような記述が。

ターミナル
remote:        rake aborted!
remote:        Uglifier::Error: Unexpected character '`'
remote:        --

エラー文言からバックフォート(`)がunexpectedだからダメだよ〜となっているんだなという事は何となく理解したものの、じゃあどうしたら良いのかは分からないので調べてみました。
(ちなみに自分はapp/assets/javascriptのファイル内でバックフォートを使った記述をしていました。)

解決方法


config/environments/production.rb内にある以下の記述をコメントアウトしてあげればOKでした。

production.rb
# config.assets.js_compressor = :uglifier     #この一文をコメントアウト

コメントアウトした後はGithubでコミット&プッシュをしてmasterに反映させます。
GitHub Desktopを使用している場合は、changeに変更内容が上がっていると思うのでコミット&プッシュ。

ターミナル上でコマンドを打つ場合は以下のように行う。
①コミットするchange(ファイル)の選択

ターミナル
$git add -A  #コミットするchangeの選択。 -Aは全部ということ

②選択したファイルのコミット&プッシュ

ターミナル
$git commit -m "コミットメッセージ"  #""内には変更内容が分かるメッセージを入力

これでmasterに変更が反映されたので、改めてHerokuにデプロイします。
以下のように表示され、無事にデプロイが完了しました!

ターミナル
remote: Verifying deploy... done.
To https://git.heroku.com/(設定したアプリ名).git
 * [new branch]      master -> master

最後に


production.rbファイルを変更した後、コミット&プッシュを忘れない!
初めこれをする前に再度デプロイをしてしまって全く同じエラーが出ました。。:sweat:

参考記事

Herokuでのデプロイ方法はこちらの記事を参考にさせてもらいました。
【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】

エラー解決時に参考にさせていただいた記事

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

【Rails】deviseを使用した簡単なログイン機能まとめ

はじめに

学習中の備忘録です。

概要

新規アプリ作成の際にdeviseのコマンドなど忘れるのでまとめ。

  • 導入
  • devise設定ファイル作成
  • モデル作成
  • ビューファイル作成
  • deviseによって設定されるPrefixの一部

前提

rails 5.2.3

導入

gemファイルに追記

Gemfile
gem 'devise'

インストール

ターミナル
$ bundle install

サーバー再起動

ターミナル
$ rails s

deviseの設定ファイルを作成

ターミナル
$ rails g devise:install

新規作成されるファイル

  • config/initializers/devise.rb
  • config/locales/devise.en.yml

モデル作成

ターミナル
$ rails g devise user

新規作成されるファイル

  • app/models/user.rb
  • db/migrate/20XXXXXXXXXXXX_devise_create_users.rb
  • test/fixtures/users.yml
  • test/models/user_test.rb

また、config/routes.rbに以下の様な記述が自動的に追記されます。

【例】config/routes.rb
Rails.application.routes.draw do
  devise_for :users
#以下略

devise_for :usersの記述により、ログイン・新規登録で必要なルーティングが生成されます。

作成されたmigrationファイルを実行

ターミナル
$ rails db:migrate

ビューファイル作成

ターミナル
$ rails g devise:views

新規作成されるファイル

  • app/views/devise以下のディレクトリにあるビューファイル各種

deviseによって設定されるPrefixの一部

リクエスト Prefix パス
devise/sessions#new new_user_session /users/sign_in
devise/registrations#new new_user_registration /users/sign_up
devise/sessions#destroy destroy_user_session /users/sign_out

あとは好きな場所に上記のリンクをはれば完成です。

まとめ

ユーザー情報の編集などは必要に応じて追記するかもです。

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