20200917のRubyに関する記事は25件です。

新人エンジニアが見返すべきGitコマンド 【随時更新】

いま自分がどこのブランチにいるか確認

$ git branch

これでブランチを作成&移動までできる。

$ git checkout -b [ブランチ名]

いま自分がいないブランチを削除することができる。

$ git branch -D [ブランチ名]

これで他のリポジトリのデータを取得する。リモートのデータを取得したければ、通常は[origin]でいける。

$ git fetch [リポジトリ]

だれかのブランチで作業したい・・・

$ git checkout [誰かが作業していたブランチ名]

これで直前のコミットを削除することができる。間違ってmasterにpushしてしまったときに使える。

$ git reset --hard HEAD^

PUSH。originはリモートリポジトリのこと。ブランチ名は作業しているブランチ名を入力すればいい。→このあとGIthubやGitlabでプルリクエストを作成する。

$ git push origin [ブランチ名]

変更したファイルをステージングさせる。という認識ではいるけど、奥が深そう。

$ git add -A

コミットするときに簡単にメッセージを入力できるやつ。

$ git commit -m “[コミットメッセージを書く]"

作業ツリーのファイルすべてをステージする

$ git add .

Git 管理対象のファイルの変更、削除をすべてステージする。(新規ファイルは無視。)

$ git add -u
$ git add --update

および作業ツリーのファイルをすべてステージする。引数なしで、すべての作業ツリーのファイルを反映。

$ git add --all
$ git add -A 

これで、bookmark.rbだけをステージングエリアに追加できる。

$ git add api/app/models/bookmark.rb

git addに関しての参考記事
https://www-creators.com/archives/4939

直前のコミットを取り消したいとき。 —hardとすることにより、変更した内容も取り消される。

この—hardの部分を —softにすると変更はそのままで、コミットだけ取り消される。

$ git reset --hard HEAD^

ローカルに最新のリーモートのmasterを取り込む

$ git pull origin master

★常にmasterブランチを新しい状態で作業をすることが大事、、、

そのあとにcheckoutで作業ブランチに移動する。そして

$ git merge origin master

をすると最新のmasterブランチの変更を取り込むことができる。
するとコンフリクトが起こる→コマンド+クリックでコンフリクトしているファイルを開ける。
どの変更を残すか、それとも両方の変更を適用するかを決めることができる。

随時更新していきます。
間違っている点などあればご指摘お願いします。

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

常に、自分が投稿したものをログインしている自分だけに表示したい

【概要】

1.結論

2.whereメソッドとは何か

3.どのように使うか

4.ここから学んだこと

1.結論

whereメソッドをつかう!

2.whereメソッドとは何か

欲しい条件を探すメソッドです!

モデル.where(条件名: 条件の内容)

で使用できます!

ブログや、ニュース記事での検索ワード欄にて特定の文字を探すプログラムを記載する時に頻繁に使います!


3.どのように使うか

自分は下記のように使用しました!

time_controller.rb
@times = Time.where(user_id: current_user.id).includes(:user).order("created_at DESC")

現在ログインしているユーザーが投稿した物を、投稿した人=ログインしているユーザー(current_user.id)のみに表示させたかったのでwhereメソッドを使用しました!
includes(:user)はN+1問題解決(プログラムの無駄な処理を軽減)し、order("created_at DESC")は新規投稿順にしています!

参考にしたURL:
[Rails]ログインユーザーidを使って、異なるテーブルに存在する値を出力したい[ActiveRecord]

5.ここから学んだこと(エラーの時に使用)

if条件式で場合分けしようとしましたが、いくら条件式作っても表示自体を制限しないと、条件式下で全部表示されてしまいます。whereメソッドは検索ワード欄を作りたいと意識するあまりに、本質は”表示を制限する”ことが抜けていました。具体的な事象➡︎抽象的な事象に変換するともっとメソッドに対しての活用視野が広がると確信しました。

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

RubyKaigi Takeout 2020 の延長線で盛り上がってた Symbol#to_s について

ブログ記事の転載です。

RubyKaigi Takeout 2020 お疲れ様でした。
今回はオンラインということではじめて参加したんですが興味深い内容の話があって楽しかったです。
特に sawanobori からのサンダル紹介、焚き火で飯を炊いて魚を焼くコミッタの日常配信はニューノーマルを感じました。
いやー Ruby コミッタともなると過酷な環境で開発する技術が必要なんですねー勉強になります!
あ、あと Refinements はとてもすばらしく最高な機能なので皆さんどんどん使っていってニューノーマルになっていきましょう。

さてさて、まだまだ RubyKaigi 熱は冷めやらぬのですが今回は 1日目の Ruby Committers vs the World で盛り上がってた Symbol#to_s の話について簡単にまとめてみました。

Symbol#to_s で frozen String を返す提案

ちょうど去年の今頃 #to_s で返す文字列を frozen にしようという提案がされました。

このチケットで本題の『Symbol#to_s の戻り値 frozen にしよう』という話が出てきました。
また、このチケットでは Symbol#to_s だけではなくて以下のメソッドも frozen String を返す変更が含まれています。

  • Module#name
  • TrueClass/FalseClass#to_s
  • NilClass#to_s

関連する PR や経緯などは以下のリンクを参考にしてください。

この変更はまつもとさんから承認され、マージ後に Ruby 2.7 preview2 で『実験的な機能』としてプレリリースされました。

Ruby 2.7 で変わるはず『だった』 Symbol#to_s

Symbol#to_s が frozen String を返すようになってパフォーマンスが上がってよかったよかった。
で、終わればよかったんですが Symbol#to_s の変更をマージしたらいずれかの gem で問題があることがわかりました。
例えば pry では次のように Symbol#to_s の戻り値を破壊的に変更するコードが書かれていて影響が出ていました。

def method_missing(method, *args, &block)
  meth = method.to_s
  if meth.end_with?('?')
    # to_s の戻り値を破壊的に変更しているので frozen String だとエラーになる…
    meth.chop!
    present?(meth) || present?(meth.tr('_', '-'))
  else
    super
  end
end

このように Symbol#to_s の戻り値を破壊的に変更しているコードはいくつか報告され、また Ruby 2.7 ではキーワード引数周りの対応もあったので混乱を避けるために Ruby 2.7 では Symbol#to_s の対応は見送られることとなりました。
また、Revert されたのは Symbol#to_s の変更だけで以下のメソッドは frozen String を返すように変更され Ruby 2.7 の機能としてリリースされました。

  • Module#name
  • TrueClass/FalseClass#to_s
  • NilClass#to_s

1年の時を経て…

先月開催された RubyKaigi Takeout 2020Ruby Committers vs the World の延長戦でちょうど Symbol#to_s の議論がされていました。
そこでも Symbol#to_s をどうするのかいろいろと議論されていたんですが 1つの結論として Symbol#to_s を非互換にするのではなくて Symbol#name という別名のメソッドを追加する流れとなりました。
そしてその当日に Symbol#name が開発版へとマージされました。勢いある。

#16150 の時系列

まとめ

と、言う感じで Symbol#to_s 問題について簡単にまとめてみました。
結果だけ見れば影響範囲が大きい非互換だったんですが、実際にリリースしてみるまでわからない問題が存在するのはある程度は仕方ないのでむずかしいですねえ。
むしろ今回のケースではプレリリース版で問題があることがわかったのでプレリリースとしては正しい動きをしていた例とも言えますね。
正式版で問題になる前にわかってよかったよかった。
みなさんも Ruby 3.0 preview 版が出たらどんどん使ってみて問題がないか試してみましょう。

関連

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

AWSを使ってアプリケーションを公開する手順(7)Capistranoによる自動デプロイ

はじめに

AWSを使ってアプリケーションを公開する手順を記載していく。
この記事ではCapistranoを使ってデプロイ作業を自動化する。

Capistranoの導入

CapistranoはRubyで書かれており、Gemが公開されている。
この記事ではrailsにCapistranoを導入する手順を記載するが、PHPなどの別のフレームワークでも使用できるらしい。

Capistrano関連のGemをインストールする

Gemfileを以下のように編集する。

Gemfile
group :development, :test do
  gem 'capistrano'
  gem 'capistrano-rbenv'
  gem 'capistrano-bundler'
  gem 'capistrano-rails'
  gem 'capistrano3-unicorn'
end

ターミナルで以下のコマンドを実行し、Gemfileを読み込む。

bundle install

以下のコマンドを実行し、Capistranoの関連ファイルを生成する。

bundle exec cap install

以下のファイルが生成される。各ファイルの詳細は後述する。

  • Capfile
  • config/deploy.rb
  • config/deploy/production.rb
  • config/deploy/staging.rb

Capfileを編集する

Capistranoを動作させるにはいくつかのライブラリ(Gem)を読み込む必要がある。Capfileとは、Capistrano関連のライブラリのうちどれを読み込むか指定するためのファイルである。

Capfileを以下のように編集する。
これによりデプロイに必要な動作が記述されたファイルが入ったディレクトリを読み込む。
参考

Capfile
require "capistrano/setup"
require "capistrano/deploy"
require 'capistrano/rbenv'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
require 'capistrano3/unicorn'

Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }

production.rbを編集する

config/deployディレクトリにproduction.rbとstaging.rbが作成された。
これらのファイルはデプロイについての設定を記載するファイルである。
production.rbは本番環境の設定ファイル、staging.rbはステージング環境の設定ファイルである。
production.rb(staging.rb)には下記の内容を記述する。

  • サーバホスト名
  • AWSサーバのログインユーザ名
  • サーバロール
  • SSHの設定
  • その他サーバに紐づく設定

production.rbを以下のように変更する。
(アプリケーションのElastic IPが12.345.67.890の場合)

config/deploy/production.rb
server '12.345.67.890', user: 'ec2-user', roles: %w{app db web}

開発環境、テスト環境、ステージング環境、本番環境とは

  • 開発環境
    • ローカルで動作確認などを行う。ここで問題なければテスト環境での検証を行う。
  • テスト環境
    • 誤記やリンクのミス、不具合がないかを検証する。
  • ステージング環境
    • 本番環境の前に動作や表示に問題がないかを検証する。テスト環境は本番環境とサーバの構成が異なるのに対し、ステージング環境は本番環境とサーバの構成が同じである。
  • 本番環境
    • ステージング環境で問題なければアップロードする。

deploy.rbを編集する

configディレクトリに作成されたdeploy.rbには本番環境、ステージング環境共通の設定を記述する。
具体的には以下を記述する。

  • アプリケーション名
  • gitのリポジトリ
  • 利用するSCM(Software Configuration Management,ソフトウェア構成管理)
  • タスク
  • それぞれのタスクで実行するコマンド

deploy.rbの記述を削除し、以下のように変更する。
(ここでは例としてCapistranoのバージョンが「3.11.0」、アプリケーション名が「testapp」、Githubのuser名が「test1234」、リポジトリ名が「testapp」、rubyのバージョンが「2.5.1」、ローカルPCのEC2インスタンスのSSH鍵(pem)へのパスが「~/.ssh/xxx.pem」とする。)

config/deploy.rb
# config valid only for current version of Capistrano
# capistranoのバージョンを記載。固定のバージョンを利用し続け、バージョン変更によるトラブルを防止する
lock '3.11.0'

# Capistranoのログの表示に利用する
set :application, 'testapp'

# どのリポジトリからアプリをpullするかを指定する
set :repo_url,  'git@github.com:test1234/testapp.git'

# バージョンが変わっても共通で参照するディレクトリを指定
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system', 'public/uploads')

set :rbenv_type, :user
set :rbenv_ruby, '2.5.1'

# どの公開鍵を利用してデプロイするか
set :ssh_options, auth_methods: ['publickey'],
                  keys: ['~/.ssh/xxx.pem'] 

# プロセス番号を記載したファイルの場所
set :unicorn_pid, -> { "#{shared_path}/tmp/pids/unicorn.pid" }

# Unicornの設定ファイルの場所
set :unicorn_config_path, -> { "#{current_path}/config/unicorn.rb" }
set :keep_releases, 5

# デプロイ処理が終わった後、Unicornを再起動するための記述
after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
  task :restart do
    invoke 'unicorn:restart'
  end
end

Capistranoのバージョン確認方法

gemfile.lockというファイルに記載されている。

DSL(Domain Specific Language)について

DSLとは、特定の処理の効率を上げるために擬似的に用意されるプログラムである。
例えば、set :name, 'value'という記述がある。このとき、fetch nameとすることで'value'を取り出すことができる。setした値はdeploy.rbやproduction.rbでも取り出すことができる。
また、task :xx do〜endという記述がある。これはCapfileでrequireしたものに加えてタスクを追加している。ここで記述したものはcap deploy時に実行される。

Capistranoによる自動デプロイ後のディレクトリ構成

Capistranoによる自動デプロイが実行されると、本番環境のディレクトリ構成が変化する。Capistranoによるアプリケーションのバックアップなど、複数のディレクトリが作成される。
例えば以下のようなディレクトリが作成される。

  • releasesディレクトリ
    • Capistranoを通じてデプロイされたアプリケーションはreleasesというディレクトリにまとめられる。ここに過去のアプリケーションが残っているため、デプロイ時に何か問題が発生した時、以前のバージョンに戻すことが可能。deploy.rbのset :keep_releasesの記述は保存しておく数を指定しており、今回は5回分のバージョンを保存しておくように設定している。
  • currentディレクトリ
    • releasesディレクトリの中で最新のものが自動的にこのディレクトリにコピーされる。つまり、このディレクトリにあるアプリケーションの内容が、現在デプロイされているアプリケーションの内容ということになる。
  • sharedディレクトリ
    • バージョンが変わっても共通で参照されるディレクトリが格納される。具体的には、log,public,tmp,vendorディレクトリが格納される。

unicorn.rbを編集する

Capistranoの導入によって、本番環境のディレクトリ構成が変わるのでそれに伴いunicorn.rbの記述も変更する。

unicorn.rbを以下のように変更する。

config/unicorn.rb
#サーバ上でのアプリケーションコードが設置されているディレクトリを変数に入れておく
#変更:階層を一個深くする
app_path = File.expand_path('../../../', __FILE__)

#アプリケーションサーバの性能を決定する
worker_processes 1

#アプリケーションの設置されているディレクトリを指定
#変更:currentを指定
working_directory "#{app_path}/current"

#Unicornの起動に必要なファイルの設置場所を指定
#変更:sharedディレクトリ追記
pid "#{app_path}/shared/tmp/pids/unicorn.pid"

#ポート番号を指定
#変更:sharedディレクトリ追記
listen "#{app_path}/shared/tmp/sockets/unicorn.sock"

#エラーのログを記録するファイルを指定
#変更:sharedディレクトリ追記
stderr_path "#{app_path}/shared/log/unicorn.stderr.log"

#通常のログを記録するファイルを指定
#変更:sharedディレクトリ追記
stdout_path "#{app_path}/shared/log/unicorn.stdout.log"

#Railsアプリケーションの応答を待つ上限時間を設定
timeout 60

preload_app true
GC.respond_to?(:copy_on_write_friendly=) && GC.copy_on_write_friendly = true

check_client_connection false

run_once = true

before_fork do |server, worker|
  defined?(ActiveRecord::Base) &&
    ActiveRecord::Base.connection.disconnect!

  if run_once
    run_once = false # prevent from firing again
  end

  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exist?(old_pid) && server.pid != old_pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH => e
      logger.error e
    end
  end
end

after_fork do |_server, _worker|
  defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end

Nginxの設定ファイル(rails.conf)を変更する

ディレクトリ構成が変わったのでrails.confも以下のように変更する。
(アプリケーション名が「testapp」、Elastic IPが「12.345.67.890」の場合を例として記載する)

rails.conf
upstream app_server {
  # sharedの中を参照するよう変更
  server unix:/var/www/testapp/shared/tmp/sockets/unicorn.sock;
}

server {
  listen 80;
  server_name 12.345.67.890;

# クライアントからアップロードされてくるファイルの容量の上限を2ギガに設定。デフォルトは1メガなので大きめにしておく
  client_max_body_size 2g;

  # currentの中を参照するよう変更
  root /var/www/testapp/current/public;

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
    # currentの中を参照するよう変更
    root   /var/www/testapp/current/public;
  }

  try_files $uri/index.html $uri @unicorn;

  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://app_server;
  }

  error_page 500 502 503 504 /500.html;
}

Nginxの設定を変更したらEC2インスタンスにSSH接続し、
下記のコマンドを実行して再読み込み・再起動を行う。

sudo service nginx reload
sudo service nginx restart

MySQLを再起動する

MySQLが立ち上がっていないとデプロイできないので、
下記のコマンドを実行し念のためMySQLも再起動する。

sudo service mysqld restart

unicorn masterのプロセスをkillする

自動デプロイを実行する前にunicorn masterのプロセスをkillしておく。
まずは下記のコマンドを実行しunicorn masterのプロセスIDを確認する。

ps aux | grep unicorn

下記のコマンドで確認したプロセスをkillする。
(今回はunicorn masterのプロセスIDが17877だったとする)

kill 17877

ローカルでの修正内容をmasterにプッシュする

今回編集したファイルを全てmasterブランチにプッシュしておく。

自動デプロイを実行する

ローカル環境で下記のコマンドを実行し自動デプロイを行う。
エラーがでなければ成功。

bundle exec cap production deploy

エラーが出た時に確認すること

  • もう一度実行する
  • 記述ミスがないか
  • 手順を飛ばしていないか

ブラウザで確認する

ブラウザのURL欄にElastic IPを入力するとアプリケーションにアクセスできる(:3000をつける必要はない)。

エラーが出た時に確認すること

  • 開発環境でエラーが出ていないか
  • /var/www/testapp/current/log/unicorn.stderr.logでエラーがないか(リポジトリ名が「testapp」の場合)
  • プッシュやプルを忘れていないか
  • MySQLやNginxの再起動を行ってみる
  • EC2インスタンスの再起動を行ってみる

関連記事

AWSを使ってアプリケーションを公開する手順(1)AWSアカウントの作成
AWSを使ってアプリケーションを公開する手順(2)EC2インスタンスの作成
AWSを使ってアプリケーションを公開する方法(3)EC2インスタンスの環境構築
AWSを使ってアプリケーションを公開する手順(4)データベースの作成
AWSを使ってアプリケーションを公開する手順(5)アプリケーションを公開する
AWSを使ってアプリケーションを公開する手順(6)Nginxを導入する

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

Rails 簡単な簡単ログイン機能の追加

欲しい結果

"簡単ログイン"のボタンを押すと、
登録済みのゲストユーザーでログインするボタンの設置。

前提条件

RailsチュートリアルをベースにしたWEBアプリ。

やること

  1. sessions_controllerのcreateアクションとは別にeasy_loginアクションを作成して登録済みのゲストユーザーの emailを渡す。
  2. routs.rbにeasy_loginのルーティングを記載。
  3. viewで簡単ログインボタンを設置する。

  4. sessions_controller.rbに作成して登録済み(seeds.rb)の
    ゲストユーザーでログインできる様にeasy_loginアクションを追記する。

sessions_controller.rb
    def easy_login
      user = User.find_by(email: "test@example.com")
          log_in user #session_helperで事前に定義ずみ。
          redirect_back_or user #session_helperで事前に定義ずみ。
    end  
  1. config/routes.rbで以下を追記  
config/routes.rb
  post   '/easy_login',   to: 'sessions#easy_login'
  1. app/views/sessions/new.html.erbの任意の場所に以下を追記
app/views/sessions/new.html.erb
<p><%= link_to "簡単ログイン", easy_login_path, method: :post, class: 'btn btn-primary' %></p>

結果

とりあえずは簡単ログインボタンから、ゲストユーザーでのログインができた。

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

《未経験→webエンジニア》実務4日目

【今日やったこと】

APIテスト
SQL文の学習

【知らなかったこと】

「クエリ」は、データベースなどへの要求を文字列で表したもの。 「SQL」は、データベースへ問い合わせるための言語仕様。

内部結合は、先にAテーブルの指定カラムを拾った後にBのテーブルを結合する※重なる部分だけ
外部結合は、テーブルを左か右で指定して、その指定したテーブルを基準に結合する※重ならない部分も!
参考URLhttps://qiita.com/naoki_mochizuki/items/3fda1ad6594c11d7b43chttp://www.pursue.ne.jp/jouhousyo/SQLDoc/select22.html

・MinIO
S3の互換ツールのイメージ。Docker上で動かせて、コストもかけずにお試しできるので便利らしい※S3だと画像をgitに入れてしまうと重くなってしまうのも難点
参考URLhttps://dev.classmethod.jp/articles/s3-compatible-storage-minio/

・冗長な構成冗長構成とは、情報システムなどの構成の一種で、設備や装置を複数用意し、一部が故障しても運用を継続できるようにしたもの。システムなどが持つそのような性質を「冗長性」、そのような構成法を「冗長化」という。

【明日】やるべきこと、読みたい記事など

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

CarrierWaveで「Nil location provided. Can't build URI.」が出た時の対処法

環境

ubuntu(wsl)
Rails 6.0.3
ruby 2.5.1
CarrierWave

想定状況と原因

エラーの箇所がこちら

view/user/show.html.slim
 = link_to image_tag(user.image.url), user

一旦Userモデルの中身を調べてみます。
↓Userモデルの中身

name: "藤田 翔太",
  email: "jeromy_weimann@wehner.info",
  password_digest: [FILTERED],
  admin: false,
  image: nil,

imageの値がnilになっているのが原因のようです。

解決策

解決策1. nilとなっているユーザーを削除する。

rails cなどでnilになっているユーザーを探して削除する。

解決策2. デフォルト画像の設定をする

image_uploader.rbに以下を追加します。

app/uploaders/image_uploader.rb
  def default_url(*args)
    'default.png'
  end

app/assets/imagesに画像を置いたら終わりです。

参考文献

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

VSCodeでスペルミスを指摘してくれるCode Spell CheckerをRuby言語に対応させる

はじめに

VSCodeの拡張機能のCode Spell Checkerはスペルミスを指摘してくれる便利な機能ですが初期設定ではRuby言語には対応しておらず、自分で設定しないと動作しません。
この記事ではその設定方法を紹介したいと思います。

背景

Railsチュートリアルで学習を進めている時にrails testでよくエラーになっていたのですが、そのほとんどがスペルミスによるものでした。
そこでスペルミスをチェックしてくれるCode Spell Checkerという拡張機能を知り、インストールしてみたものの機能しませんでした。
どうやら初期設定ではRubyには対応していないということなので、設定で追加する方法を調べてみました。

Code Spell Checkerをインストールしたけど言語が対応していなくて困っている方を対象にこの記事を書きました。

動作環境

macOS Catalina バージョン: 10.15.6
VSCode バージョン: 1.49.0
Code Spell Checker バージョン: 1.9.0

設定方法

流れとしてはsettings.jsonファイルにcSpell.enabledLanguageIdsを使って言語を追加します。

まずはsettings.jsonのファイルの開き方。
command+カンマ(,)で設定画面を開きます。
設定画面の右上にあるアイコンをクリックしてsettings.jsonのファイルを開きます。
vscodeの設定画面.png

あとはsettings.jsonのファイルに"cSpell.enabledLanguageIds":["言語"]を追加するだけです。

settings.json
    "cSpell.enabledLanguageIds": [
            "css",
            "html",
            "javascript",
            "json",
            "less",
            "markdown",
            "plaintext",
            "scss",
            "text",
            "ruby",
            "yaml",
            "yml"
          ],

settings_json.png

まとめ

これで追加した言語のファイルにもCode Spell Checkerが動作するようになりました。
詳しくはCode Spell Checkerのサイトに書いてあるので見てみてください。
Code Spell Checker - Visual Studio Marketplace

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

RubyのStringIOから値を回収する

Rubyには、ファイルに書き出すと見せかけて文字列を生成する、StringIOというクラスがあります。便利に使えるのですが、少しハマった箇所がありました。

StringIOとは

まず、ファイルやパイプなどに対して読み書き、ファイル内の位置移動などを司るIOというクラスがあります。読み書きを行うメソッドが、実際に出力する先をIOで取るようにすれば、実際のアクセス先がどこであろうと利用可能です。

これに対して、StringIOは、StringIOのインターフェースで扱えるようにしたものです。引数としてIOを取るメソッドにStringIOを渡せば、値を文字列として回収できます。

なお、StringIOIOの間に継承関係はなく、ダックタイピングとして実装されています。

値の回収に失敗する例

ということで、StringIOで値を取得しようとしたのですが、失敗したことがありました。

失敗例
sio = StringIO.new
SomeAPI.download(key, output: sio)
data = sio.read

IOだからreadすればいいと思ったのですが、これではdataに取れる値は空文字列となってしまいます。

失敗の要因と改善策

失敗の要因ですが、StringIOは現在の位置も記録しているので、追記を繰り返してStringIOへ書き出した場合、位置が末端に来ています。そのままreadを行っても、すでに末端にいるので何も取れません。

もちろんrewindしてからreadでもいいのですが、そんなことをしなくても、StringIO#stringを使えばバッファとして持っている文字列をそのまま得ることができます。

外部リンク

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

RMagickを導入して、既存の既存の画像ファイルを別のフォーマットに変換する

問題

すでにRMagickが導入されている環境において、既存の画像ファイルを別のフォーマットに変換したい場合は、どのようにすれば良いでしょうか?

解決策

以下はローカルに存在するpngファイルをwebp形式に変換するサンプルコードになります。

require 'rmagick'

image = Magick::ImageList.new('xxx.png')
image.write('xxx.webp')

RMagickの導入

ここからはRMagickの導入方法を示します。

ImageMagickの導入

RMagickは前提ソフトウェアとしてImageMagickを必要とします。インストール方法は環境ごとにRMagickのREADME.mdを参考にインストールしてください。わたしの環境はmacOSで、かつ、パッケージマネージャHomebrew導入済みなので、非常に簡単でした。

brew update
brew upgrade
brew install imagemagick

RMagickの導入

Imagemagickの導入後はRubyGem経由で、rmagickをインストールします。これについてもRMagickのREADME.mdが参考になります。ちなみにわたしはbundlerを利用しました。

bundle init
vi Gemfile     #=> gem 'rmagick' を追記
bundle install

ちなみにRMagickの導入は結構トラブルが多いようなので、なにか困った場合はRMagickのREADME.mdの"Things that can go wrong"を参考にするとよいと思います。

わたしの環境でもbundle installの際に以下のようなエラーが発生。

Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
ERROR: Can't install RMagick 4.1.2. Can't find pkg-config in

このトラブルについても"Things that can go wrong"の対応方法を元に解消することができました。

brew install pkg-config

環境情報

$ brew -v
Homebrew 2.5.1
Homebrew/homebrew-core (git revision a2bbe; last commit 2020-09-16)
Homebrew/homebrew-cask (git revision dfa88; last commit 2020-09-16)
$ ruby -v
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin18]
$ bundle -v
Bundler version 2.1.2
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.erbファイルを.slimに変更してみる

環境

Ruby 2.5.7
Rails 5.2.4

経緯

Railsのテンプレートエンジンとして.erbを使っているのですが、テンプレートエンジンはそれ以外にも.haml.slimがあります。
今回.slimに興味を持ち、既存の.erbファイルの一部を.slimに変更して動作まで確認できたので、その過程を書いていきます。
ほぼケーススタディとなるので、参考にしていただければと思います。

基本構文

基本的な変更点としては次のようなものがあります。

example.html.slim
/ 開始タグのみ、終了タグは記述しない
<body></body>
=>
body

<h1>タイトル</h1>
=>
h1 タイトル

/ id名class名は続けて書く
<ul class="list"></ul>
=>
ul.list

<span id="btn"></span>
=>
span#btn

/ divはdivすらも省略する
<div id="main-contents" class="flex container" ></div>
=>
#main-contents.flex.container

/ rubyコード<%= %>は省略する(=があるものは=だけ書く)
<%= link_to '次へ' %>
=>
= link_to '次へ'

/ <% %> =が付かないものは、先頭に-をつける
/ <% end %>は全て省略(ループ文など)
<% if 条件文 %>
<% else %>
<% end %>
=>
- if 条件文
- else

構文の詳細はこちらを参照してください。
GitHub - slim
Qiita - 速習テンプレートSlim(HTML作成編)
Qiita - RailsのHTMLテンプレートエンジン、Slimの基本的な記法
Qiita - 【爆速で習得】Railsでslimを使う方法から基本文法まで

実際のコードで.erbと.slimを比較

ここからは、私が実際に変更したファイルをbefore/after形式で下記の4種類ご紹介します。
*application.html.erb/slim
*_form.html.erb/slim
*new.html.erb/slim
*edit.html.erb/slim

これらのViewファイルは私のGitHub上でも公開しております。
GitHub - matchi_ver.slim

application.html

application.html.erb
<!DOCTYPE html>
<html lang="ja">
<head>
  <%= favicon_link_tag('favicon.ico') %>
  <%= favicon_link_tag 'home-icon.png', rel: 'apple-touch-icon', size: '180x180', type: 'image/png' %>
  <%= favicon_link_tag 'home-icon.png', rel: 'android-touch-icon', size: '192x192', type: 'image/png' %>
  <title>Matchi</title>
  <script src="//maps.google.com/maps/api/js?key=<%= ENV['GOOGLE_PLATFORM_API_KEY'] %>"></script>
  <%= include_gon %>
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>
  <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
  <%= javascript_include_tag 'application' %>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">
  <!-- Global site tag (gtag.js) - Google Analytics -->
  <script async src="https://www.googletagmanager.com/gtag/js?id=<%= ENV['GOOGLE_ANALYTICS_TRACKING_ID'] %>"></script>
  <script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());

    gtag('config', "<%= ENV['GOOGLE_ANALYTICS_TRACKING_ID'] %>");
  </script>
</head>

<body>
<header>
  <div class="header-container">
    <%# PC画面ヘッダー %>
    <div class="flex pc-header">
      <%= link_to root_path do %>
        <div class="logo-image"></div>
      <% end %>
      <div class="header-nav">
        <nav>
          <ul class="flex header-ul">
            <% url = request.fullpath %>
            <%# urlに'owner'があれば店舗用ヘッダー %>
            <% if url.include?('owner') %>
              <% if master_admin_signed_in? %>
                <li><%= link_to '管理者トップ', new_master_admin_session_path %></li>
                <li><%= link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete %></li>
              <% end %>
              <% if owner_restaurant_signed_in? %>
                <li><%= link_to '店舗トップ', owner_restaurant_path(current_owner_restaurant) %></li>
                <li><%= link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete %></li>
              <% end %>
              <% if public_user_signed_in? %>
                <li><%= link_to '一般会員TOP', mypage_path(current_public_user) %></li>
                <li><%= link_to '一般会員ログアウト', destroy_public_user_session_path, method: :delete %></li>
              <% else %>
                <li><%= link_to '一般会員ログイン', new_public_user_session_path %></li>
              <% end %>

            <%# urlに'master'があれば管理者用ヘッダー %>
            <% elsif url.include?('master') %>
              <% if master_admin_signed_in? %>
                <li><%= link_to '店舗新規登録', new_owner_restaurant_registration_path %></li>
                <li><%= link_to '管理者トップ', new_master_admin_session_path %></li>
                <li><%= link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete %></li>
              <% end %>
              <% if public_user_signed_in? %>
                <li><%= link_to '一般会員TOP', mypage_path(current_public_user) %></li>
                <li><%= link_to '一般会員ログアウト', destroy_public_user_session_path, method: :delete %></li>
              <% else %>
                <li><%= link_to '一般会員ログイン', new_public_user_session_path %></li>
              <% end %>
              <% if owner_restaurant_signed_in? %>
                <li><%= link_to '店舗トップ', owner_restaurant_path(current_owner_restaurant) %></li>
                <li><%= link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete %></li>
              <% else %>
                <li><%= link_to '店舗ログイン', new_owner_restaurant_session_path %></li>
              <% end %>

            <%# 上記以外なら一般ユーザー用ヘッダー %>
            <% else %>
              <% if master_admin_signed_in? && owner_restaurant_signed_in? %>
                <li><%= link_to '管理者トップ', new_master_admin_session_path %></li>
                <li><%= link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete %></li>
                <li><%= link_to '店舗TOP', owner_restaurant_path(current_owner_restaurant) %></li>
                <li><%= link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete %></li>
              <% elsif owner_restaurant_signed_in? %>
                <li><%= link_to '店舗TOP', owner_restaurant_path(current_owner_restaurant) %></li>
                <li><%= link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete %></li>
              <% elsif master_admin_signed_in? %>
                <li><%= link_to '管理者トップ', new_master_admin_session_path %></li>
                <li><%= link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete %></li>
                <li><%= link_to '店舗ログイン', new_owner_restaurant_session_path %></li>
              <% end %>
              <% if public_user_signed_in? %>
                <li><%= link_to 'MyPage', mypage_path(current_public_user) %></li>
                  <%# if alert.count >= 1 %>
                    <li><%#= link_to 'お知らせがあります。' %></li>
                  <%# end %>
                <li><%= link_to 'ログアウト', destroy_public_user_session_path, method: :delete %></li>
              <% else %>
                <li><%= link_to '新規登録', new_public_user_registration_path %></li>
                <li><%= link_to 'ログイン', new_public_user_session_path %></li>
              <% end %>
            <% end %>
          </ul>
        </nav>
      </div>
    </div>

    <%# スマホ画面ヘッダー %>
    <div class="flex sp-header">
      <div class="hamburger">
        <span class="bar bar-top"></span>
        <span class="bar bar-center"></span>
        <span class="bar bar-bottom"></span>
      </div>
    <%= link_to root_path do %>
      <div class="logo-image"></div>
    <% end %>
      <%# 未読のお知らせの通知 %>
      <div class="alert">
        <i id="alert-bell" class="fa-2x far fa-bell"><div class="hidden icon"></div></i>
      </div>
      <%# ハンバーガーメーニュー %>
      <div class="hamburger-menu">
        <ul>
          <% if public_user_signed_in? %>
            <li><%= link_to 'MyPage', mypage_path(current_public_user) %></li>
          <% else %>
            <li><%= link_to '新規登録', new_public_user_registration_path %></li>
            <li><%= link_to 'ログイン', new_public_user_session_path %></li>
          <% end %>
          <li><%= link_to 'サービス紹介', about_path %></li>
          <li><%= link_to 'レストラン一覧', public_restaurants_path %></li>
          <li><%= link_to 'メニュー一覧', public_menus_path %></li>
          <% if public_user_signed_in? %>
            <li><%= link_to 'ログアウト', destroy_public_user_session_path, method: :delete %></li>
          <% end %>
        </ul>
      </div>
    </div>
  </div>
</header>

<main>
  <div class="body-container">
    <%= yield %>
  </div>
</main>

<footer>
  <div class="flex footer-container">
    <%= link_to root_path do %>
      <div class="logo-image"></div>
    <% end %>
    <div class="footer-menu">
      <ul class="footer-links">
        <li><%= link_to 'お問い合わせ', contacts_new_path %></li>
        <li><%= link_to '利用規約', terms_path %></li>
        <li><%= link_to 'プライバシーポリシー', privacy_path %></li>
        <li><%= link_to '運営者情報', admin_path %></li>
      </ul>
    </div>
  </div>
  <div class="copyright">
    <small>©︎ 2020 MasaoSasaki</small>
  </div>
  <div id="move-head">
    <div class="circle move-head"><i class="fa-2x fas fa-arrow-up"></i></div>
  </div>
</body>
</footer>
</html>
application.html.slim
doctype html
html lang="ja"
  head
    = favicon_link_tag('favicon.ico')
    = favicon_link_tag 'home-icon.png', rel: 'apple-touch-icon', size: '180x180', type: 'image/png'
    = favicon_link_tag 'home-icon.png', rel: 'android-touch-icon', size: '192x192', type: 'image/png'
    title Matchi
    script src="//maps.google.com/maps/api/js?key=#{ENV['GOOGLE_PLATFORM_API_KEY']}"
    = include_gon
    = csrf_meta_tags
    = csp_meta_tag
    = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application'
    meta name="viewport" content="width=device-width,initial-scale=1"
    link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet"
    / Global site tag (gtag.js) - Google Analytics
    javascript: async src="https://www.googletagmanager.com/gtag/js?id=#{ENV['GOOGLE_ANALYTICS_TRACKING_ID']}"

  body
    header
      .header-container
        / PC画面ヘッダー
        .flex.pc-header
          = link_to root_path
            .logo-image
          .header-nav
            nav
              ul.flex.header-ul
                - url = request.fullpath
                / urlに'owner'があれば店舗用ヘッダー
                - if url.include?('owner')
                  - if master_admin_signed_in?
                    li = link_to '管理者トップ', new_master_admin_session_path
                    li = link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete
                  - if owner_restaurant_signed_in?
                    li = link_to '店舗トップ', owner_restaurant_path(current_owner_restaurant)
                    li = link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete
                  - if public_user_signed_in?
                    li = link_to '一般会員TOP', mypage_path(current_public_user)
                    li = link_to '一般会員ログアウト', destroy_public_user_session_path, method: :delete
                  - else
                    li = link_to '一般会員ログイン', new_public_user_session_path
                / urlに'master'があれば管理者用ヘッダー
                - elsif url.include?('master')
                  - if master_admin_signed_in?
                    li = link_to '店舗新規登録', new_owner_restaurant_registration_path
                    li = link_to '管理者トップ', new_master_admin_session_path
                    li = link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete
                  - if public_user_signed_in?
                    li = link_to '一般会員TOP', mypage_path(current_public_user)
                    li = link_to '一般会員ログアウト', destroy_public_user_session_path, method: :delete
                  - else
                    li = link_to '一般会員ログイン', new_public_user_session_path
                  - if owner_restaurant_signed_in?
                    li = link_to '店舗トップ', owner_restaurant_path(current_owner_restaurant)
                    li = link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete
                  - else
                    li = link_to '店舗ログイン', new_owner_restaurant_session_path
                / 上記以外なら一般ユーザー用ヘッダー
                - else
                  - if master_admin_signed_in? && owner_restaurant_signed_in?
                    li = link_to '管理者トップ', new_master_admin_session_path
                    li = link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete
                    li = link_to '店舗TOP', owner_restaurant_path(current_owner_restaurant)
                    li = link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete
                  - elsif owner_restaurant_signed_in?
                    li = link_to '店舗TOP', owner_restaurant_path(current_owner_restaurant)
                    li = link_to '店舗ログアウト', destroy_owner_restaurant_session_path, method: :delete
                  - elsif master_admin_signed_in?
                    li = link_to '管理者トップ', new_master_admin_session_path
                    li = link_to '管理者ログアウト', destroy_master_admin_session_path, method: :delete
                    li = link_to '店舗ログイン', new_owner_restaurant_session_path
                  - if public_user_signed_in?
                    li = link_to 'MyPage', mypage_path(current_public_user)
                      / if alert.count >= 1
                          li = link_to 'お知らせがあります。'
                    li = link_to 'ログアウト', destroy_public_user_session_path, method: :delete
                  - else
                    li = link_to '新規登録', new_public_user_registration_path
                    li = link_to 'ログイン', new_public_user_session_path
        /スマホ画面ヘッダー
        .flex.sp-header
          .hamburger
            span.bar.bar-top
            span.bar.bar-center
            span.bar.bar-bottom
          = link_to root_path
            .logo-image
          / 未読のお知らせの通知
          .alert
            i#alert-bell.fa-2x.far.fa-bell
              .hidden.icon
          / ハンバーガーメーニュー
          .hamburger-menu
            ul
              - if public_user_signed_in?
                li = link_to 'MyPage', mypage_path(current_public_user)
              - else
                li = link_to '新規登録', new_public_user_registration_path
                li = link_to 'ログイン', new_public_user_session_path
              li = link_to 'サービス紹介', about_path
              li = link_to 'レストラン一覧', public_restaurants_path
              li = link_to 'メニュー一覧', public_menus_path
              - if public_user_signed_in?
                li = link_to 'ログアウト', destroy_public_user_session_path, method: :delete
    main
      .body-container == yield
    footer
      .flex.footer-container
        = link_to root_path
          .logo-image
          .footer-menu
            ul.footer-links
              li = link_to 'お問い合わせ', contacts_new_path
              li = link_to '利用規約', terms_path
              li = link_to 'プライバシーポリシー', privacy_path
              li = link_to '運営者情報', admin_path
      .copyright
        small ©︎ 2020 MasaoSasaki
      #move-head
        .circle.move-head
          i.fa-2x.fas.fa-arrow-up

new.html

new.html.erb
<div class="contents menus-new">
  <h2>メニュー追加</h2>
  <%= render partial: 'form', locals: {
    restaurant: @restaurant,
    menu: @menu,
    tags: @tags,
    menu_tags: @menu_tags,
    path: owner_restaurant_menus_path,
    truth: false, submit: '作成'
  } %>
</div>

new.html.slim
.contents.menus-new
  h2 メニュー追加
  == render 'form',
    restaurant: @restaurant,
    menu: @menu,
    tags: @tags,
    menu_tags: @menu_tags,
    path: owner_restaurant_menus_path,
    truth: false, submit: '作成'

edit.html

edit.html.erb
<div class="contents menus-edit">
  <h2 class="menus-edit-h2">メニュー編集</h2>
  <%= render partial: 'form', locals: {
    restaurant: @restaurant,
    menu: @menu,
    tags: @tags,
    menu_tags: @menu_tags,
    path: owner_restaurant_menu_path,
    truth: true, submit: '更新'
  } %>
</div>

edit.html.slim
contents.menus-edit
  h2.menus-edit-h2 メニュー編集
  == render 'form',
    restaurant: @restaurant,
    menu: @menu,
    tags: @tags,
    menu_tags: @menu_tags,
    path: owner_restaurant_menu_path,
    truth: true, submit: '更新'

_form.html

_form.html.erb
<div class="menu-form">
  <%= form_with model: [restaurant, menu], url: path, local: true do |f| %>
    <section class="menu-status">
      <div class="menu-form1">
        <h3>メニュー詳細</h3>
        <table>
          <tbody>
            <tr>
              <td><%= f.label :title, value: 'メニュー名' %></td>
              <td><%= f.text_field :title %></td>
            </tr>
            <tr>
              <td><%= f.label :regular_price, value: '正規価格(税抜き):' %></td>
              <td><%= f.number_field :regular_price %></td>
            </tr>
            <tr>
              <td><%= f.label :discount_price, value: '提供価格(税抜き):' %></td>
              <td><%= f.number_field :discount_price %></td>
            </tr>
            <tr>
              <td><%= f.label :reservation_method, value: '予約方法' %></td>
              <td><%= f.select :reservation_method, Menu.reservation_methods.keys.map {|method| [method]} %></td>
            </tr>
            <tr>
              <td><%= f.label :is_sale_frag, value: '販売ステータス' %></td>
              <td><%= f.select :is_sale_frag, [['販売中', true], ['販売停止中', false]] %></td>
            </tr>
          </tbody>
        </table>
      </div>
      <div class="menu-form2">
        <h3>メニュー画像</h3>
        <div class="menu-image">
          <%= f.attachment_field :menu_image %>
          <div class="image-preview"></div>
          <h4>タグの追加(任意)</h4>
          <%= text_field_tag :tag_name %>
          <%= button_tag '追加', type: 'button', class: "add-tag-btn" %>
          <div id="tag-list"></div>
          </div>
        </div>
      </div>
    </section>
    <section class="menu-tag-form">
      <h3>タグ詳細</h3>
      <table>
        <tbody>
          <%# 編集画面でのみ表示 %>
          <% if truth %>
            <tr>
              <td><h4>現在のタグ一覧</h4></td>
              <td>
                <% menu_tags.each do |menu_tag| %>
                  <div class="menu-tag">
                    <%= Tag.find(menu_tag.tag_id).name %>
                    <%= link_to 'x', {controller: 'menu_tags', action: 'destroy', tag_id: menu_tag, menu_id: params[:id], restaurant_id: params[:restaurant_id]}, method: :delete %>
                  </div>
                <% end %>
              </td>
            </tr>
          <% end %>
          <tr>
            <td><h4>タグの追加<br>(一つ以上選択推奨)</h4></td>
            <td>
              <% tag_count = 0 %>
              <% tags.each do |tag| %>
                <%# 推奨タグ7個を表示 %>
                <% if tag_count < 7 %>
                  <div class="check-box">
                    <% if menu_tags.exists?(tag_id: tag.id) %>
                      <%= check_box :tag_id, tag.id, checked: true %>
                      <%= label_tag :tag_id, "#{tag.name}"%>
                    <% else %>
                      <%= check_box :tag_id, tag.id %>
                      <%= label_tag :tag_id, "#{tag.name}"%>
                    <% end %>
                  </div>
                  <% tag_count += 1 %>
                <% else %>
                  <% break %>
                <% end %>
              <% end %>
            </td>
          </tr>
        </tbody>
      </table>
    </section>

    <section class="menu-form-area">
      <div class="content-form">
        <p><%= f.label :content, value: '内容' %></p>
        <%= f.text_area :content %>
      </div>
      <div class="cancel-form">
        <p><%= f.label :cancel, value: 'キャンセル規定' %></p>
        <%= f.text_area :cancel %>
      </div>
    </section>

    <div class="submit"><%= f.button "#{submit}", onclick: 'submit();', type: 'button', class: 'btn' %></div>
  <% end %>

</div>
_form.html.slim
.menu-form
  = form_with model: [restaurant, menu], url: path, local: true do |f|
    section.menu-status
      .menu-form1
        h3 メニュー詳細
        table
          tbody
            tr
              td = f.label :title, value: 'メニュー名'
              td = f.text_field :title
            tr
              td = f.label :regular_price, value: '正規価格(税抜き):'
              td
                = f.number_field :regular_price
                |tr
              td = f.label :discount_price, value: '提供価格(税抜き):'
              td
                = f.number_field :discount_price
                |tr
              td = f.label :reservation_method, value: '予約方法'
              td = f.select :reservation_method, Menu.reservation_methods.keys.map {|method| [method]}
            tr
              td = f.label :is_sale_frag, value: '販売ステータス'
              td = f.select :is_sale_frag, [['販売中', true], ['販売停止中', false]]
      .menu-form2
        h3 メニュー画像
        .menu-image
          = f.attachment_field :menu_image
          .image-preview
          h4 タグの追加(任意)
          = text_field_tag :tag_name
          = button_tag '追加', type: 'button', class: "add-tag-btn"
          #tag-list
    section.menu-tag-form
      h3 タグ詳細
      table
        tbody
          / 編集画面でのみ表示
          - if truth
            tr
              td: h4 現在のタグ一覧
              td
                - menu_tags.each do |menu_tag|
                  .menu-tag
                    = Tag.find(menu_tag.tag_id).name
                    = link_to 'x', {controller: 'menu_tags', action: 'destroy', tag_id: menu_tag, menu_id: params[:id], restaurant_id: params[:restaurant_id]}, method: :delete
          tr
            td: h4 タグの追加<br>(一つ以上選択推奨)
            td
              - tag_count = 0
              - tags.each do |tag|
                / 推奨タグ7個を表示
                - if tag_count < 7
                  .check-box
                    - if menu_tags.exists?(tag_id: tag.id)
                      = check_box :tag_id, tag.id, checked: true
                      = label_tag :tag_id, "#{tag.name}"
                    - else
                      = check_box :tag_id, tag.id
                      = label_tag :tag_id, "#{tag.name}"
                  - tag_count += 1
                - else
                  - breeak
    section.menu-form-area
      .content-form
        p = f.label :content, value: '内容'
        = f.text_area :content
      .cancel-form
        p = f.label :cancel, value: 'キャンセル規定'
        = f.text_area :cancel
    .submit= f.button "#{submit}", onclick: 'submit();', type: 'button', class: 'btn'

まとめ・感想

上記のコードは全て動作確認済みです。
.erbと.slimのコード量を比較すると大体2/3ぐらいになります。
インデントがとても大事で、インデントを間違えるだけで普通にsyntax errorになるので、その場合はブロックごとにコメントアウトしながら確認をしました。
application.html.slimなら、bodyは全てコメントアウトして、まずはhead内だけ取り掛かり、head内でもsyntax errorならまたその中でコメントアウトを駆使して問題を切り分けていくような流れです。
これからslimに書き換えようと思っている方の参考になれば幸いです。

コードでわからない箇所や不明点、質問、解釈の違い、記述方法に違和感がありましたら、コメント等でご指摘いただけると幸いです。

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

参考サイト

GitHub - slim
Qiita - 速習テンプレートSlim(HTML作成編)
Qiita - RailsのHTMLテンプレートエンジン、Slimの基本的な記法
Qiita - 【爆速で習得】Railsでslimを使う方法から基本文法まで
Qiita - Slim コードのリファクタリング
GitHub - matchi_ver.slim

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

【Rails】rails dbコマンドまとめ

rails5.0からは、rakeコマンドとrailsコマンドどちらも使えるようになっている模様

データベースを作成 / 削除する(定義元は、database.yml)

# 作成する
$ rails db:create

# 削除する
$ rails db:drop

全てのテーブルをdropして、テーブルを再生成する

# db/schemaを元に再生成
$ rails db:reset

# db/migrateファイルを元に再生成
$ rails db:migrate:status

マイグレーションファイルの一つ前の命令をなかったことにする

# 一つ前の命令をなかったことにする
$ rails db:rollback

# 二つ前までの命令を連続でなかったことにする
$ rails db:rollback STEP=2

テストデータをデータベースに反映させる

# seedファイルを元に作成
$ rails db:seed

開発環境の初期設定を一気に行う

# create / schema:load / seedコマンドを順に行う
$ rails db:setup

参考記事

https://qiita.com/parsetree/items/e9b08c6b11f762b949de

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

【Ruby on Rails】検索機能(非選択)

目標

search.gif

開発環境

ruby 2.5.7
Rails 5.2.4.3
OS: macOS Catalina

前提

※ ▶◯◯ を選択すると、説明等が出てきますので、
  よくわからない場合の参考にしていただければと思います。

【Ruby on Rails】検索機能(モデル、方法選択式)
こちらを実装した上で編集を加えていきます。

流れ

1 controllerの編集
2 viewの編集

controllerの編集

今回は曖昧検索に絞っています。
完全一致にしたい場合は、
where(name: @content)
となります。

app/controllers/searchs_controller.rb
class SearchsController < ApplicationController
  def search
    @content = params["content"]
    @users = User.where('name LIKE ?', '%'+@content+'%')
    @posts = Post.where('title LIKE ?', '%'+@content+'%')
  end
end

viewの編集

app/views/search.html.erb
<% if @users.present? && @posts.present? %>
  <h3>【Users,Postsモデルの検索結果】検索ワード:<%= @content %></h3>
  <h4>・ユーザー名</h4>
  <%= render 'users/index', users: @users %>
  <h4>・投稿内容</h4>
  <%= render 'posts/index', posts: @posts %>
<% elsif @users.present? && @posts.empty? %>
  <h3>【Usersモデルの検索結果】検索ワード:<%= @content %></h3>
  <h4>・ユーザー名</h4>
  <%= render 'users/index', users: @users %>
<% elsif @users.empty? && @posts.present? %>
  <h3>【Postsモデルの検索結果】検索ワード:<%= @content %></h3>
  <h4>・投稿内容</h4>
  <%= render 'posts/index', posts: @posts %>
<% else %>
  <h3>検索ワード:<%= @content %>に該当はありません</h3>
<% end %>

補足【エラー時】
routingや部分テンプレートは
【Ruby on Rails】検索機能(モデル、方法選択式)
こちらを参考にしてください。

補足【present、emptyについて】
nil? empty? blank? present? exists? メソッドを状況に応じて使い分けよう[Rails]
こちらでわかりやすく解説されていました。

補足【&&について】
Rubyのand,&&とor,||の違いと注意点
こちらでわかりやすく解説されていました。

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

パスワード無しでdeviseのユーザー情報を更新する方法

環境

ruby (2.6.5)
rails(6.0.0)
devise (4.7.2)

ユーザー情報編集ページの実装

まずはルーティングされている users/registrations#edit が実行されるようにリンク先を指定します。

Prefix                 Verb   URI Pattern               Controller#Action
edit_user_registration GET    /users/edit(.:format)     users/registrations#edit

下記のように。

<%= link_to 'マイページ', edit_user_registration_path(current_user), class: "user-nickname" %>

deviseコントローラーの編集

パスワード無しでデータを更新する機能はdeviseのコントローラーに実装するので、
ターミナルでdeviseのコントローラーを生成します。

$ rails g devise:controllers users

生成された中のRegistrationsControllerを下記のように編集します。

app>controllers>users>registrations_controlle.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

  def after_update_path_for(_resource)
    root_path
  end

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

update_resourceでパスワード無しで更新しています。
after_update_path_forで更新後のリダイレクト先を指定しています。
configure_account_update_paramsで今回はUserテーブルのnicknameというカラムだけ更新を許可しています。

詳しくは公式のwikiを参照してください。
https://github.com/heartcombo/devise/wiki/How-To:-Allow-users-to-edit-their-account-without-providing-a-password

devise editのビューファイル編集

ターミナルでdeviseのビューファイルを生成します。

$ rails g devise:views

生成されたviewファイルのedit.html.viewを必要な入力フォームだけになるように編集します。

app>views>devise>registrations>edit.html.erb
<h2>Edit <%= resource_name.to_s.humanize %></h2>

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

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

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

  <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
    <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
  <% end %>

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

routes.rbの編集

下記のように編集し、registration時のコントローラーの指定を行います。

routes.rb
devise_for :users, controllers: {
    registrations: 'users/registrations'
  }

Userモデルのパスワードのバリデーション編集

updateの際、パスワードのバリデーションに弾かれないように、
on: :createと追記しています。
この記述によってパスワードのバリデーションはcreateアクション実行時にのみ適用されることになります。

app>models>user.rb
  with_options presence: true do
    validates :nickname, :birthday
    validates :email, uniqueness: true
    validates :first_name, :last_name, format: { with: regexp_name }
    validates :first_name_read, :last_name_read, format: { with: regexp_name_read }
    validates :password, format: { with: regexp_password }, on: :create
  end

以上を実装することでパスワード無しでdeviseのUserモデルのテーブル更新を行うことができました。
ここまでご覧いただき、有難うございました。

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

Railsのlinkd_toタグ

はじめに

railsのlink_toメソッドの書き方をまとめました。

目次

  1. link_toメソッドとは
  2. 基本的な書き方

1. link_toメソッドとは

link_toメソッドとはビューで使用するhelperメソッドです。
リンクを表示させたい時に使用し、htmlのaタグを生成してくれます。
link_toメソッドにリンクとして表示する文字列とリンク先を引数として渡すことで、リンクを表示させることができます。
以下にlink_toメソッドの基本的なソースコードの書き方の紹介をしていきます。

2. 基本的な書き方

  • 第一引数にリンクのテキスト
  • 第二引数にパス、URLの指定

これらを引数として渡すことでリンクを作成することができます。

URLやパスを使用する方法

  • URLを用いる場合
<%= link_to 'Yahoo', 'http://www.yahoo.co.jp/' %>
  • パスを用いる場合
<%= link_to ‘ユーザー一覧’, ‘/users/index’ %>

ルーティングを使用する方法

同じアプリケーション内へのリンクを作成する場合は以下を使います。
「config/routes.rb」で設定しているルーティングの名前に「_path」をつけたものをリンク先として指定します。
ルーティングの名前を確認するときは以下のコマンドを使います。作成したアプリケーションで実行します。

ターミナル
rails routes
   Prefix        Verb       URI Pattern                  Controller#Action
   incomes       POST       /incomes(.:format)            incomes#create
   new_income    GET        /incomes/new(.:format)        incomes#new
   edit_income   GET        /incomes/:id/edit(.:format)   incomes#edit
   income        PATCH      /incomes/:id(.:format)        incomes#update
                 PUT        /incomes/:id(.:format)        incomes#update
                 DELETE     /incomes/:id(.:format)        incomes#destroy

例えば、incomes_controllerのnewアクション(新規作成画面)にリンクを貼りたいときは以下のように書きます。

  • Prefixを用いる場合
<%= link_to '新規作成’, new_income_path %>
  • URI Patternを用いる場合
<%= link_to '新規作成’, ‘/incomes/new’ %>

idを指定する必要があるとき

上記のURI Patternにidを含むものがありますが、これはどの「income」についての編集画面にリンクを設定するのかというのを表しています。
「edit_income_path」にincomeのidを引数としてわたすことで、インカムデータのidを元にリンク先を設定してくれます。

methodオプションを使用する方法

link_toメソッドの引数にはHTTPメソッドを指定することができます。何も指定しなければGETとなります。書き方は以下の通りです。

<%= link_to ‘削除’, income_path(params[:id]), method: :delete %>

また、id属性やclass属性を設定することもできます。

do~endを使用する方法

link_toメソッドは以下のようにdo~endのブロックを使って記述することもできます。

<%= link_to income_path, class: 'hoge' do %>
  <div>a</div>
      <h4>b</h4>
       <p>c</p>
<% end %>

link_to do ~ end の中の要素は同時にリンクできるようになります。

参考リンク

https://udemy.benesse.co.jp/development/web/link-to.html

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

入力したtextの改行がviewで表示されない

概要

text_areaで入力した文章に改行が入っていたのに、いざ投稿一覧でみてみると改行が反映されていない時の解決法について共有しておきます。

例えば、postのmessageに


xxx
yyy
zzz



入力したつもりが、投稿一覧で確認してみると


xxx yyy zzz


となってしまった時の対処法です。

結論

index.html.erb
<%= post.message %>

となっていた部分を

index.html.erb
<%= simple_format(post.message) %>

としましょう。

simple_formatとは?

simple_formatは、改行文字を含むテキストをブラウザ上で表示させる時に使われるヘルパーになります。simple_formatの機能について簡単にまとめます。

文字列を<p>で囲む
改行には<br/>を付与
連続した改行については</p><p>を付与

(他記事を参照)

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

[Rails] warning: constant Gem::ConfigMap is deprecatedが発生したら

はじめに

先日、Rails consoleを日本語対応させようとrbenvの環境をいじったら、誤ってRubyGemsのバージョンをアップデートしてしまいました。
すると、RubyGemsのバージョンアップ後に以下のような警告が発生してrubocopなどが正しくうごかなくなってしまいました。

/Users/hoge/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/bundler-1.16.4/lib/bundler/rubygems_integration.rb:200: warning: constant Gem::ConfigMap is deprecated

解決方法

RubyGemsのバージョンをupdate or downgradeして、bundlerのバージョンと相性のよいバージョンに合わせることで解決しました。
今回bundlerのバージョンが1.16.4だったので、rubygems-updateのバージョンを2.5.1にして、 $gem list rubygems-update を実行して解決しました。
bundlerのバージョンもアップデートして問題ない場合はbundlerとRubyGemsを最新バージョンにすることでも解決すると思います。

RubyGemsのバージョン変更は以下のサイトを参考にさせていただきました。
https://blog.ruedap.com/2011/02/21/ruby-gem-update-version-down

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

単体モデルテストで got errors: User must exist と出る

単体モデルテストで got errors: User must exist と出た時の対処法

ruby '2.6.5'
rails '6.0.0'

medicine_spec.rb
before do
    @medicine = FactoryBot.build(:medicine)
    @medicine.image = fixture_file_upload('public/images/money.jpg')
  end
  describe '薬の新規登録' do
    context '薬の新規登録がうまくいくとき' do
      it "medicine,symptom,date,imageが存在すれば登録できる" do
        expect(@medicine).to be_valid
      end
    end

このテストコードで下記のようなエラー分が出ます。

5.png

仮説を立てました。
got errors: User must existと出たのでuser情報が関係してると仮説を立てました。

medicine.rb
   class Medicine < ApplicationRecord
     has_one_attached :image
     belongs_to :user
     with_options presence: true do
       validates :medicine
       validates :symptom
       validates :date
     end
   end
user.rb
    has_many :medicines

モデルのアソシエーションは組めています。次に考えたのは値をいれるテストの時にアソシエーション組めていないと仮説を立てます。

medicines.rb
   FactoryBot.define do
     factory :medicine do
        medicine { 'ロキソニン' }
        symptom { 'かぜ' }
        date { Time.now.utc }
       association :user
     end
   end

association :userを追加しました。テストしてみます。

6.png

無事テスト通りました!

テストは必ず通しますがエラーになったらアプリケーションの繋がりを確認できるので引き続き学習していきます!6.png

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

Rubyを利用して、インターネット上のファイルをダウンロードし、ローカルに保存したい (注意点あり)

課題

Rubyを利用して、インターネット上のファイルをダウンロードし、ローカルに保存したい場合はどのようにすればよいでしょうか?

解決策

まずはテキストファイルの例です。Wikipedia - HyperText Markup LanguageをHTMLファイルとしてダウンロードしたい場合は次のように書くと良いです。

require 'open-uri'

uri_str = 'https://ja.wikipedia.org/wiki/HyperText_Markup_Language'
URI.open(uri_str) do |res|
  IO.copy_stream(res, 'HyperText_Markup_Language.html')
end

画像のようなバイナリでも同じです。Wikipedia - Portable Network GraphicsにPNGファイルの例としてあげられている画像をダウンロードしてみましょう。

require 'open-uri'

uri_str = 'https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png'
URI.open(uri_str) do |res|
  IO.copy_stream(res, 'PNG_transparency_demonstration_1.png')
end

注意

open-uriライブラリはNet::HTTPNet::HTTPSNet::FTPなどのラッパーで、httpやhttpsのURLを一般的なファイルのように扱うことができます。このライブラリはKernel#openを再定義するため、以下のように書くこともできます。

require 'open-uri'

uri_str = 'https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png'
open(uri_str) do |res|
  IO.copy_stream(res, 'PNG_transparency_demonstration_1.png')
end

ただし、Ruby2.7からはopen-uriにより拡張されるKernel#openを使って、URIを開くのは非推奨になりました。上記のコードをRuby2.7で実行すると、以下のような警告が出ます。

warning: calling URI.open via Kernel#open is deprecated, call URI.open directly or use URI#open

警告が出るだけで動かなくなったわけではないのですが、URI#openOpenURI#open_uriを利用することをお勧めします。

環境情報

$ ruby -v
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin18
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SCLを利用してCentOS7にRuby 2.5をインストール

はじめに

rubyのインストール方法を確立させるため、あれこれ試してみた。

手順

1.CentOSのSCLをインストールする

# yum install -y centos-release-scl

2.開発者向けツールをインストールする

# yum group install -y "Development Tools"

3.必要ライブラリをインストールする

# yum install -y zlib-devel
# yum install -y ImageMagick ImageMagick-devel

4.ruby2.5をインストールする

# yum install -y rh-ruby25 rh-ruby25-ruby-devel

5.ruby2.5のライブラリを共有ライブラリから読み込めるようにする

# echo '/opt/rh/rh-ruby25/root/usr/lib64' > /etc/ld.so.conf.d/rh-ruby.conf
# ldconfig

6.ruby2.5へのリンクを張る

# update-alternatives --display ruby
# update-alternatives --install /usr/bin/ruby ruby /opt/rh/rh-ruby25/root/bin/ruby 25 \
--slave /usr/bin/gem gem /opt/rh/rh-ruby25/root/bin/gem
# update-alternatives --display ruby
ruby -ステータスは自動です。
リンクは現在 /opt/rh/rh-ruby25/root/bin/ruby を指しています。
/opt/rh/rh-ruby25/root/bin/ruby - 優先度 25
 スレーブ gem: /opt/rh/rh-ruby25/root/bin/gem
現在の「最適」バージョンは /opt/rh/rh-ruby25/root/bin/ruby です。

7.rubyのバージョンを確認する

# ruby -v
ruby 2.5.5p157 (2019-03-15 revision 67260) [x86_64-linux]

トラブルシューティング

共有ライブラリエラーが表示される

# ruby- v
ruby: error while loading shared libraries: libruby.so.2.5: cannot open shared object file: No such file or directory

原因

共有ライブラリにruby実行ファイルのライブラリが認識されていない。

対処

# echo '/opt/rh/rh-ruby25/root/usr/lib64' > /etc/ld.so.conf.d/rh-ruby.conf
# ldconfig

そのほか

SCL(Software CoLlectioin)

RedHat社が提供するCentOS向けのパッケージ。yumと競合しない使い方ができるらしい。

参考

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

ポートフォリオ作成 Ruby on Rails

現在Ruby on Railsを用いて作成中のwebアプリについて投稿していきたいと思います。

ポートフォリオ概要

github,heroku URL

https://github.com/ShotaNagato/new_app
https://shota-rails-app.herokuapp.com

アプリについて

何かを教えたい先生と教えて欲しい生徒をオンラインでつなげるマッチングサイト。

開発背景

プログラミングを独学していてわからないところやエラーに時間をかけすぎてしまう。スクールに通う余裕はない。聞きたい時に聞きたいことだけ聞いて終わりという関係の先生が必要。

環境

エディタ VScode
Ruby 2.7.1
Rails 6.0.3.2
DB MySQL

実装済み機能

生徒と先生それぞれでの登録、ログイン、ログアウト機能
生徒、先生の一覧表示機能
登録内容更新機能

実装予定機能

プロフィール作成機能
募集投稿機能
投稿一覧表示機能
メッセージ機能

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

Rails 6で認証認可入り掲示板APIを構築する #12 userとpostの関連付け

Rails 6で認証認可入り掲示板APIを構築する #11 userモデルのテストとバリデーション追加

postからuserへの関連付けをする

postとuserの関連付けを行います。
想定読者がRailsチュートリアル完了済み前提のため意味の説明は割愛しますが、postにbelongs_to :userを、userにhas_many :postsを追加しましょう。

$ rails g migration AddUserIdToPosts user:references

レコードがある状態だとnotnull制約に引っかかってmigrationがエラーになるので、db:resetしてしまいます(乱暴)

$ rails db:reset
$ rails db:migrate
app/models/post.rb
...
 class Post < ApplicationRecord
+  belongs_to :user
+
...
app/models/user.rb
...
   include DeviseTokenAuth::Concerns::User


+  has_many :posts, dependent: :destroy
+
...

2つのテーブルの関連付けを行ったら、ちゃんと動作するかrails cで実験してみます。

$ rails  c
[1] pry(main)> user = User.create!(name: "hoge", email: "test@example.com",  password: "password")
[2] pry(main)> post = Post.create!(subject: "test", body: "testtest", user: user)
[3] pry(main)> user.posts
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 1]]
=> [#<Post:0x000000000488dbb0
  id: 1,
  subject: "test",
  body: "testtest",
  created_at: Tue, 08 Sep 2020 08:36:20 UTC +00:00,
  updated_at: Tue, 08 Sep 2020 08:36:20 UTC +00:00,
  user_id: 1>]
[4] pry(main)> post.user
=> #<User id: 1, provider: "email", uid: "test@example.com", name: "hoge", email: "test@example.com", created_at: "2020-09-08 08:36:11", updated_at: "2020-09-08 08:36:11">

どうやら、無事userからpostsを呼んだり、postからuserを呼んだりできていますね。

postのserializerを直す

postsのAPIから、ユーザーのIDと名前、メールアドレスを取得したいと思います。
その際に直すべきはserializerとcontroller。
最低限動くにはserializerだけで良いのですが、controllerも手を入れないとN+1問題という無駄なSQLが大量に流れてパフォーマンスを落とす状態になるのでご注意ください。

app/serializers/post_serializer.rb
...
 class PostSerializer < ActiveModel::Serializer
   attributes :id, :subject, :body
+  belongs_to :user

こうするとuserがくっついてきます。

$ curl localhost:8080/v1/posts/1
{"post":{"id":1,"subject":"test","body":"testtest","user":{"id":1,"provider":"email","uid":"test@example.com","name":"hoge","email":"test@example.com","created_at":"2020-09-08T08:36:11.972Z","updated_at":"2020-09-08T08:36:11.972Z"}}}

くっついてきたは良いけど、なんかuserの不要な情報までいっぱい取れてきてしまいましたね。
userのserializerがないので追加しましょう。

userのserializerを作る

modelを作った際にserializerは自動生成されるのですが、devise_token_authでmodel生成したため手動でコマンドを叩きます。
なお、devise_token_authによって自動生成されたcontrollerのレスポンスjsonはactiveModelSerializerが効きません。もし有効化したい場合はdevise系のcontrollerをオーバーライドする必要があるのですが、今回は割愛します。

今後postモデルからuser

$ rails g serializer user
app/serializers/user_serializer.rb
# frozen_string_literal: true

class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :email
end
$ curl localhost:8080/v1/posts/1
{"post":{"id":1,"subject":"test","body":"testtest","user":{"id":1,"name":"hoge","email":"test@example.com"}}}

これでひとまずOK。

N+1問題への対応

それでは、複数ユーザー・複数投稿データをrails cで作ってみて、curl localhost:8080/v1/postsを叩いてみます。
無事にデータは取ってこれますが、rails sで立ち上げているターミナルに移動してみると…

Started GET "/v1/posts" for 127.0.0.1 at 2020-09-08 08:48:08 +0000
Processing by V1::PostsController#index as */*
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT $1  [["LIMIT", 20]]
   app/controllers/v1/posts_controller.rb:12:in `index'
[active_model_serializers]   User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 3], ["LIMIT", 1]]
[active_model_serializers]   ↳ app/controllers/v1/posts_controller.rb:12:in `index'
[active_model_serializers]   CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 3], ["LIMIT", 1]]
[active_model_serializers]    app/controllers/v1/posts_controller.rb:12:in `index'
[active_model_serializers]   CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 3], ["LIMIT", 1]]
[active_model_serializers]   ↳ app/controllers/v1/posts_controller.rb:12:in `index'
[active_model_serializers]   User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
...
[active_model_serializers]    app/controllers/v1/posts_controller.rb:12:in `index'
[active_model_serializers]   CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
[active_model_serializers]   ↳ app/controllers/v1/posts_controller.rb:12:in `index'
[active_model_serializers] Rendered ActiveModel::Serializer::CollectionSerializer with ActiveModelSerializers::Adapter::Json (30.42ms)
Completed 200 OK in 34ms (Views: 32.5ms | ActiveRecord: 0.8ms | Allocations: 21448)

省略していますが、こんな感じに大量のSQLが流れています。
これがN+1問題です。

postに紐づくuserを、1レコードずつfindして取ってきているので無駄なSQLが大量に流れます。

これはPost.allしてくるタイミングでincludesしておけばOKです。

app/controllers/v1/posts_controller.rb
     def index
-      posts = Post.order(created_at: :desc).limit(20)
+      posts = Post.includes(:user).order(created_at: :desc).limit(20)
       render json: posts
     end
Started GET "/v1/posts" for 127.0.0.1 at 2020-09-08 08:51:50 +0000
Processing by V1::PostsController#index as */*
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT $1  [["LIMIT", 20]]
   app/controllers/v1/posts_controller.rb:12:in `index'
  User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3)  [["id", 3], ["id", 2], ["id", 1]]
  ↳ app/controllers/v1/posts_controller.rb:12:in `index'
[active_model_serializers] Rendered ActiveModel::Serializer::CollectionSerializer with ActiveModelSerializers::Adapter::Json (5.32ms)
Completed 200 OK in 41ms (Views: 32.7ms | ActiveRecord: 5.1ms | Allocations: 17394)

usersとposts、それぞれのテーブル1回ずつだけの計2本になりました。

とりあえずアプリケーションの動きとしては直ったように見えますが、実はこの状態でrspecを動かすと盛大にコケまくります。
次回はrspecとseedを確認していきます。

続き


連載目次へ

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

(ギリ)20代の地方公務員がRailsチュートリアルに取り組みます【第10章】

前提

・Railsチュートリアルは第4版
・今回の学習は3周目(9章以降は2周目)
・著者はProgate一通りやったぐらいの初学者

基本方針

・読んだら分かることは端折る。
・意味がわからない用語は調べてまとめる(記事最下段・用語集)。
・理解できない内容を掘り下げる。
・演習はすべて取り組む。
・コードコピペは極力しない。

 
 認証システム開発・第5段回目、ついに2ケタ・第10章に突入です。RESTアクションを完成させていきましょう。
 
本日のBGMはこちら。
PLASTIC GIRL IN CLOSET "TOY"
もう10年前のアルバムになるのか…時が経つのは早いですね。そら私の20代終わっちゃうわ。

 

【10.1.1 編集フォーム メモと演習】

・リンクのaタグ内に target="_blank"を入れると、リンクを別タブで開くようになる。(セキュリティ対策は演習で)
・Active Recordのnew_recordメソッドで新規ユーザーか既存ユーザーか、Rails内部で判断している。

1. 先ほど触れたように、target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点です。具体的には、フィッシング (Phising) サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。Gravatarのような著名なサイトではこのような事態は起こらないと思いますが、念のため、このセキュリティ上のリスクも排除しておきましょう。対処方法は、リンク用のaタグのrel (relationship) 属性に、"noopener"と設定するだけです。早速、リスト 10.2で使ったGravatarの編集ページへのリンクにこの設定をしてみましょう。
→ 下記。

<a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>

 
2. リスト 10.5のパーシャルを使って、new.html.erbビュー (リスト 10.6) とedit.html.erbビュー (リスト 10.7) をリファクタリングしてみましょう (コードの重複を取り除いてみましょう)。ヒント: 3.4.3で使ったprovideメソッドを使うと、重複を取り除けます3 。(関連するリスト 7.27の演習課題を既に解いている場合、この演習課題をうまく解けない可能性があります。うまく解けない場合は、既存のコードのどこに差異があるのか考えながらこの課題に取り組んでみましょう。例えば筆者であれば、リスト 10.5で用いた変数を渡すテクニックを使って、リスト 10.6やリスト 10.7で必要になるURLをリスト 10.5に渡してみるでしょう。)
→ やるだけよん。(なぜか7章の演習やってたけどビュー同士の違いが発生してなかった。考察してるときに戻してなかったか)

 

【10.1.2 編集の失敗 演習】

1. 編集フォームから有効でないユーザー名やメールアドレス、パスワードを使って送信した場合、編集に失敗することを確認してみましょう。
→ ちゃんと編集ページに戻されてエラーが出ました。

 

【10.1.3 編集失敗時のテスト 演習】

1. リスト 10.9のテストに1行追加し、正しい数のエラーメッセージが表示されているかテストしてみましょう。ヒント: 表 5.2で紹介したassert_selectを使ってalertクラスのdivタグを探しだし、「The form contains 4 errors.」というテキストを精査してみましょう。
→ 下記

assert_select "div.alert", "The form contains 4 errors."

 

【10.1.4 TDDで編集を成功させる 演習】

1. 実際に編集が成功するかどうか、有効な情報を送信して確かめてみましょう。

2. もしGravatarと紐付いていない適当なメールアドレス (foobar@example.comなど) に変更した場合、プロフィール画像はどのように表示されるでしょうか? 実際に編集フォームからメールアドレスを変更して、確認してみましょう。
→ まとめて、サンプルユーザーのアドレスを適当に変更→成功、グラバターのデフォ画像が表示される。

 

【10.2 認可 メモ】

認証(authentication):サイトのユーザーを識別
認可(authorization):ユーザーが実行可能な操作を管理
 

【10.2.1 ユーザーにログインを要求する メモと演習】

before_action:
コントローラにおいて、何らかのアクションが実行される直前に、特定のメソッドを実行する。オプションにonly: [:アクション]を渡すことで、特定アクションにだけ適用する。

1. デフォルトのbeforeフィルターは、すべてのアクションに対して制限を加えます。今回のケースだと、ログインページやユーザー登録ページにも制限の範囲が及んでしまうはずです (結果としてテストも失敗するはずです)。リスト 10.15のonly:オプションをコメントアウトしてみて、テストスイートがそのエラーを検知できるかどうか (テストが失敗するかどうか) 確かめてみましょう。
→ Yes, RED !

 

【10.2.2 正しいユーザーを要求する 演習】

1. 何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。
→ usersリソースのURLが異なる(editは/users/1/edit、updateは/users/1)から。第7章の表7.1参照。

 
2. 上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか?
→ editでしょ。HTTPリクエストがGETだから。ブラウザに表示してくれる。

 

【10.2.3 フレンドリーフォワーディング メモと演習】

requestオブジェクト:Railsガイド参照。URL以外にもいろいろなクライアント側の情報が含まれているよ。

1. フレンドリーフォワーディングで、渡されたURLに初回のみ転送されていることを、テストを書いて確認してみましょう。次回以降のログインのときには、転送先のURLはデフォルト (プロフィール画面) に戻っている必要があります。ヒント: リスト 10.29のsession[:forwarding_url]が正しい値かどうか確認するテストを追加してみましょう。
→ これが分からなかった。調べた結果が以下。このテストではまずedit_user_path(@user)にアクセスしようとしているから、session[:forwarding_url]がそのURLと等しいかチェック。そしてログイン後はアクセスしようとしていたedit_user_url(@user)に戻ってるかチェックして、session[:forwarding_url]がnilか(deleteされているか)を確かめていると。なるほどな〜。

users_edit_test.rb
  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    assert_equal session[:forwarding_url], edit_user_url(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user)
    assert_nil session[:forwarding_url]
    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

 
2. 7.1.3で紹介したdebuggerメソッドをSessionsコントローラのnewアクションに置いてみましょう。その後、ログアウトして /users/1/edit にアクセスしてみてください (デバッガーが途中で処理を止めるはずです)。ここでコンソールに移り、session[:forwarding_url]の値が正しいかどうか確認してみましょう。また、newアクションにアクセスしたときのrequest.get?の値も確認してみましょう (デバッガーを使っていると、ときどき予期せぬ箇所でターミナルが止まったり、おかしい挙動を見せたりします。熟練の開発者になった気になって (コラム 1.1)、落ち着いて対処してみましょう)。
→ (byebug)にsession[:forwarding_url]を入れると格納されているURL(~/users/1/edit)が表示され、request.get?を入れるとtrueが返ってきます。

 

【10.3.1 ユーザーの一覧ページ 演習】

1. レイアウトにあるすべてのリンクに対して統合テストを書いてみましょう。ログイン済みユーザーとそうでないユーザーのそれぞれに対して、正しい振る舞いを考えてください。ヒント: log_in_asヘルパーを使ってリスト 5.32にテストを追加してみましょう。
→ 自分で試しに書いた下のコードでGREENだったんですがダメ?しかも2パターン。

site_layout_test
require 'test_helper'

class SiteLayoutTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "layout links" do
    get root_path
    assert_template 'static_pages/home'
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
    assert_select "a[href=?]", signup_path
    get contact_path
    assert_select "title", full_title("Contact")
    get signup_path
    assert_select "title", full_title("Sign up")
    log_in_as(@user)
    follow_redirect!  # もしくは get user_path(@user)
    assert_select "a[href=?]", users_path
    assert_select "a[href=?]", user_path(@user)
    assert_select "a[href=?]", edit_user_path(@user)
    assert_select "a[href=?]", logout_path
  end

end

調べてみた他の演習まとめとの違い
・ログイン済みユーザーのテストを非ログイン時のテストと分けている。
⇨これはコードの可読性を考えると分けた方がいいのかも。あとは明確に動作を分けた方がテストとしては確実?

・ログイン後にget root_pathしている。
⇨これは不自然じゃないか?ログイン後のデフォルト動作はユーザーページへ飛ぶんやろ?何でわざわざhomeに行く?んで、log_in_asではpostリクエストしてるわけだから、follow_redirect!で実際にそのページへ行ってテストしてもよし、get user_path(@user)でテスト対象のページを指定してもよし、と考えました。後々不具合が出るようであれば見直します。

 

【10.3.2 サンプルのユーザー メモと演習】

 ここでfakerジェム入れる時に赤文字が出てたのでbundle update。こういうのは慣れてきましたね。

1. 試しに他人の編集ページにアクセスしてみて、10.2.2で実装したようにリダイレクトされるかどうかを確かめてみましょう。
→ ためしに~/user/2/editにアクセスしようとすると、homeに飛ばされます。

 

【10.3.3 ページネーション 演習】

1. Railsコンソールを開き、pageオプションにnilをセットして実行すると、1ページ目のユーザーが取得できることを確認してみましょう。
→ 下記

>> User.paginate(page: nil)
  User Load (1.0ms)  SELECT  "users".* FROM "users" LIMIT ? OFFSET ?  [["LIMIT", 11], ["OFFSET", 0]]
   (0.2ms)  SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1, name: "Example User", email: "example@railstutorial.org", 
以下、長いから省略

 
2. 先ほどの演習課題で取得したpaginationオブジェクトは、何クラスでしょうか? また、User.allのクラスとどこが違うでしょうか? 比較してみてください。
→ User::ActiveRecord_Relationクラス。一緒ですね。

>> User.paginate(page: nil).class
=> User::ActiveRecord_Relation
>> User.paginate(page: nil).class.superclass
=> ActiveRecord::Relation
>> User.all.class
=> User::ActiveRecord_Relation

 

【10.3.4 ユーザー一覧のテスト メモと演習】

 ページネーションには他にもKaminariやPagyなど、いろいろなメソッドがある。(今後試してみよう)

1. 試しにリスト 10.45にあるページネーションのリンク (will_paginateの部分) を2つともコメントアウトしてみて、リスト 10.48のテストが redに変わるかどうか確かめてみましょう。
→ 当然REDです。

 
2. 先ほどは2つともコメントアウトしましたが、1つだけコメントアウトした場合、テストが greenのままであることを確認してみましょう。will_paginateのリンクが2つとも存在していることをテストしたい場合は、どのようなテストを追加すれば良いでしょうか? ヒント: 表 5.2を参考にして、数をカウントするテストを追加してみましょう。
→ GREENのままなので、count: 2を追加。

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

 

【10.3.5 パーシャルのリファクタリング 演習】

1. リスト 10.52にあるrenderの行をコメントアウトし、テストの結果が redに変わることを確認してみましょう。
→ REDになります。

 

【10.4.1 管理ユーザー 演習】

1. Web経由でadmin属性を変更できないことを確認してみましょう。具体的には、リスト 10.56に示したように、PATCHを直接ユーザーのURL (/users/:id) に送信するテストを作成してみてください。テストが正しい振る舞いをしているかどうか確信を得るために、まずはadminをuser_paramsメソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。最初のテストの結果は redになるはずです。
→ いまいちFILL_INに何を入れたらいいか分からなかったので調べたところ、下記の解答に。(先にUserコントローラ内のuser_paramsメソッドのpermitに:adminを追加しています)

users_controller_test.rb
  test "should not allow the admin attribute to be edited via the web" do
    log_in_as(@other_user)
    assert_not @other_user.admin?
    patch user_path(@other_user), params: {
                                    user: { password:              @other_user.password,
                                            password_confirmation: @other_user.password,
                                            admin: true } }
    assert_not @other_user.reload.admin?
  end

 あれー?でもGREENになるよ?解答書いてる人たちは本当にREDになったのかな??ってことで調べるとこの記事が。@other_user.passwordやったらあかんやん!ってことで"password"に変えたらREDになりました。その後permitから:adminを消してテストはGREENです。

 

【10.4.2 destroyアクション 演習】

1. 管理者ユーザーとしてログインし、試しにサンプルユーザを2〜3人削除してみましょう。ユーザーを削除すると、Railsサーバーのログにはどのような情報が表示されるでしょうか?
→ データベースから該当IDのユーザーをDELETEしているのが分かります。

 

【10.4.3 ユーザー削除のテスト 演習】

1. 試しにリスト 10.59にある管理者ユーザーのbeforeフィルターをコメントアウトしてみて、テストの結果が redに変わることを確認してみましょう。
→ 無事REDでした。

 

第10章まとめ

・edit,update,deleteを実装。
・indexでユーザー一覧を表示。ページネーションをジェムで実装。
・admin属性を付与しユーザーの管理権限を実装。論理値を返すadmin?が使えるように。
・befoure_actionで特定アクションの前に特定メソッドを実行。(after_actionもあるよ)
・フレンドリーフォワーディング=元行きたかったページリクエスト(GETのみ)をセッションにstoreし、ログイン後にリダイレクト。その後、そのstoreしたセッションは削除。
・db/seeds.rbにサンプルデータ(ユーザー)を作成。

 
 それなりにボリュームのあった第10章が終わりました。これで基本的な機能が一通り実装できました。演習に悩む場面はありますが、調べれば理解できない内容はありません。落ち着いて取り組んでいきましょう。
 次だ次!第11章!…あ、メールアドレス使った認証機能かあ…(遠い目)。こうなってる理由は次章以降で説明します。

 
⇦ 第9章はこちら
学習にあたっての前提・著者ステータスはこちら
 

なんとなくイメージを掴む用語集

・フォワーディング(forwarding)
 何かを転送すること。フレンドリーフォワーディングは「親切な転送」ってところか。

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

Railsチュートリアル備忘録1

環境

macOS Catalina 10.15.5
Rails 6.0.3

Railsチュートリアルとそれに付随するいろいろを書いていきます。
Githubに慣れたいので、チュートリアルは第6版に準拠しています。

1.5.1 Herokuのセットアップとデプロイ

リスト 1.18

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

gem 'rails',      '6.0.3'
gem 'puma',       '4.3.4'
gem 'sass-rails', '5.1.0'
gem 'webpacker',  '4.0.7'
gem 'turbolinks', '5.2.0'
gem 'jbuilder',   '2.9.1'
gem 'bootsnap',   '1.4.5', require: false

group :development, :test do
  gem 'sqlite3', '1.4.1'
  gem 'byebug',  '11.0.1', platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  gem 'web-console',           '4.0.1'
  gem 'listen',                '3.1.5'
  gem 'spring',                '2.1.0'
  gem 'spring-watcher-listen', '2.0.1'
end

group :test do
  gem 'capybara',           '3.28.0'
  gem 'selenium-webdriver', '3.142.4'
  gem 'webdrivers',         '4.1.2'
end

group :production do
  gem 'pg', '1.1.4'
end

上記のように記載変更して$ bundle install --without production実行。

しかし以下のエラー発生。

You have requested:
  spring = 2.1.0

The bundle currently has spring locked at 2.1.1.
Try running `bundle update spring`

If you are updating multiple gems in your Gemfile at once,
try passing them all to `bundle update`

エラーに従いbundle update springしたが、
今度はgemlistのpumaに関するエラー。

An error occurred while installing puma (4.3.4), and Bundler cannot continue.
Make sure that `gem install puma -v '4.3.4' --source 'https://rubygems.org/'` succeeds before
bundling.

% gem install puma -v 4.3.4を実行。

Fetching puma-4.3.4.gem
Building native extensions. This could take a while...
ERROR:  Error installing puma:
    ERROR: Failed to build gem native extension.

    current directory: /Users/user/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/puma-4.3.4/ext/puma_http11
/Users/user/.rbenv/versions/2.7.0/bin/ruby -I /Users/user/.rbenv/versions/2.7.0/lib/ruby/2.7.0 -r ./siteconf20200913-23274-ktgz78.rb extconf.rb
checking for BIO_read() in -lcrypto... yes
checking for SSL_CTX_new() in -lssl... yes
checking for openssl/bio.h... yes
checking for DTLS_method() in openssl/ssl.h... yes
checking for TLS_server_method() in openssl/ssl.h... yes
checking for SSL_CTX_set_min_proto_version in openssl/ssl.h... yes
creating Makefile

current directory: /Users/user/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/puma-4.3.4/ext/puma_http11
make "DESTDIR=" clean

current directory: /Users/user/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/puma-4.3.4/ext/puma_http11
make "DESTDIR="
compiling http11_parser.c
ext/puma_http11/http11_parser.c:44:18: warning: unused variable 'puma_parser_en_main' [-Wunused-const-variable]
static const int puma_parser_en_main = 1;
                 ^
1 warning generated.
compiling io_buffer.c
compiling mini_ssl.c
mini_ssl.c:145:7: warning: unused variable 'min' [-Wunused-variable]
  int min, ssl_options;
      ^
mini_ssl.c:299:40: warning: function 'raise_error' could be declared with attribute 'noreturn' [-Wmissing-noreturn]
void raise_error(SSL* ssl, int result) {
                                       ^
2 warnings generated.
compiling puma_http11.c
puma_http11.c:203:22: error: implicitly declaring library function 'isspace' with type 'int (int)' [-Werror,-Wimplicit-function-declaration]
  while (vlen > 0 && isspace(value[vlen - 1])) vlen--;
                     ^
puma_http11.c:203:22: note: include the header <ctype.h> or explicitly provide a declaration for 'isspace'
1 error generated.
make: *** [puma_http11.o] Error 1

make failed, exit code 2

Gem files will remain installed in /Users/user/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/puma-4.3.4 for inspection.
Results logged to /Users/user/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/extensions/x86_64-darwin-19/2.7.0/puma-4.3.4/gem_make.out

改めて$ bundle installしたが以下ループ。
$ gem listでもpuma 4.3.4になっている。

いろいろ調べた結果、下記記事を参考。
https://qiita.com/aiandrox/items/9389696ebc3cc6d3422e

puma4.3.6で対応したそうなのでGemfile変更。

Gemfile
gem 'puma',       '4.3.6'

$ bundle updateの後、
問題なく$ bundle installが通った。

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

rails チュートリアル

6章まで終了
次回7章から

mailの大文字小文字問題が消化不良

データベースのindexについてあとで調べる必要あり

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