20200929のRubyに関する記事は22件です。

windowsでrubyが使えるようにするまで...

Windows10でRubyを使うために、rbenv install 2.7.1ができるようになるまでが長い道のりだったのでまとめました。

環境

  • Windows10
  • Ubuntu

概要

実施することは以下になります。

  1. 初期化設定:echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
  2. 設定の保存:source ~/.bash_profile
  3. rbenvのインストール:brew install rbenv ruby-build
  4. バージョンを指定してインストール:rbenv install 2.7.1
  5. 使用するrbenvのバージョンを設定:rbenv global 2.7.1
  6. rubyのインストール:sudo apt install ruby

これらのコマンドがエラーを出さずに全部通れば、以降の話は何も関係ありません。
初めてのセッティング故に環境が整っておらず、発生したエラーとその対処を記していきます。同じエラーがあれば参考にしていただければと思います。
上から順番にコマンドを入力していってます。

目次

  1. .bash_profileの設定
  2. Homebrewをインストール
  3. rbenvをインストール

.bash_profileの設定

  • echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
エラー:No such file or directory
原因:.bash_profileファイルが存在していないところに書き足そうとした
対応:.bash_profileファイルを作成する

対処:touch ~/.bash_profile

  • echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
  • source ~/.bash_profile
エラー:Command 'rbenv' not found, but can be installed with: sudo apt install rbenv
原因:$(rbenv init -)でrbenvコマンドをしようとしているが、rbenvがわからない
対応:rbenvをインストールする

対処:brew install rbenv ruby-build

エラー:Command 'brew' not found 
原因:brewがわからない
対応:Homebrewをインストールする

Homebrewをインストール

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

  • brew -v
    (brewコマンドが使えるか確認)

エラー:Command 'brew' not found
原因:インストールされたHomebrewがどこにあるのかわかっていない。
対応:Homebrewまでのパスを通してあげる

対処:echo 'export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"' >> ~/.bashrc
対処:source ~/.bashrc

  • brew -v(Homebrew 2.5.2と表示される)

rbenvをインストール

  • brew install rbenv ruby-build
  • source ~/.bash_profile(rbenvがインストールされたので、.bash_profileの書き換えを保存)
  • rbenv install --list-all(インストールできるるrbenvのバージョンの一覧を表示)
  • rbenv install 2.7.1(今回はVer. 2.7.1をインストール)
エラー:configure: error: C compiler cannot create executables
原因:Homebrewに必要なものが足りていなかった
対応:足りないものをインストールする

対処:sudo apt install build-essential curl file git

エラー:E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?
原因:apt-getが最新になっていない
対応:apt-getをupdateする

対処:sudo apt-get update

  • sudo apt install build-essential curl file git
  • rbenv install 2.7.1
エラー:ERROR: Ruby install aborted due to missing extensions. Try running apt-get install -y zlib1g-dev to fetch missing dependencies.
原因:zlib1g-devがないらしい
対応:zlib1g-devをインストールする

対処:sudo apt-get install -y zlib1g-dev

  • rbenv install 2.7.1
  • rbenv versions(指定したバージョンのrbenvがインストールできていることを確認)
  • rbenv global 2.7.1(rbenvのバージョンを全体に反映させる。特定のディレクトリのみならglobalではなくlocalで設定。)
  • ruby -v
エラー:Command 'ruby' not found, but can be installed with: sudo apt install ruby
原因:ruby自体がまだない
対応:rubyをインストールする

対処:sudo apt install ruby

  • ruby -v(ruby 2.7.1p83と表示される)

最後に

原因や対応として間違っているものがあれば教えていただけるととてもありがたいです。
何かの参考になれば幸いです!

参考にさせていただいたもの

とても助けていただきました、ありがとうございました。

今回の疑問点

調べてまとめるためのメモ

  • rubyのバージョンって基本最新をインストールしたらいいの?
  • sudo, aptってなに?
  • brewってなに?
  • yarnとかnpmとかとどう違うの?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

windowsでrubyが使えるようになるまで...

Windows10でRubyを使うために、rbenv install 2.7.1ができるようになるまでが長い道のりだったのでまとめました。

環境

  • Windows10
  • Ubuntu

概要

実施することは以下になります。

  1. 初期化設定:echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
  2. 設定の保存:source ~/.bash_profile
  3. rbenvのインストール:brew install rbenv ruby-build
  4. バージョンを指定してインストール:rbenv install 2.7.1
  5. 使用するrbenvのバージョンを設定:rbenv global 2.7.1
  6. rubyのインストール:sudo apt install ruby

これらのコマンドがエラーを出さずに全部通れば、以降の話は何も関係ありません。
初めてのセッティング故に環境が整っておらず、発生したエラーとその対処を記していきます。同じエラーがあれば参考にしていただければと思います。
上から順番にコマンドを入力していってます。

目次

  1. .bash_profileの設定
  2. Homebrewをインストール
  3. rbenvをインストール

.bash_profileの設定

  • echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
エラー:No such file or directory
原因:.bash_profileファイルが存在していないところに書き足そうとした
対応:.bash_profileファイルを作成する

対処:touch ~/.bash_profile

  • echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
  • source ~/.bash_profile
エラー:Command 'rbenv' not found, but can be installed with: sudo apt install rbenv
原因:$(rbenv init -)でrbenvコマンドをしようとしているが、rbenvがわからない
対応:rbenvをインストールする

対処:brew install rbenv ruby-build

エラー:Command 'brew' not found 
原因:brewがわからない
対応:Homebrewをインストールする

Homebrewをインストール

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

  • brew -v(brewコマンドが使えるか確認)

エラー:Command 'brew' not found
原因:インストールされたHomebrewがどこにあるのかわかっていない。
対応:Homebrewまでのパスを通してあげる

対処:echo 'export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"' >> ~/.bashrc
対処:source ~/.bashrc

  • brew -v
エラー:Command 'brew' not found
原因:.bash_profileファイルにもパスを通す必要がある
対応:.bash_profileファイルに設定を追記

対処:echo export PATH='/usr/local/bin:$PATH' >> ~/.bash_profile
対処:source ~/.bash_profile

  • brew -v(Homebrew 2.5.2と表示される)

rbenvをインストール

  • brew install rbenv ruby-build
  • source ~/.bash_profile(rbenvがインストールされたので、.bash_profileの書き換えを保存)
  • rbenv install --list-all(インストールできるるrbenvのバージョンの一覧を表示)
  • rbenv install 2.7.1(今回はVer. 2.7.1をインストール)
エラー:configure: error: C compiler cannot create executables
原因:Homebrewに必要なものが足りていなかった
対応:足りないものをインストールする

対処:sudo apt install build-essential curl file git

エラー:E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?
原因:apt-getが最新になっていない
対応:apt-getをupdateする

対処:sudo apt-get update

  • sudo apt install build-essential curl file git
  • rbenv install 2.7.1
エラー:ERROR: Ruby install aborted due to missing extensions. Try running apt-get install -y zlib1g-dev to fetch missing dependencies.
原因:zlib1g-devがないらしい
対応:zlib1g-devをインストールする

対処:sudo apt-get install -y zlib1g-dev

  • rbenv install 2.7.1
  • rbenv versions(指定したバージョンのrbenvがインストールできていることを確認)
  • rbenv global 2.7.1(rbenvのバージョンを全体に反映させる。特定のディレクトリのみならglobalではなくlocalで設定。)
  • ruby -v
エラー:Command 'ruby' not found, but can be installed with: sudo apt install ruby
原因:ruby自体がまだない
対応:rubyをインストールする

対処:sudo apt install ruby

  • ruby -v(ruby 2.7.1p83と表示される)

最後に

原因や対応として間違っているものがあれば教えていただけるととてもありがたいです。
何かの参考になれば幸いです!

参考にさせていただいたもの

とても助けていただきました、ありがとうございました。

今回の疑問点

調べてまとめるためのメモ

  • rubyのバージョンって基本最新をインストールしたらいい?
  • sudo, aptとは?
  • brewとは?
  • yarnとかnpmとかとどう違う?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

もう迷わない!RSpec導入の流れ

RSpecとは

 Railsにおいて、単体テストコードを記述するときに用いるGemのこと。

テストコードを記述する直前までの大まかな流れ

  1. Gemfileに追記
  2. Gemをインストール
  3. rspecをインストール
  4. テストコードをターミナル上で可視化できるようにする
  5. FactoryBotとFakerのGemをGemfileに追記
  6. Gemのインストール
  7. (テスト用の画像を用意)

1. Gemfileに追記

gem 'rspec-rails'と記述する。
必ず、group :development, :test do ~ endのグループ内に

Gemfile
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails'
end

2. Gemをインストール

ターミナルでカレントディレクトリがテストコードを使用するアプリケーションのディレクトリであることを確認の上

bundle install

3. rspecをインストール

rails g rspec:install

これによって、「specディレクトリ」や「.rspecファイル」が生成される

4. テストコードをターミナル上で可視化できるようにする

.rspec
--format documentation

を追記する。

5. FactoryBotとFakerのGemをGemfileに追記

必要に応じてGemを導入。今回はチャットアプリを想定しているため、導入。

Gemfile
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'faker'
end

先ほどと同じgroupの中に記述。

6. Gemのインストール

bundle install

7. (テスト用の画像を用意)

public/imagesにファイル名「test_image.png」で配置。

ポイント

  • rspecは「bundle install」だけでなく、「rails g rspec:install」をして初めて使える。
  • 記述するのは、group :development, :test do ~ endのグループ内。

最後に

bundle installはRailsに取り込んだだけ!
rails g rspec:installで解凍するイメージ!

細かいテストコードはまた別の機会に。

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

Ruby で解く AtCoder ACL Beginner Contest C UnionFind(DSU)

はじめに

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

今回のお題

AtCoder Beginner Contest C - Connect Cities
Difficulty: 363

今回のテーマ、UnionFind

つい最近、同様の問題 があったり、AtCoder Library Practice Contest があったりとは言え、灰レートは凄いです。
ちなみに、前回はDifficulty: 676でした。

Ruby

ruby.rb
class UnionFind
  def initialize(n)
    @parents = Array.new(n, -1)
  end
  def find(x)
    @parents[x] < 0 ? x : @parents[x] = find(@parents[x])
  end
  def parents
    @parents
  end
  def union(x, y)
    x = find(x)
    y = find(y)
    return if x == y
    if @parents[x] > @parents[y]
      x, y = y, x
    end
    @parents[x] += @parents[y]
    @parents[y] = x
  end
end

n, m = gets.split.map(&:to_i)
u = UnionFind.new(n)
m.times do
  a, b = gets.split.map(&:to_i)
  u.union(a - 1, b - 1)
end
puts u.parents.select{ _1 < 0 }.size - 1
sub.rb
puts u.parents.select{ _1 < 0 }.size - 1 # 今回

puts -u.parents.min # 前回

前回 --Qiitaの最終行を変更するだけでACします。

sub.rb
@parents = [-3, 0, -2, 2, 0]

前回の入力例 1で説明しますと、u.parentsの中は、u.parents[0]u.parents[2]が代表の2グループに分けれらますので、それを1つの道路で結べばOK。

AtCoder Library (ACL)のRuby版

ここ --githubで、ACLのRuby版を作成されている動きもあります。
要注目ですね。

Ruby
コード長 (Byte) 567
実行時間 (ms) 170
メモリ (KB) 15612

まとめ

  • ACLBC C を解いた
  • Ruby に詳しくなった

参照したサイト
Ruby で解く AtCoder ABC177 D UnionFind --Qiita

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

【Ruby on Rails】deviseの不要なルートを作らない方法

目標

deviseで自動的に作成される不要なルートを作らない。
→新たな新規adminの作成を防ぐ。
※管理者(admin)情報はseedにて記述する場合。

開発環境

ruby 2.5.7
Rails 5.2.4.3
OS: macOS Catalina

前提

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

seedに記述

seedに管理者の情報を記述。

db/seeds.rb
...

Admin.create!(
  email: 'aa@aa.com',
  password: 'aaaaaa',
)
ターミナル
$ rails db:seed

これで管理者ページにログインできます。
※seedに関しては開発環境と本番環境で分ける必要があります。
詳しくはこちら

routesを編集

config/routes.rb
  devise_for :admins, :skip => [:registrations, :password],controllers: {
    sessions: 'admins/sessions',
  }

ログインページがデフォルトの状態の場合

このままではエラーが出てしまうので、下記ファイルをの該当箇所を削除。

app/views/admins/shared/_links.html.erb
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
  <%= link_to "Sign up", new_registration_path(resource_name) %><br />
<% end %>

<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
  <%= link_to "Forgot your password?", new_password_path(resource_name) %><br />
<% end %>

参考

サインアップするためにDeviseルートを削除するにはどうすればよいですか?

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

ajaxを使ってタグの追加と削除、(成功・エラー)メッセージの表示を行う。

環境

Ruby 2.5.7
Rails 5.2.4

ライブラリ

jQuery 1.12.4

前提

今回はTagテーブルを使って解説していきます。

まずはajaxを使わなくてもタグの追加と削除ができることを事前にご確認ください。

エラーメッセージの日本語化やアソシエーションについては触れませんので必要な方はご自身でお願い致します。

turbolinksは無効にしています。(有効時の動作は確認していません。)

手順

タイトルにもある通り、次の手順でajaxを利用してタグの追加・削除・エラーメッセージの表示を実装していきます。

1.tag.rbにバリデーションの設定
2.部分テンプレートの用意(views/layouts/_error_messages.html.erb, views/layouts/_flash_messages.html.erb, views/tags/_tag.html.erb)
3.タグの一覧画面、兼新規作成フォームを作成(views/tags/index.html.erb)
4.アクションごとのjs.erbを用意(views/tags/create.js.erb, views/tags/destroy.js.erb)
5.tags_controller.rb(:index, :create, :destroy)の用意

イメージとしては、送信ボタンを押すとコントローラーのcreateアクションが実行
createアクション内でcreate.js.erbを呼び出すメソッドが実行
create.js.erb内に書かれているJavaScript(jQuery)が発火
DOM操作により、エラーメッセージの要素とタグ一覧の要素が上書き(jQueryの.html()メソッド)
削除時のdestroyアクションも同様です。

完成形の流れとしては
1.index画面でフォームに値を入力する。
1-1.存在しないタグなら新規保存(サクセスメッセージ)
1-2.存在するタグなら保存失敗(バリデーションエラーメッセージ)
1-3.空白なら保存失敗(バリデーションエラーメッセージ)

2.個別のタグを削除する
1-1.サクセスメッセージを表示

全てajaxのため、ページの全体の更新はありません。

1.tag.rbにバリデーションの設定

tag.rb
class Tag < ApplicationRecord

  ...

  # 空白の禁止と一意性(重複NG)を付与します。
  validates :name, presence: true, uniqueness: true

end

2.部分テンプレートの用意(layouts/_error_messages.html.erb, layouts/_flash_messages.html.erb, _tag.html.erb)

ここで作る3つの部分テンプレートを、アクションに応じてDOM操作で上書きしていく形です。
jQueryで書くと1行1行DOM操作をすることになって冗長になってしまうので、部分テンプレートを利用して後述するjQueryが<%= render ... %>1行で済むようになっています。

バリデーションエラーメッセージ用の部分テンプレート
すでにある方は流用してもらって大丈夫です。

layouts/_error_messages.html.erb
<% if model.errors.any? %>
  <div id="validation-errors">
    <p><%= model.errors.count %>件のエラーがあります。</p>
    <ul class="error-messages">
      <% model.errors.full_messages.each do |message| %>
        <li class="error-message"><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

jQueryのDOM操作によって上書きされた時にバリデーションエラーが発生していれば、エラーメッセージが表示されます。
ローカル変数のmodelは後述するindex.html.erbで@tag(Tag.new)に置き換えます。

次にサクセスメッセージ(フラッシュメッセージ)用の部分テンプレート
こちらもすでにある方は流用してください。

layouts/_flash_messages.html.erb
<% flash.each do |key, value| %>
  <p class="alert alert-<%= key %>">
    <%= value %>
  </p>
<% end %>

eachで配列から取得することになっていますが、これは複数のフラッシュメッセージを取得するというよりも、どのようなキー(flash[キー])でもこの一文で再表示ができるようになっています。
今回使うキーは[:success], [:warning]の2種類です。(ちなみにこの2種類はbootstrapでデザインも用意されているため、今回選択しました。)
この二つを使う場合は部分テンプレートを下記のように書き換えることもできます。

layouts/_flash_messages.html.erb
<% case flash.keys[0] %>
  <% when "success" %>
  <p class="alert alert-<%= flash.keys[0] %>">
    <%= flash[:success] %>
  </p>
  <% when "warning" %>
  <p class="alert alert-<%= flash.keys[0] %>">
    <%= flash[:warning] %>
  </p>
<% end %>

flashに格納されるキーは配列のため、keys[0]とする必要があります。
case文でkeyパターンに応じて表示するフラッシュメッセージを変化させます。
p要素のclassでkeys[0]を使用している理由は、キーによって(今回の場合はsuccessかwarning)cssのデザインを変更するためです。

しかしこれでは、キーの種類(キーの名前は必要に応じて任意でつけることができます。)が増えた時にその都度行を増やしていく必要があるため、冗長になってしまいます。

そこで今回はeach文を使用することで、その手間をなくした形です。
また部分テンプレート化することで、バリデーションエラーメッセージと同様に、他のviewでも使い回すことができます。

次にタグ一覧を表示する部分テンプレート

tags/_tag.html.erb
<div class="tags-index__tags--tag">
  <%= tag.name %>(<%= tag.menu_tag_ids.count %>)
  <%= link_to 'X', tag_path(tag), method: :delete, remote: true %>
</div>

こちらも部分テンプレートを使わなければ後述するjQueryで4行分書く必要がり、jQueryでのDOM操作は極力シンプルに管理したいので部分テンプレート化しています。
ローカル変数のtagはindex.html.erbでインスタンス変数@tags(Tag.all)を指定することで、部分テンプレート内では個別のローカル変数tagとして自動で置き換わります。(index.html.erbのところで詳しく解説します。)
(<%= tag.menu_tag_ids.count %>)はタグ名の横にそのタグが使われているメニューの数を表示しています。(メニューテーブル(割愛) 1:多 メニュータグテーブル(中間テーブル)(割愛) 多:1 タグテーブル(今回))
<%= link_to 'X', tag_path(tag), method: :delete, remote: true %>を押すとdestroyアクションが発動します。remote: trueとすることで、ajax通信であることを明示します。

"remote: true"の仕組み
通常のリクエスト(リンク)ではHTMLを取得しますが、remote: true(html変換後はdata-remote="true"属性)が付与されたリクエストの場合はJSファイルのみを取得します。
Railsの場合はlayouts/application.html.erbの<head>タグ内にjsを読みこむタグがあるので、通常はHTMLを取得するとjsも自動で再読み込みがされます。なお、turbolinksを使っている場合はこの限りではありません。

3.タグの一覧画面、兼新規作成フォームを作成(views/tags/index.html.erb)

タグの一覧画面とその中に新規入力のフォームも作成していきます。

views/tags/index.html.erb
<div class="contents tags-index">
  <div class="tags-index--messages"><!--メッセージの表示エリア--></div>
  <h2>タグ一覧</h2>
  <div class="tags-index__list" id="tags-index--tag-list">
    <%= render @tags %>
  </div>
  <div class="tags-index__form">
    <%= form_with model: @tag, url: tags_path(@tag) do |f| %>
      <%= f.text_field :name %>
      <%= f.submit %>
    <% end %>
  </div>
</div>

タグ一覧の下に新規追加フォームがくっついてる状態です。

新規登録フォームはform_withを利用します。
form_withはデフォルトがremote: true属性を持っているので省略可能です。(form_forを使う場合はremote: true属性を明示する必要があります。)
form_withでajaxを使わない場合はlocal: true属性を明示する必要があります。
フォームヘルパーはremote: trueの場合はJSのみを、local: trueの場合はHTMLをアクションに伝えます。
また、フォームヘルパーはデフォルトのHTTPメソッドがPOSTでもあるのでmethod: :post省略可能です。

<%= render @tags %>のインスタンス変数 @tagsは後述するコントローラー側でTag.allを取得しています。
見慣れない部分テンプレートの書き方ですが、この略記法の部分テンプレートは変数名に対応する_変数名.html.erbが呼び出され、その部分テンプレート内ではローカル変数(今回でいう部分テンプレート内のtag)として使えるようになります。
今回の@tagsように配列で取得した場合は、部分テンプレート内ではその配列の数だけ、繰り返し表示がされます。
これは書き換えると以下のようになります。

views/tags/index.html.erb
<%= render partial: 'tag', locals: {tags: @tags} %>
views/tags/_tag.html.erb
<% tags.each do |tag| %>
  <div class="tags-index__tags--tag">
    <%= tag.name %>(<%= tag.menu_tag_ids.count %>)
    <%= link_to 'X', tag_path(tag), method: :delete, remote: true %>
  </div>
<% end %>

今回の記述方法はこれらを略記した形です。
Railsガイド - レイアウトとレンダリング

4.アクションごとのjs.erbを用意(views/tags/create.js.erb, views/tags/destroy.js.erb)

コントローラーから呼び出すjs(.erb)を作成します。
ここにjsを記述することで、コントローラーからcreate, destroyがそれぞれ呼び出された時に発火するjQueryでDOM操作が行われます。
ここで使われている.html()はそこに要素が何もなければ追加となり、すでに別の要素があれば書き換えとなります。

createアクション時のjs(.erb)

views/tags/create.js.erb
<% if @tag.errors.any? %>
  $(".tags-index--messages").html("<p><%= j(render partial: 'layouts/error_messages', locals: {model: @tag}) %></p>")
<% else %>
  $(".tags-index--messages").html("<p><%= j(render partial: 'layouts/flash_messages', locals: {model: @tag}) %></p>")
  $("#tags-index--tag-list").html("<%= j(render @tags) %>")
  $("#tag_name").val('')
<% end %>

<% if @tag.errors.any? %>はsubimitボタンが押された時に@tagにエラーが発生しているかどうかをチェックします。

"オブジェクト.errors.any?"と"オブジェクト.invalid?"の違い
今回の場合、コントローラー側で@tag.saveが試された後にcreate.js.erbを呼んでいるため@tagにはすでにerrorが発生しているかどうかがわかる状態でjs.erbに渡って来ています。
他方、invalid?メソッドは保存前のオブジェクトに対して手動でバリデーションチェックを行うものです。
通常はオブジェクトが保存されようとした時(.saveを試みた時)に自動でバリデーションチェックが実行され、今回は.saveの成否によってアクションを分けるため、改めて手動でinvalid?をするよりは、.saveの時点ですでにエラーが発生しているかどうか(.errors.any?)を確認した方がDRYに則っていると言えます。

エラーが発生している場合(空白やタグ名の重複)
・バリデーションエラーメッセージ(error_messages.html.erb)を$(".tags-index--messages")に追加または書き換え

エラーが発生していない場合(正常に保存ができた)は
・フラッシュメッセージ(flash_message.html.erb)を$(".tags-index--messages")に追加または書き換え
・タグ一覧($("#tags-index--tag-list"))を書き換え
・フォームに入力されたタグ名をクリア($("#tag_name").val(''))

が、実行されます。

destroyアクション時のjs(.erb)

views/tags/destroy.js.erb
$(".tags-index--messages").html("<p><%= j(render partial: 'layouts/flash_messages', locals: {model: @tag}) %></p>")
$("#tags-index--tag-list").html("<%= j(render @tags) %>")

create時と同じです。
・タグが削除されたらフラッシュメッセージの追加または書き換え
・タグ一覧の書き換え

が、実行されます。

"partial: "の必要性renderを使ってオプションの追加(今回でいうlocals: {tag: @tag})が必要な場合はファイル名(パス)の前にpartial:を明示する必要があります。
Railsガイド - レイアウトとレンダリング

5.tags_controller.rb(:index, :create, :destroy)の用意

コントローラーに今回必要なアクションを記載していきます

tags_controller.rb
class TagsController < ApplicationController

  def index
    @tags = Tag.all
    @tag = Tag.new
  end

  def create
    @tags = Tag.all
    @tag = Tag.new(tag_params)

    respond_to do |format|
      if @tag.save
        format.js { flash.now[:success] = "保存しました。" }
      else
        format.js
      end
    end
  end

  def destroy
    @tags = Tag.all
    @tag = Tag.new
    Tag.find(params[:id]).destroy
    flash.now[:warning] = "削除しました。"
  end

  private
  def tag_params
    params.require(:tag).permit(:name)
  end

end

今回はcreateのform_with、destroyのlink_to、それぞれでremote: true(form_withの場合はデフォルトなので省略)を明示しているため、どちらもJSのみをリクエストしていることになります。

tags_controller.rb
def create

  ...

  respond_to do |format|
    if @tag.save
      format.js { flash.now[:success] = "保存しました。" }
    else
      format.js
    end
  end
end

respond_to do |format|はリクエストの形式によって処理を分けるブロックパラメータです。
今回、タグの追加・削除についてはHTMLで要求が来る(local: true)ことは想定していませんのでformat.js {処理}のみを記載しています。
HTMLでの要求が来た時に処理を分けたい場合はformat.html {処理}を書けば、リクエストの方式に応じで処理を分けることができます。
if @tag.saveが成功した場合はflash[:success]キーに"保存しました。"というメッセージを載せてcreate.js.erbに渡します。
失敗した場合はバリデーションエラーメッセージのみを表示するため、フラッシュメッセージは使いませんので、format.jsの後ろに処理は書かず、表示のみを行います。
前述しましたが、この時にバリデーションエラーが発生している@tagをcreate.js.erbに渡して、そこでエラー処理(バリデーションエラーメッセージの表示)をしています。

destroyアクションではflash[:warning]キーに"削除しました。"というメッセージを載せてdestroy.js.erbに渡します。

"format.js"を書く場合と書かない場合
通常、コントローラーにはviewファイルに対応する同名のアクション名を作ります。(def index end ならindex.html.erb)
アクション名の中身で特にrenderredirect_toなどのに指定がなければアクションの最後にrender :アクション名が省略されて(暗示的に)実行されています。
(def index endなど、中身を書かなくてもviewがレンダリングされるのはこのためです。)
今回、createアクションでは、条件によってフラッシュメッセージを渡したいので明示的にformat.jsを記述し、その中で処理を分けていましたが、destroyアクションでは今のところ処理を分ける必要はないので、respond_toformat.jsrender :destroyは記述していません。
render :destroyに関してはすでに暗示的に記述されているため、書く必要がないということです。最初に書きましたが、暗示的にrender :アクション名が発動するのはアクションの中で他にrender :別のアクション名redirect_toがない場合に限るので、他方createアクションのように明示的にformat.jsを記述し実行された場合、アクションの最後に暗示的なrender :createが実行されることはありません。(DoubleRenderingErrorにならない理由です。)

Railsガイド - Action Controller の概要
Pikawaka - resoond_to
Qiita - 【Rails】flashメッセージを使用して簡易メッセージを表示させる詳しい方法と解説

まとめ

質問や解釈の違い、記述方法に違和感ありましたら、コメント等でご指摘いただけると幸いです。

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

参考サイト

Railsガイド - レイアウトとレンダリング
Railsガイド - Action Controller の概要
Pikawaka - resoond_to
Qiita - 【Rails】flashメッセージを使用して簡易メッセージを表示させる詳しい方法と解説

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

【Ruby on Rails】ajaxを使ってタグの追加と削除、(成功・エラー)メッセージの表示を行う。

環境

Ruby 2.5.7
Rails 5.2.4

ライブラリ

jQuery 1.12.4

前提

今回はTagテーブルを使って解説していきます。

まずはajaxを使わなくてもタグの追加と削除ができることを事前にご確認ください。

エラーメッセージの日本語化やアソシエーションについては触れませんので必要な方はご自身でお願い致します。

turbolinksは無効にしています。(有効時の動作は確認していません。)

手順

タイトルにもある通り、次の手順でajaxを利用してタグの追加・削除・エラーメッセージの表示を実装していきます。

1.tag.rbにバリデーションの設定
2.部分テンプレートの用意(views/layouts/_error_messages.html.erb, views/layouts/_flash_messages.html.erb, views/tags/_tag.html.erb)
3.タグの一覧画面、兼新規作成フォームを作成(views/tags/index.html.erb)
4.アクションごとのjs.erbを用意(views/tags/create.js.erb, views/tags/destroy.js.erb)
5.tags_controller.rb(:index, :create, :destroy)の用意

イメージとしては、送信ボタンを押すとコントローラーのcreateアクションが実行
createアクション内でcreate.js.erbを呼び出すメソッドが実行
create.js.erb内に書かれているJavaScript(jQuery)が発火
DOM操作により、エラーメッセージの要素とタグ一覧の要素が上書き(jQueryの.html()メソッド)
削除時のdestroyアクションも同様です。

完成形の流れとしては
1.index画面でフォームに値を入力する。
1-1.存在しないタグなら新規保存(サクセスメッセージ)
1-2.存在するタグなら保存失敗(バリデーションエラーメッセージ)
1-3.空白なら保存失敗(バリデーションエラーメッセージ)

2.個別のタグを削除する
1-1.サクセスメッセージを表示

全てajaxのため、ページの全体の更新はありません。

1.tag.rbにバリデーションの設定

tag.rb
class Tag < ApplicationRecord

  ...

  # 空白の禁止と一意性(重複NG)を付与します。
  validates :name, presence: true, uniqueness: true

end

2.部分テンプレートの用意(layouts/_error_messages.html.erb, layouts/_flash_messages.html.erb, _tag.html.erb)

ここで作る3つの部分テンプレートを、アクションに応じてDOM操作で上書きしていく形です。
jQueryで書くと1行1行DOM操作をすることになって冗長になってしまうので、部分テンプレートを利用して後述するjQueryが<%= render ... %>1行で済むようになっています。

バリデーションエラーメッセージ用の部分テンプレート
すでにある方は流用してもらって大丈夫です。

layouts/_error_messages.html.erb
<% if model.errors.any? %>
  <div id="validation-errors">
    <p><%= model.errors.count %>件のエラーがあります。</p>
    <ul class="error-messages">
      <% model.errors.full_messages.each do |message| %>
        <li class="error-message"><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

jQueryのDOM操作によって上書きされた時にバリデーションエラーが発生していれば、エラーメッセージが表示されます。
ローカル変数のmodelは後述するindex.html.erbで@tag(Tag.new)に置き換えます。

次にサクセスメッセージ(フラッシュメッセージ)用の部分テンプレート
こちらもすでにある方は流用してください。

layouts/_flash_messages.html.erb
<% flash.each do |key, value| %>
  <p class="alert alert-<%= key %>">
    <%= value %>
  </p>
<% end %>

eachで配列から取得することになっていますが、これは複数のフラッシュメッセージを取得するというよりも、どのようなキー(flash[キー])でもこの一文で再表示ができるようになっています。
今回使うキーは[:success], [:warning]の2種類です。(ちなみにこの2種類はbootstrapでデザインも用意されているため、今回選択しました。)
この二つを使う場合は部分テンプレートを下記のように書き換えることもできます。

layouts/_flash_messages.html.erb
<% case flash.keys[0] %>
  <% when "success" %>
  <p class="alert alert-<%= flash.keys[0] %>">
    <%= flash[:success] %>
  </p>
  <% when "warning" %>
  <p class="alert alert-<%= flash.keys[0] %>">
    <%= flash[:warning] %>
  </p>
<% end %>

flashに格納されるキーは配列のため、keys[0]とする必要があります。
case文でkeyパターンに応じて表示するフラッシュメッセージを変化させます。
p要素のclassでkeys[0]を使用している理由は、キーによって(今回の場合はsuccessかwarning)cssのデザインを変更するためです。

しかしこれでは、キーの種類(キーの名前は必要に応じて任意でつけることができます。)が増えた時にその都度行を増やしていく必要があるため、冗長になってしまいます。

そこで今回はeach文を使用することで、その手間をなくした形です。
また部分テンプレート化することで、バリデーションエラーメッセージと同様に、他のviewでも使い回すことができます。

次にタグ一覧を表示する部分テンプレート

tags/_tag.html.erb
<div class="tags-index__tags--tag">
  <%= tag.name %>(<%= tag.menu_tag_ids.count %>)
  <%= link_to 'X', tag_path(tag), method: :delete, remote: true %>
</div>

こちらも部分テンプレートを使わなければ後述するjQueryで4行分書く必要がり、jQueryでのDOM操作は極力シンプルに管理したいので部分テンプレート化しています。
ローカル変数のtagはindex.html.erbでインスタンス変数@tags(Tag.all)を指定することで、部分テンプレート内では個別のローカル変数tagとして自動で置き換わります。(index.html.erbのところで詳しく解説します。)
(<%= tag.menu_tag_ids.count %>)はタグ名の横にそのタグが使われているメニューの数を表示しています。(メニューテーブル(割愛) 1:多 メニュータグテーブル(中間テーブル)(割愛) 多:1 タグテーブル(今回))
<%= link_to 'X', tag_path(tag), method: :delete, remote: true %>を押すとdestroyアクションが発動します。remote: trueとすることで、ajax通信であることを明示します。

"remote: true"の仕組み
通常のリクエスト(リンク)ではHTMLを取得しますが、remote: true(html変換後はdata-remote="true"属性)が付与されたリクエストの場合はJSファイルのみを取得します。
Railsの場合はlayouts/application.html.erbの<head>タグ内にjsを読みこむタグがあるので、通常はHTMLを取得するとjsも自動で再読み込みがされます。なお、turbolinksを使っている場合はこの限りではありません。

3.タグの一覧画面、兼新規作成フォームを作成(views/tags/index.html.erb)

タグの一覧画面とその中に新規入力のフォームも作成していきます。

views/tags/index.html.erb
<div class="contents tags-index">
  <div class="tags-index--messages"><!--メッセージの表示エリア--></div>
  <h2>タグ一覧</h2>
  <div class="tags-index__list" id="tags-index--tag-list">
    <%= render @tags %>
  </div>
  <div class="tags-index__form">
    <%= form_with model: @tag, url: tags_path(@tag) do |f| %>
      <%= f.text_field :name %>
      <%= f.submit %>
    <% end %>
  </div>
</div>

タグ一覧の下に新規追加フォームがくっついてる状態です。

新規登録フォームはform_withを利用します。
form_withはデフォルトがremote: true属性を持っているので省略可能です。(form_forを使う場合はremote: true属性を明示する必要があります。)
form_withでajaxを使わない場合はlocal: true属性を明示する必要があります。
フォームヘルパーはremote: trueの場合はJSのみを、local: trueの場合はHTMLをアクションに伝えます。
また、フォームヘルパーはデフォルトのHTTPメソッドがPOSTでもあるのでmethod: :post省略可能です。

<%= render @tags %>のインスタンス変数 @tagsは後述するコントローラー側でTag.allを取得しています。
見慣れない部分テンプレートの書き方ですが、この略記法の部分テンプレートは変数名に対応する_変数名.html.erbが呼び出され、その部分テンプレート内ではローカル変数(今回でいう部分テンプレート内のtag)として使えるようになります。
今回の@tagsように配列で取得した場合は、部分テンプレート内ではその配列の数だけ、繰り返し表示がされます。
これは書き換えると以下のようになります。

views/tags/index.html.erb
<%= render partial: 'tag', locals: {tags: @tags} %>
views/tags/_tag.html.erb
<% tags.each do |tag| %>
  <div class="tags-index__tags--tag">
    <%= tag.name %>(<%= tag.menu_tag_ids.count %>)
    <%= link_to 'X', tag_path(tag), method: :delete, remote: true %>
  </div>
<% end %>

今回の記述方法はこれらを略記した形です。
Railsガイド - レイアウトとレンダリング

4.アクションごとのjs.erbを用意(views/tags/create.js.erb, views/tags/destroy.js.erb)

コントローラーから呼び出すjs(.erb)を作成します。
ここにjsを記述することで、コントローラーからcreate, destroyがそれぞれ呼び出された時に発火するjQueryでDOM操作が行われます。
ここで使われている.html()はそこに要素が何もなければ追加となり、すでに別の要素があれば書き換えとなります。

createアクション時のjs(.erb)

views/tags/create.js.erb
<% if @tag.errors.any? %>
  $(".tags-index--messages").html("<p><%= j(render partial: 'layouts/error_messages', locals: {model: @tag}) %></p>")
<% else %>
  $(".tags-index--messages").html("<p><%= j(render partial: 'layouts/flash_messages', locals: {model: @tag}) %></p>")
  $("#tags-index--tag-list").html("<%= j(render @tags) %>")
  $("#tag_name").val('')
<% end %>

<% if @tag.errors.any? %>はsubimitボタンが押された時に@tagにエラーが発生しているかどうかをチェックします。

"オブジェクト.errors.any?"と"オブジェクト.invalid?"の違い
今回の場合、コントローラー側で@tag.saveが試された後にcreate.js.erbを呼んでいるため@tagにはすでにerrorが発生しているかどうかがわかる状態でjs.erbに渡って来ています。
他方、invalid?メソッドは保存前のオブジェクトに対して手動でバリデーションチェックを行うものです。
通常はオブジェクトが保存されようとした時(.saveを試みた時)に自動でバリデーションチェックが実行され、今回は.saveの成否によってアクションを分けるため、改めて手動でinvalid?をするよりは、.saveの時点ですでにエラーが発生しているかどうか(.errors.any?)を確認した方がDRYに則っていると言えます。

エラーが発生している場合(空白やタグ名の重複)
・バリデーションエラーメッセージ(error_messages.html.erb)を$(".tags-index--messages")に追加または書き換え

エラーが発生していない場合(正常に保存ができた)は
・フラッシュメッセージ(flash_message.html.erb)を$(".tags-index--messages")に追加または書き換え
・タグ一覧($("#tags-index--tag-list"))を書き換え
・フォームに入力されたタグ名をクリア($("#tag_name").val(''))

が、実行されます。

destroyアクション時のjs(.erb)

views/tags/destroy.js.erb
$(".tags-index--messages").html("<p><%= j(render partial: 'layouts/flash_messages', locals: {model: @tag}) %></p>")
$("#tags-index--tag-list").html("<%= j(render @tags) %>")

create時と同じです。
・タグが削除されたらフラッシュメッセージの追加または書き換え
・タグ一覧の書き換え

が、実行されます。

"partial: "の必要性renderを使ってオプションの追加(今回でいうlocals: {tag: @tag})が必要な場合はファイル名(パス)の前にpartial:を明示する必要があります。
Railsガイド - レイアウトとレンダリング

5.tags_controller.rb(:index, :create, :destroy)の用意

コントローラーに今回必要なアクションを記載していきます

tags_controller.rb
class TagsController < ApplicationController

  def index
    @tags = Tag.all
    @tag = Tag.new
  end

  def create
    @tags = Tag.all
    @tag = Tag.new(tag_params)

    respond_to do |format|
      if @tag.save
        format.js { flash.now[:success] = "保存しました。" }
      else
        format.js
      end
    end
  end

  def destroy
    @tags = Tag.all
    @tag = Tag.new
    Tag.find(params[:id]).destroy
    flash.now[:warning] = "削除しました。"
  end

  private
  def tag_params
    params.require(:tag).permit(:name)
  end

end

今回はcreateのform_with、destroyのlink_to、それぞれでremote: true(form_withの場合はデフォルトなので省略)を明示しているため、どちらもJSのみをリクエストしていることになります。

tags_controller.rb
def create

  ...

  respond_to do |format|
    if @tag.save
      format.js { flash.now[:success] = "保存しました。" }
    else
      format.js
    end
  end
end

respond_to do |format|はリクエストの形式によって処理を分けるブロックパラメータです。
今回、タグの追加・削除についてはHTMLで要求が来る(local: true)ことは想定していませんのでformat.js {処理}のみを記載しています。
HTMLでの要求が来た時に処理を分けたい場合はformat.html {処理}を書けば、リクエストの方式に応じで処理を分けることができます。
if @tag.saveが成功した場合はflash[:success]キーに"保存しました。"というメッセージを載せてcreate.js.erbに渡します。
失敗した場合はバリデーションエラーメッセージのみを表示するため、フラッシュメッセージは使いませんので、format.jsの後ろに処理は書かず、表示のみを行います。
前述しましたが、この時にバリデーションエラーが発生している@tagをcreate.js.erbに渡して、そこでエラー処理(バリデーションエラーメッセージの表示)をしています。

destroyアクションではflash[:warning]キーに"削除しました。"というメッセージを載せてdestroy.js.erbに渡します。

"format.js"を書く場合と書かない場合
通常、コントローラーにはviewファイルに対応する同名のアクション名を作ります。(def index end ならindex.html.erb)
アクション名の中身で特にrenderredirect_toなどのに指定がなければアクションの最後にrender :アクション名が省略されて(暗示的に)実行されています。
(def index endなど、中身を書かなくてもviewがレンダリングされるのはこのためです。)
今回、createアクションでは、条件によってフラッシュメッセージを渡したいので明示的にformat.jsを記述し、その中で処理を分けていましたが、destroyアクションでは今のところ処理を分ける必要はないので、respond_toformat.jsrender :destroyは記述していません。
render :destroyに関してはすでに暗示的に記述されているため、書く必要がないということです。最初に書きましたが、暗示的にrender :アクション名が発動するのはアクションの中で他にrender :別のアクション名redirect_toがない場合に限るので、他方createアクションのように明示的にformat.jsを記述し実行された場合、アクションの最後に暗示的なrender :createが実行されることはありません。(DoubleRenderingErrorにならない理由です。)

Railsガイド - Action Controller の概要
Pikawaka - resoond_to
Qiita - 【Rails】flashメッセージを使用して簡易メッセージを表示させる詳しい方法と解説

まとめ

質問や解釈の違い、記述方法に違和感ありましたら、コメント等でご指摘いただけると幸いです。

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

参考サイト

Railsガイド - レイアウトとレンダリング
Railsガイド - Action Controller の概要
Pikawaka - resoond_to
Qiita - 【Rails】flashメッセージを使用して簡易メッセージを表示させる詳しい方法と解説

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

Rack::Timeout でパスごとにタイムアウトを指定する

rack-timeout を使用しておりパスごとにタイムアウトの指定を変更するため以前(rails 5.2.3、 rack-timeout 0.5.1 まで)は以下のように指定していました(多少簡略化してあります)。

Gemfile
gem 'rack-timeout', require: 'rack/timeout/base'
config/initializers/rack_timeout.rb
module Rack
  class DynamicTimeout < Rack::Timeout
    def call(env)
      # URL が /admin から始まる場合は 30 秒、それ以外は 10 秒となるように
      @service_timeout = env['REQUEST_URI'].start_with?('/admin') ? 30 : 10

      super(env)
    end
  end
end

timeout_params = {
  service_timeout: 10,
  wait_timeout: 5.minutes.to_i,
  wait_overtime: 5.minutes.to_i,
  service_past_wait: false,
}

Rails.application.config.middleware.insert_before Rack::Runtime, Rack::DynamicTimeout, timeout_params

ところが Rails 6、あるいは rack-timeout 0.6.0 からタイムアウトが動的に指定した service_timeout の値よりも短くなっていました。
以下のようにログに出力されている値(30秒)よりも実際は短かった(10秒)です。

severity:ERROR  message:source=rack-timeout id=0284947e-c24c-4024-a9ba-c542b3eb58f9 timeout=30000ms service=10000ms state=timed_out

色々ログを仕込んだりして原因を調べたところ、どうやら Rack::DynamicTimeoutcall とは別に Rack::Timeout 自体の call が呼ばれているようでした。

都合2回 call メソッドが実行されて最初は Rack::DynamicTimeout で次が Rack::Timeout になっており、最終的にタイムアウトの値は Rack::Timeout で指定された値が使われるという状態です。
どこで呼ばれているか、それを変更できるかというところまで調べていくとちょっと時間がかかりそうだったので、今回は Rack::Timeout を上書きするパッチで対応することにしました。

Gemfile
gem 'rack-timeout'
config/initializers/rack_timeout.rb
module Rack
  module DynamicTimeout
    def call(env)
      # URL が /admin から始まる場合は 30 秒、それ以外は 10 秒となるように
      @service_timeout = env['REQUEST_URI'].start_with?('/admin') ? 30 : 10

      super(env)
    end
  end
end

Rack::Timeout.prepend Rack::DynamicTimeout

Module#prepend でモジュールを差し込んで Rack::Timeoutcall が呼ばれても対応できるようにしました。
ひとまずこれで。

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

Rubyにおける日時の扱い。DateとTimeの使い分け。

はじめに

Rubyにおける日付や時刻の操作にはDateクラス、Timeクラスを使います。
Dateクラスは年月日だけを扱い、Timeクラスは年月日時分秒を扱います。
Timeクラスだけ使っておけばいいような気がしますが、Dateクラスにも得意分野があるので、その使い分けについて解説していきます。

目次

1.Dateは年月日、Timeは年月日時分秒
2.Dateクラスは日付処理が得意
3.結論

1.Dateは年月日、Timeは年月日時分秒

まず、時分秒まで表示したい、つまり、日時を扱いたい場合はTimeを使います。DateクラスのサブクラスであるDateTimeクラスも使えますが、require 'date'しないといけないことや、サマータイム、うるう秒を扱えないことなどを考慮すると、Timeクラスを使っておくのが無難だと思われます。

2.Dateクラスは日付処理が得意

日付や年単位での処理はDateクラスを使うと便利です。


[1]pry(main)> Date.today.next_month # 来月の同日
=> #<Date: 2020-10-29 ((2459152j,0s,0n),+0s,2299161j)>

[2] pry(main)> Date.new(2020,10,-1) # 月末日(2020年10月)
=> #<Date: 2020-10-31 ((2459154j,0s,0n),+0s,2299161j)>

[3] pry(main)> Date.today.leap? # うるう年の判定(今年=2020年)
=> true

[4] pry(main)> Date.today.next_year.leap? # うるう年の判定(来年=2021年)
=> false

[5] pry(main)> Date.today + 3 # 3日後
=> #<Date: 2020-10-02 ((2459125j,0s,0n),+0s,2299161j)>

[6] pry(main)> Date.today - 7 # 1週間前(7日前)
=> #<Date: 2020-09-22 ((2459115j,0s,0n),+0s,2299161j)>

[7] pry(main)> Date.today >> 6 # 半年後(6ヶ月後)
=> #<Date: 2021-03-29 ((2459303j,0s,0n),+0s,2299161j)>

[8] pry(main)> Date.today << 1 # 1ヶ月前
=> #<Date: 2020-08-29 ((2459091j,0s,0n),+0s,2299161j)>

来月の同日や月末日の取得、うるう年の判定などを簡単にできるのは便利です。
また、日付のインクリメント、デクリメント操作はTimeクラスでは、秒単位で扱わなければならないため少々面倒くさいです。その点、Dateクラスでは日単位は、+ と - ,月単位は << と >> で楽にできる。

3.結論

時刻まで表示したいときなど、基本的にはTimeクラスを使えばいいが、日、月、年単位での処理を主に行うときはDateクラスを使うと便利。

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

DevContainer で Ruby 開発する際に厄介な BUNDLE_APP_CONFIG

TL;DR

  • Docker 公式の ruby イメージでセットされている環境変数 BUNDLE_APP_CONFIG は DevContainer 内での開発において邪魔になることがある
  • BUNDLE_APP_CONFIG の影響を無くしたい場合は devcontainer.json に以下を記述すると良い
.devcontainer/devcontainer.json
{
  // ...
  "remoteEnv": {
    "BUNDLE_APP_CONFIG": null
  },
  // ...
}

厄介な BUNDLE_APP_CONFIG

VSCode Remote - Containers での Ruby 開発では大抵の場合、Docker 公式の ruby イメージ をベースイメージにして DevContainer を作ることになる1かと思いますが、このイメージには厄介な問題があります。

それが環境変数 BUNDLE_APP_CONFIG です。
Dockerfile 内で ENV 命令によりデフォルトで /usr/local/bundle に設定されています。

BUNDLE_APP_CONFIG は bundler のアプリケーションローカルな設定 (bundle config set --local した際に作られる設定) をどこに保存するかを指定するための環境変数です。
この環境変数が設定されていない場合、bundler はアプリケーションルートの .bundle/ 以下に設定を作成します。

さて、話を移して VSCode の DevContainer で Ruby 開発をする場合、インストールした gem を永続化しつつコンテナにマウントするには以下のようにワークスペース内の vendor ディレクトリ等に突っ込んでおきたいところです。2

$ bundle config set --local path vendor/bundle

この設定により path 設定が記述された設定ファイルが通常は .bundle/config として保存されるはずです。ローカル設定にしたことでワークスペース内に設定ファイルが作られ、コンテナを破棄しても再設定は不要となります。
しかし、前述のように Docker 公式の ruby イメージでは BUNDLE_APP_CONFIG が設定されてしまっているため、このローカル設定は /usr/local/bundle/config に保存されてしまいます。

bundle config で確認してみると以下のような感じです。

$ bundle config
Settings are listed in order of priority. The top value will be used.
path
Set for your local app (/usr/local/bundle/config): "vendor/bundle"

app_config
Set via BUNDLE_APP_CONFIG: "/usr/local/bundle"

silence_root_warning
Set via BUNDLE_SILENCE_ROOT_WARNING: true

これでは Rebuild などによりコンテナを破棄した際に設定が失われてしまうため、都度 bundle config ... を実行するか、/usr/local/bundle をホストからマウントするようにするなどの工夫が必要になってしまいます。

スマートに解決するには BUNDLE_APP_CONFIG 自体を何とかした方が良いでしょう。

ちなみに公式イメージで BUNDLE_APP_CONFIG が設定されているのは上記の逆の考え方で、「ホスト側の .bundle/config がコンテナ内に影響を与えないようにするため」のようです3

対策

× BUNDLE_APP_CONFIG に空文字を設定する

公式ドキュメント に記載されている方法です。

The environment variables we set are canonically listed in the above-linked Dockerfiles, but some of them include GEM_HOME, BUNDLE_PATH, BUNDLE_BIN, BUNDLE_SILENCE_ROOT_WARNING, and BUNDLE_APP_CONFIG.

If these cause issues for your use case (running multiple Ruby applications in a single container, for example), setting them to the empty string should be sufficient for undoing their behavior.

結論から言うとこの方法ではうまくいきません。
bundler は BUNDLE_APP_CONFIG の有無だけを見ている4ので、空文字が設定されている場合はカレントディレクトリ直下に config ファイルが作成されます。

実験してみましょう。
DevCotainer 用の Dockerfile で BUNDLE_APP_CONFIG に空文字をセットします。

devcontainer/Dockerfile
FROM ruby:2.7

# 空文字を設定
ENV BUNDLE_APP_CONFIG=

# 以下略

DevContaier をビルドして中に入ってみます。
ターミナル上ではなぜか BUNDLE_APP_CONFIG が消えて問題なく動作します (理由は不明ですが VSCode の実装の問題かもしれません)。

$ bundle config
Settings are listed in order of priority. The top value will be used.
silence_root_warning
Set via BUNDLE_SILENCE_ROOT_WARNING: true
$ bundle config set --local path vendor/bundle
$ bundle config
Settings are listed in order of priority. The top value will be used.
path
Set for your local app (/workspace/sample_app/.bundle/config): "vendor/bundle"

silence_root_warning
Set via BUNDLE_SILENCE_ROOT_WARNING: true

問題となるのはデバッグ時です。
デバッガ (rdebug-ide) を Gemfile で管理し、bundle install でインストールしているケースを想定します。
以下のような launch.json になるはずです。

.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Local File",
      "type": "Ruby",
      "request": "launch",
      "program": "${workspaceFolder}/main.rb",
      "useBundler": true
    }
  ]
}

デバッグを実行すると DEBUG CONSOLE に以下のように必要な gem が見つからない旨のエラーが出てしまいます。

bundler: failed to load command: rdebug-ide (/usr/local/bundle/bin/rdebug-ide)
Bundler::GemNotFound: Could not find concurrent-ruby-1.1.7 in any of the sources
  /usr/local/lib/ruby/2.7.0/bundler/spec_set.rb:86:in `block in materialize'
  /usr/local/lib/ruby/2.7.0/bundler/spec_set.rb:80:in `map!'
  /usr/local/lib/ruby/2.7.0/bundler/spec_set.rb:80:in `materialize'
  /usr/local/lib/ruby/2.7.0/bundler/definition.rb:170:in `specs'
  /usr/local/lib/ruby/2.7.0/bundler/definition.rb:237:in `specs_for'
  /usr/local/lib/ruby/2.7.0/bundler/definition.rb:226:in `requested_specs'
  /usr/local/lib/ruby/2.7.0/bundler/runtime.rb:101:in `block in definition_method'
  /usr/local/lib/ruby/2.7.0/bundler/runtime.rb:20:in `setup'
  /usr/local/lib/ruby/2.7.0/bundler.rb:149:in `setup'
  /usr/local/lib/ruby/2.7.0/bundler/setup.rb:20:in `block in <top (required)>'
  /usr/local/lib/ruby/2.7.0/bundler/ui/shell.rb:136:in `with_level'
  /usr/local/lib/ruby/2.7.0/bundler/ui/shell.rb:88:in `silence'
  /usr/local/lib/ruby/2.7.0/bundler/setup.rb:20:in `<top (required)>'

これは、デバッグ用のサブプロセスでは BUNDLE_APP_CONFIG="" が効いているために .bundle/config が参照されず path 設定が読み込まれないためです。

BUNDLE_APP_CONFIG.bundle のパスを設定してしまう (Dockerfile)

例えば以下のように Dockerfile などでローカル設定用の .bundle の絶対パスを指定してしまう方法です。

devcontainer/Dockerfile
ENV BUNDLE_APP_CONFIG=/workspace/.bundle

この方法であれば大体うまくいきますが、モノレポに複数のプロジェクトを突っ込んでいるケース (Gemfile が複数あるケース) には対応できません。

BUNDLE_APP_CONFIG.bundle のパスを設定してしまう (VSCode 設定)

例えば上記のデバッグがうまくいかない問題は以下のように解決できます。

.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Local File",
      "type": "Ruby",
      "request": "launch",
      "program": "${workspaceFolder}/main.rb",
      "useBundler": true,
      "env": {
        "BUNDLE_APP_CONFIG": "${workspaceFolder}/.bundle"
      }
    }
  ]
}

この方法であれば Multi-root Workspace にすることでモノレポのケースにも対応することができます。
ただしこの設定だけではデバッグ用のサブプロセスにしか有効にならないので、必要に応じて settings.json のターミナル設定や tasks.json にも同様の設定を記述する必要があります。
また、何らかの理由で BUNDLE_APP_CONFIG を設定して使っている人と launch.json 等の設定を共有することが難しくなります。

〇 devcontainer.json の設定で BUNDLE_APP_CONFIG を削除する

下記のような設定を devcontainer.json に追加することで、環境変数を空にするのでも上書きするのでもなく削除 (unset) することができます。

.devcontainer/devcontainer.json
{
  // ...
  "remoteEnv": {
    "BUNDLE_APP_CONFIG": null
  },
  // ...
}

この方法であればコンテナ内の VSCode プロセス及びそのサブプロセス全てで BUNDLE_APP_CONFIG が削除されます。
個人的にはこの方法が最も副作用が少なそうなのでおすすめです。


  1. mcr.microsoft.com/vscode/devcontainers/ruby もベースはこのイメージです 

  2. 余談ですが bundle install--path オプションは非推奨になったんですね…最近知りました 

  3. https://github.com/docker-library/ruby/issues/129 

  4. 2020年9月現在の最新 v2.4.1 ではこうなっています 

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

ルーティングのネストによって起きていること

動作環境
Ruby 2.6.5
Rails 6.0.3.2

ルーティングのネストによって何が起きているのかがわかりづらかったので、自分の確認を兼ねて投稿してみました。

ルーティングのネストをした場合

routes.rb
Rails.application.routes.draw do
  resources :hoges do
    resources :fugas, only: [:index]
  end
end

上記のコードでは、hogesに対してfugasがネストされており、ターミナル上でrails routesを実行すると、以下のようになります。

Prefix ➡︎ hoge_fugas
Verb ➡︎ GET
URI Pattern ➡︎ /hoges/:hoge_id/fugas(.:format)
Controller#Action ➡︎ fugas#index

もちろん、上記以外も表示されますが今回はネストの箇所のみを扱います。
これだけだといまいちわかりづらいので、ルーティングのネストをしていない場合と比較していきます。

ルーティングのネストをしない場合

routes.rb
Rails.application.routes.draw do
  resources :hoges
  resources :fugas, only: [:index]
end

上記のコードでは先ほどのコードと違い、ネストされていません。では先ほどと同様にターミナル上でrails routesを実行して、同じ箇所を確認してみましょう。

Prefix ➡︎ fugas
Verb ➡︎ GET
URI Pattern ➡︎ /fugas(.:format)
Controller#Action ➡︎ fugas#index

先ほどと比べてPrefixとURI Patternが異なっていることがわかります。
PrefixはURI Patternを簡単に書いたものなので、URI Patternのみに注目していきます。

ルーティングのネストをした場合としない場合の違い

ネストした場合
URI Pattern ➡︎ /hoges/:hoge_id/fugas(.:format)
ネストしない場合
URI Pattern ➡︎ /fugas(.:format)

つまり、ルーティングのネストとはURI Patternを変更するために行っているものと言えると思います。しかし、この言い方だとわかりづらいと思うので、もう少し詳しく説明します。

ネストすることによってURI Patternが変更したためにfugasコントローラーのindexアクションを作動するには、hoge_idを指定する必要が出てきます。
そのため、paramsの中にhoge_idを入れることができるようになります。
つまり、先ほどはルーティングのネストをURI Patternを変更するためのものと言いましたが、paramsの中にネストする側(今回はhoge)のid情報を入れるためのものと言い換えることができます。

paramsにネストする側(今回はhoge)のid情報を入れることにより、ネストされた側(今回はfuga)のコントローラー内でネストする側の情報を扱うことができるということになります。

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

Rails6でHTMLとCSSのみを使ってシンプルにフォームをつくるメモ

はじめに

Railsで bootstrapなどのCSSフレームワークを使わない、素のコードが意外とみつかないので、そのメモ

前提

Receptionモデルがあり、カラムは名前と目的。

目標

このようなシンプルなフォームをHTMLとCSSのみをつかってシンプルに書く
スクリーンショット 2020-09-29 13.14.53.png

HTML

  • フィールドはtext_field,select_field,submitの3つ
html
      <%= form_with model: @reception do |f| %>
        <ul>
          <li>
            <%= f.label :name, "名前" %>
          </li>
          <li>
            <%= f.text_field :name, placeholder: "名前を入力してください" %>
          </li>
          <li>
            <%= f.label :purpose, "ご用件を選択してください" %>
          </li>
          <li>
            <%= f.select :purpose,
            (
              [
                ["面接のため", '面接'],
                ["面談のため", '面談'],
                ["打ち合わせのため", '打ち合わせ'],
                ["その他", 'その他']
              ]
            ), {}, size: "4"
            %>
          </li>
          <li>
            <%= f.submit '送信する' %>
          </li>
        </ul>
      <% end %>

CSS

  • Rubyのform_witth メソッドによって、formタグが生成される
  • ul,li のリスト表示にする
  • selectタグをsize: "4"の第4引数に指定することで選択肢から4つ表示して状態に設定できる
css
form {
  width: 60%;
  margin: auto;
  padding-top: 1%;
}
ul {
  list-style: none;
  padding-left: 0;
  width: 100%;
  text-align: center;
}
label {
  font-size: 25px;
}
input[type=text], select {
  text-align: center;
  width: 55%;
  font-size: 20px;
  border: 1px solid #CCC;
  border-radius: 1rem;
  margin-bottom: 20px;
  padding: 1%;
}
input[type=submit] {
  text-align: center;
  width: 55%;
  font-size: 20px;
  border: 1px solid #CCC;
  border-radius: 1rem;
  margin-bottom: 20px;
  padding: 1%;
}

まとめ

もっときれいに書く方法があると思うので、参考程度におねがいします。

追記

誤字を修正

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

Homebrew, rbenvの組み合わせでRuby 3.0.0 Preview 1 をインストールする

背景

Ruby 3.0.0 Preview 1 がリリースされました。
https://www.ruby-lang.org/en/news/2020/09/25/ruby-3-0-0-preview1-released/

インストールしてみる

最初にやったこと

以下を参考
https://github.com/rbenv/ruby-build#upgrading

brew update && brew upgrade ruby-build

インストール可能なRubyを確認

$ rbenv install --list
2.5.8
2.6.6
2.7.1
jruby-9.2.13.0
maglev-1.0.0
mruby-2.1.2
rbx-5.0
truffleruby-20.2.0
truffleruby+graalvm-20.2.0

Only latest stable releases for each Ruby implementation are shown.
Use 'rbenv install --list-all' to show all local versions.

stableでlatestなものしか出ていないので--list-allで確認

$ rbenv install --list-all | grep 3.0.0-preview1
3.0.0-preview1

あった。

Ruby 3.0.0 Preview 1 インストール

$ rbenv install 3.0.0-preview1

インストール確認

$ rbenv versions
  system
  2.6.1
  2.7.0
* 2.7.1 (set by /Users/soul/.rbenv/version)
  3.0.0-preview1

rbenvで使用するRubyバージョン指定

# システム全体で 3.0.0-preview1 を使用する
rbenv global 3.0.0-preview1

# バージョン確認
ruby -v
ruby 3.0.0preview1 (2020-09-25 master 0096d2b895) [x86_64-darwin19]

確認

irb(main):001:0> RUBY_VERSION
#=> "3.0.0"
require 'prime'
#=> true
# Ruby3で導入される右代入
Prime.find {|p| p > 10000} => x
#=> 10007
x
#=> 10007

Ruby3で追加される右代入記法が使えるようになっている。

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

Rails+JavaScript(jQuery)でタイムレスな検索を

記事で扱う検索機能

こんな感じの検索機能です。
今回は、shopsというテーブルからnameというカラムを抽出してビューに表示していきます。
※店舗情報を複数登録済み。そこから任意の店舗を任意のカラムで検索します。

edc53a4c67fcde418466da618219084f.gif

  • 非同期で行う
  • 文字を打つごとに検索を行い、描画する
  • 検索結果をクリックで該当のページにリンクする

おそらくですが、初学者のプログラミングにおける知識レベルや常識レベルって経験者の方が考えるそれよりもずっと低い水準にあると思うんです。
なので、自身にできる範囲内で「そんな事まで説明する必要あるの?」という事も書いて行こうかと思います。

開発環境

mac
Ruby 2.5.1
Rails 5.2.3
jQuery

必要なファイル

まず、実装に必要となるファイルを以下にまとめます。

  • 検索を行うアクションを定義したコントローラーの準備
app/controllers/shops_controller.rb
 def search
 end
  • 上記アクションに対応するjson.jbuilderの準備
views/shops/search.json.jbuilder
#上記で作成したコントローラー及びアクションに対応するようデイレクトリ関係とファイル名に注意
#この場合、views/shops(コントローラー名に対応)/search(アクション名に対応).json.jbuilder
  • 上記アクションに対するルーティングの準備
config/route.rb
resources :shops do #shopsコントローラー内の
 collection do
  get 'search'      #searchアクション
 end
end
  • 検索フォームと結果のを表示するビューファイルの準備
app/views/shops/index.html.haml
#今回は、shops一覧画面に検索を表示します。
#hamlを使って書いていきますが、通常のhtmlでもslimでも構いません。
  • jsファイルの準備
app/assets/javascripts/search.js
#.jsで終わっていれば名前はなんでも構いません

以上、5つのファイルを使って検索機能を実装していきます。

どう動くの?

検索処理の流れは以下の通りです。

検索フォームで起こったイベントを.jsファイルで受け取る。

○○.html.haml(○○.html.erb) → ○○.js

受け取った情報をjson形式でコントローラへ流す。

○○.js → ○○_controller

コントローラーでDBから必要な情報を抽出し、その情報を.js内でも使えるようjbuilderで変換。

○○_controller → ○○.json.jbuilder

再度js側に送り、htmlへの描画を行う。

○○.json.jbuilder → ○○.js → ○○.html.haml

処理の流れが分かっていないと、エラーが起きた際に対処できず1時間も2時間も無駄にすることになってしまいます。
これから記載していく実際のコードには可能な限り説明を入れますので、ぜひ上記の処理の流れを頭に入れた状態で読み進めてみて下さい。

では、始めていきましょう。

ルーティングを行う

今回は例として、shops_controller内にsearchアクションを設定します。

app/controllers/shops_controller.rb
 def search
 end
config/route.rb
resources :shops do #shopsコントローラー内の
 collection do
  get 'search'      #searchアクション
 end
end

検索フォームと結果表示欄の準備

app/views/shops/index.html.haml
.search-field
 .fas.fa-search  #虫眼鏡のfontawesomeを使用しています。
 .shop_search
   = f.text_field :text, placeholder: "店舗・住所で検索", id: "shop_search"  #テキスト入力欄を設置。
 #shop_search--result  ←コメントアウトではなくshop_search--resultというid名です。検索結果を表示。
   .shop-search-list

検索フォームに入力された情報を受け取り、受け取った情報をコントローラへ送る

app/assets/javascripts/search.js
$("#shop_search").on("keyup", function(){
  let input = $("#shop_search").val();
  $.ajax({
   type: 'GET',
   url: '/shops/search',
   data: {keyword: input},
   dataType: 'json'
  })

まず、検索フォーム(hamlファイル内のf.text_field)に入力された情報をjsファイルで受け取ります。
┗検索フォームにつけたid名を指定し、文字入力の際にキーボードから指を話した瞬間にkeyupで情報を取得。

ajax以下でその情報を
・どのような方法で
・どこに
・何を
・どのような状態で
送るかを指定します。

この場合、GETメソッドでshopsコントーラ内のsearchアクションにinputをjson方式で送るということになるかと思います。

コントローラーでアクションの設定、DBとのやりとりを記載

app/controllers/shops_controller.rb
  def search
    return nil if params[:keyword] == ""
    @shops = Shop.where('name LIKE ? OR location LIKE ?', "%#{params[:keyword]}%", "%#{params[:keyword]}%").limit(10)
    respond_to do |format|
      format.html
      format.json
    end
  end

今回私は、shopsテーブル作成の際にname(店舗名)カラムとlocation(所在地)カラムを設定していますので、この2情報で検索できるように設定していますが、
例えば店舗名のみで検索する場合は、

app/controllers/shops_controller.rb
  def search
    return nil if params[:keyword] == ""
    @shops = Shop.where('name LIKE ?, "%#{params[:keyword]}%").limit(10)
    respond_to do |format|
      format.html
      format.json
    end
  end

でOKです。

アクション内のparams[:keyword]は先のjsファイル内ajax以下のdata:{keyword: input}から来ています。
分かりづらいかもしれませんが、検索欄に入力した文字をjsファイル内ではinputとして扱い、コントローラファイル内ではkeywordとして扱っているということです。
検索欄に何も入力されていない場合(2行目"")はnilを返しています。

また、3行目ではshopsテーブルではそのkeyword(検索欄に入力された文字)を基に、DBから情報を引っ張り出します。
今回はparams[:keyword]の量端を%で囲って部分一致検索を行う仕様です。
他の検索方法については、この記事を参考にしてみて下さい。

Rails - LIKE句を使った文字のあいまい検索(特定の文字を含む語句を曖昧検索したい場合)
私もこの記事を参考にさせていただきました。ありがとうございます。

そして、今回入力された文字データはjson形式でコントローラへ入って来ている為、jbuilderへと移ります。

json.jbuilderで情報変換

search.json.jbuilder
json.array! @shops do |shop|
  json.name           shop.name
  json.location       shop.location
end

コントローラー内でDBから情報を抽出しましたがこのデータをjson形式に変換する必要があります。
それをjbuilderで行うのです。

htmlファイルへの検索結果描画

まず先ほどのjsファイルajax以下にコントローラーでの処理が終わり、戻ってきた際の処理を追記します。

app/assets/javascripts/search.js
  $("#shop_search").on("keyup", function(){
    let input = $("#shop_search").val();
    $.ajax({
      type: 'GET',
      url: '/shops/search',
      data: {keyword: input},
      dataType: 'json'
    })
    .done(function(shops){
      $("#shop_search--result").empty();
      if (shops.length !== 0) {
        shops.forEach(function(shop){
          addShop(shop);
        });
      } 
      else if (input.length == 0){
        return false;
      } else {
        addNoShop();
      }
    });
  });

.done以下がその処理です。

そしてそれぞれaddShop、addNoShopとしたものをhtmlへ描画します。

以下がjsファイル全文です。

app/assets/javascripts/search.js
$(function(){
  function addShop(shop) {
    let html = `
      <a href="/shops/${shop.id} class="shop_search-list">
        <div>${shop.name} - ${shop.location}</div>
      </a>
      `;
      $("#shop_search--result").append(html);
  };
  function addNoShop(){
    let html =`ショップがありません`
    $("#shop_search--result").append(html);
  };
  $("#shop_search").on("keyup", function(){
    let input = $("#shop_search").val();
    $.ajax({
      type: 'GET',
      url: '/shops/search',
      data: {keyword: input},
      dataType: 'json'
    })
    .done(function(shops){
      $("#shop_search--result").empty();

      if (shops.length !== 0) {
        shops.forEach(function(shop){
          addShop(shop);
        });
      } 
      else if (input.length == 0){
        return false;
      } else {
        addNoShop();
      }
    })
  });
});

let htmlとして描画する際の定型を指定します。
今回は検索結果をクリックするとその詳細ページにリンクする形で記載してみました。

hamlファイル内の検索結果を表示する箇所(#shop_search--result)にappendを使用して作成した定型文(html)を挿入します。

これで非同期の検索機能(インクリメンタルサーチ)の完成です。

やってみて

当内容はスクールで学んだ内容に自身で挑戦してみて、理解を深めながら作成したものです。
学習当初は半端な理解で出来ればいいや位の気持ちで進めていましたが、いざ1から自分の力でやるとなると手が止まってしまう事が多々ありました。反省です。

ファイルが変わるごとに、どこがどのように対応しているのかを把握する事がポイントかと思います。
また、どんな機能でもそうですが、今回の場合はconsole.log等デバッグしながら書き進めていくことを強くお勧めします。

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

Rubyで、TSVファイルからCSVファイル(BOM付き)への変換

はじめに

Rubyにおけるタブ区切りのファイル(TSVファイル)から、BOM付きのCSVファイルへの変換方法です。

目次

0.完成コード
1.BOMを付けたCSVファイルの作成
2.TSVファイル内データの読み込み
3.CSVに1行づつ読んでは書いていく
4.プログラムを実行

0.完成コード

tsv_to_csv.rb
require 'csv'

# CSVファイルを作成し、bomを付ける。
File.write("meibo.csv", "\uFEFF")

# 'meibo2.txt'の情報を1行毎に読み込み、splitで配列にして、CSVファイルに1行ずつ入れていく。
CSV.open("meibo.csv", "a", force_quotes: true ) do |meibocsv|
  File.foreach('meibo.txt') do |student|
    meibocsv << student.chomp.split("\t", -1)
  end
end

1.BOMを付けたCSVファイルの作成

BOM(Byte order mark)は、Unicodeの符号化形式で符号化したテキストの先頭に着ける数バイトのデータのことを言います。
ExcelはCSVファイルを開くときデフォルトでShift-JISで開こうとするため、UTF-8では文字化けを起こしてしまいます。その対処法として、UTF-8で出力する際にBOMを付けてあげることでExcelにUTF-8で書かれていると認識させて、文字化けを防ぐことができます。
以下のようにしてBOMを付けてます。

File.write("meibo.csv", "\uFEFF")

2.TSVファイル内データの読み込み

読み込むTSVファイルは以下。

meibo.txt
john    m   18
paul    m   20
alice   f   15
dabid   m   17
jasmin  f   17

タブ区切り形式のデータが書かれたmeibo.txtを読み込みますが、1行読み込んでは1行書き込むという手法をとりたいです。タブ区切りデータ1行をsplitで配列に変換した後、そのままCSVファイルに書き込んでいくようにします。

File.foreach('meibo.txt') do |student|
  CSVファイル) << student.chomp.split("\t", -1)
end

3.CSVに1行づつ読んでは書いていく

上のコードをCSV.openで囲えば、1行読み書きができます。
基本的に、タブ区切りファイルから、CSVファイルへ移すという場合は、1行ずつ処理していきます。なぜならば、仮に1000万行のような膨大なデータであっても、メモリがオーバーすることを防ぐためです。

CSV.open("meibo.csv", "a", force_quotes: true ) do |meibo_csv|
  File.foreach('meibo.txt') do |student|
    meibo_csv << student.chomp.split("\t", -1)
  end
end

因みにsplitの第2引数(limit)に、-1を渡しています。
この第2引数の値はデフォルトでは0になっており、配列末尾の空文字列を取り除いてしまいます。
これではTSVに空のフィールドがあったときにうまく行きません。-1を渡してあげることで、解決することができます。

4.プログラムを実行

% ruby tsv_to_csv.rb 

データが書き込まれたCSVファイルが生成される。

meibo.csv
"john","m","18"
"paul","m","20"
"alice","f","15"
"dabid","m","17"
"jasmin","f","17"

カンマ区切りのCSVファイルに正しく変換されました。

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

[Rails]カスタムフォントの設定

はじめに

アプリ開発でフリーフォントを使用してデザインを変えたのでその使用方法をまとめました。

目次

1 フリーフォントの準備
2 フォントの読み込み

1 フリーフォントの準備

まず、フリーフォントサイトから自分が使用したいものをダウンロードします。
その後、ダウンロードしたファイル(拡張子ttfまたはotf)をapp/assets/fonts下に配置します。

注意事項

フォントはそれぞれ利用条件が異なります。商用可能であっても利用できる範囲に制限があったり、使用時に報告が必要なものもありますので、ご利用の際には必ず配布ページをご確認ください。

2 フォントの読み込み

CSSファイルに以下を記述します。

@font-face {
  font-family: abcdefg;
  src: url('/assets/abcdefg.ttf') format("truetype");
  font-weight: normal;
  font-style: normal;
}

フォントを適用したい箇所にfont-familyを指定します。

p {
  font-family; “abcdefg”;
}

これでフォントを適用することができました。

3 本番環境への適応

このままでは本番環境でフォントが適用されなかったため、以下を変更しました。

/config/production.rb
# 変更前
config.assets.compile = false
# 変更後
config.assets.compile = true

これにより本番環境で読み込まれていなかった画像ファイルも表示することができました。

参考リンク

https://qiita.com/dir_sh0606/items/ce605b70f1cb333f9d59
https://qiita.com/kinpin/items/bd7c9a7d7a739e297742

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

Rubyのメソッド内で引数に再代入してから `super` と呼び出した場合 → 再代入したものが使われる

  • 「たしか super を引数なしで呼び出したら、現在のメソッドの引数と同じものが渡されるんだっけ」
  • 「あれ? 引数に代入して上書きした場合1、元のと新しいののどっちが使われるんだろう?」

と分からなくなったので、実験してみた。

class Foo
    def mtd(str)
        puts "[Foo:1] str => %p (%#08x)" % [str, str.__id__]
    end
end

class Bar < Foo
    def mtd(str)
        puts "[Bar:1] str => %p (%#08x)" % [str, str.__id__]
        str *= 2
        puts "[Bar:2] str => %p (%#08x)" % [str, str.__id__]
        super
    end
end

Bar.new.mtd("abc")
結果
[Bar:1] str => "abc" (0x00003c)       <-- 仮引数に渡された元のオブジェクト
[Bar:2] str => "abcabc" (0x000050)    <-- 再代入して上書きしたオブジェクト(IDが異なる)
[Foo:1] str => "abcabc" (0x000050)    <-- 結果:上書きしたほうが使われる

super とだけ書いた場合は super(str) という感じに補完してくれて、結果として再代入したほうが使われる。

念のためリファレンスマニュアルを見たら、サンプルコードにしっかり書いてあった。(やはり文章での説明だとどちらにも解釈できるので難しい)


  1. 代入でなく破壊的メソッドを作用させただけの場合は、元のも新しいのもオブジェクトとしては同一なので今回の疑問の対象外 

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

「もっとプログラム脳を鍛える数学パズル」_pp.018-020 (code:Ruby) -> Rust

「もっとプログラマ脳を鍛える数学パズル」をRustで書き直すのは、ボケ防止にちょうど良いかもしれない、と思った。

例題2 順列と組み合わせ(pp.018-020)

Ruby

なんだか書籍p.018のpre2_1.rbは、正しい答えが出てない気がする。
定義からすると、順列のコードは、

r.upto(n) do |i|

ではない、よな・・・。
ということで、正しい答えが出るコード。

ex2.rb
require "test/unit"

def nPr(n, r)
    result = 1
    (n-r+1).upto(n) do |i|
        result *= i
    end
    result
end

def nCr(n,r)
    result = 1
    1.upto(r) do |i|
        result = result * (n - i + 1) / i
    end
    result
end

class NPrTest < Test::Unit::TestCase
    def test_nPr
        assert_equal 1, nPr(1,1), "1P1 should return 1."
        assert_equal 720, nPr(6,6), "6P6 should return 720."
        assert_equal 20, nPr(5,2), "5P2 should return 20."
    end

    def test_nCr
        assert_equal 1, nCr(1,1), "1C1 should return 1."
        assert_equal 1, nCr(6,6), "6C6 should return 1."
        assert_equal 10, nCr(5,2), "5C2 should return 10."
    end
end

Rust

main.rs
fn main() {
    println!("{n}P{r} = {x}", n = 5, r = 2, x = permutation(5, 2));
    println!("{n}C{r} = {x}", n = 5, r = 2, x = combination(5, 2));
}

pub fn permutation(n: i64, r: i64) -> i64 {
    let mut result = 1;
    for i in (n - r + 1)..=n {
        result *= i;
    }
    return result;
}

pub fn combination(n: i64, r: i64) -> i64 {
    let mut result = 1;
    for i in 1..=r {
        result = result * (n - i + 1) / i;
    }
    return result;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_permutation() {
        assert_eq!(permutation(1, 1), 1);
        assert_eq!(permutation(6, 6), 720);
        assert_eq!(permutation(5, 2), 20);
    }

    #[test]
    fn test_combination() {
        assert_eq!(combination(1, 1), 1);
        assert_eq!(combination(6, 6), 1);
        assert_eq!(combination(5, 2), 10);
    }
}

ベタ移植。ところで、

result = result * (n - i + 1) / i;

と書くのと、

result *= (n - i + 1) / i;

と書くのは違うのか。テストを書きながら進めてよかった。

mutを使いまくるのはどうなのか」と考えてしまうが、1関数内に閉じている限りは、騒ぐほどのこともないと思う。

2020/09/29 追記

簡潔なコード例を頂いた。permutation2()combination2()として実験。

main.rs
fn main() {
    println!("{n}P{r} = {x}", n = 5, r = 2, x = permutation(5, 2));
    println!("{n}C{r} = {x}", n = 5, r = 2, x = combination(5, 2));
}

pub fn permutation(n: i64, r: i64) -> i64 {
    let mut result = 1;
    for i in (n - r + 1)..=n {
        result *= i;
    }
    return result;
}

pub fn combination(n: i64, r: i64) -> i64 {
    let mut result = 1;
    for i in 1..=r {
        result = result * (n - i + 1) / i;
    }
    return result;
}

pub fn permutation2(n: i64, r: i64) -> i64 {
    (n - r + 1..=n).product()
}

pub fn combination2(n: i64, r: i64) -> i64 {
    (1..=r).fold(1, |p, i| p * (n - i + 1) / i)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_permutation() {
        assert_eq!(permutation(1, 1), 1);
        assert_eq!(permutation(6, 6), 720);
        assert_eq!(permutation(5, 2), 20);
        assert_eq!(permutation(1,1), permutation2(1,1));
        assert_eq!(permutation(6,6), permutation2(6,6));
        assert_eq!(permutation(5,2), permutation2(5,2));
    }

    #[test]
    fn test_combination() {
        assert_eq!(combination(1, 1), 1);
        assert_eq!(combination(6, 6), 1);
        assert_eq!(combination(5, 2), 10);
        assert_eq!(combination(1, 1), combination2(1,1));
        assert_eq!(combination(6, 6), combination2(6,6));
        assert_eq!(combination(5, 2), combination2(5,2));
    }
}

確かに同一の結果になる。コードも分かりやすい。

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

[初学者向け]削除機能の実装方法

投稿内容

今回は削除機能の実装方法について投稿します。
以下のようなボタンを押した際にdestroyアクションを発動させます。
image.png

コントローラーのアクション

items_controller.rb
class ItemsController < ApplicationController

#省略

  def destroy
    if @item.destroy
      redirect_to root_path
    else
      render :show
    end
  end
end

解説) destroyアクションが呼ばれたらインスタンス変数@itemを削除します。無事削除できた場合はroot_pathへ遷移。削除できなかった場合はshowアクションへ遷移するようハンドリングします。

Rubyの記述

show.html.haml
.item-show-page__destroy-btn
  = link_to "商品を削除する", item_path(@item), method: :delete, class: "item-show-page__destroy-link"

解説) link_to@itemを指定し、メソッドはdeleteを指定します。

これでボタンをクリックすると削除できると思います。

最後に

今回の削除機能は至ってシンプルなものなので、初学者の方でもとっつきやすいかと思われます。是非参考にしてください!

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

働きながら公式テキスト1冊だけでRuby Silverに合格したので勉強法をメモ

Ruby Silverとは

Ruby技術者認定試験の一種です。
正式名称は
Ruby Association Certified Ruby Programmer Silver version 2.1:
となっており、短縮してRuby Silverと呼ばれています。

制限時間は90分で選択式の問題が50問出題されます。
全国にあるプロメトリックという会社のテストセンターでPCを操作して解答します。

Silverのレベルは以下の通り。

Rubyの文法知識、Rubyのクラスとオブジェクト、標準ライブラリの知識について、基本的な技術レベルを持つことを認定します。
(公式サイトより)

受験履歴

75点が合格ライン。
下記の通り、1回目で84点をとって無事合格!

受験回数 合否 点数
1回目 合格 84点

受験当時の状況

自社開発の企業に新卒で入社し、半年ほど経過していました。

情報系の大学出身で、学生時代は主にPythonを使用。
Rubyやjavaといったオブジェクト指向の言語はほとんど経験がなく、入社してRailsでの開発を始めたのが受験のきっかけとなりました。

わからないことが多くて悩んでいた時、先輩エンジニアに「Ruby Silverって試験があるよ〜」って教えていただいたのでその日の夜に申し込んだ記憶があります。

教材

使用した教材は、公式テキスト1冊のみです。
(3000円以上するので少しお高いですが、リファレンスとしての活用もできますので持っていて損はないかと)
51TGSmvhRsL.jpg

勉強のゴール

公式テキストに収録されている問題を完璧に解けるようになる

公式テキストに基礎力確認問題(30問)と模擬問題(50問)が収録されています。
試験では、これら80問から似たような意図の問題やまったく同じ問題が出題されます。

余談ですが、試験勉強において具体的なゴール設定は大切です。
「合格する」というゴールだど、抽象的すぎます。
試験直前までずっと不安な気持ちになってしまうため、「試験までにこれができたらOK」という具体的な行動のゴールを定めておきます。
もし落ちた場合は、ゴール設定が間違っていたかゴールに到達できなかったかのどちらかとなります。

勉強方法

制限時間や解答方式など、冒頭の試験概要は事前によく読んでおいてください。
基本サイクルは以下の通りです。

0, 暗記シート(A4の紙など)を作成
1, いきなり問題を解く
2, 問題解説を読み、覚えるべき項目を暗記シートに書く
→よく分からないときはirbで動作確認する
3, 解説を読んで必要だと思った知識の詳しいページを読み、暗記シートに追記する
4, 間違えた問題にレ点ををつける
5, 1-4を繰り返して収録されている80問をすべて解く
6, 暗記シートを覚える
7, レ点付きの問題を解き、解けたらレ点の横に○、間違えたらレ点を書き加える
8, ○がついていないレ点の問題を解く
9, 80問を何度も繰り返し解く
10, 試験直前は暗記シートだけ見る

(余裕がある方向け)
公式テキスト80問は何周やっても完璧、という状態で試験までに余裕があったら以下の公式模擬問題(50問)も解いておきましょう。
https://gist.github.com/sean2121/945035ef2341f0c39bf40762cd8531e0

大切なのは、収録されている80問を最初にすべて解くことです。
数百ページあるテキストを最初から読んでいたらいつまでたっても終わりませんし、膨大な量のメソッドやその返り値を手当たり次第に覚えていくのは非常に効率が悪いです。

まず問題を解いて、不足している知識を洗い出してから詳細のページを見に行けばいいです。
解説で十分事足りることもしばしば。

テキスト1冊だけで勉強するのは不安だと思います。
が、中途半端な状態にしてWEBのいろんな問題に手を出す方が危険です。

この試験は傾向がはっきりしていますので、80問きっちりできれば合格ラインは問題ないと感じます。
問題の出され方に慣れておきましょう。

例えば、以下の問題を解いてみてください。

以下の出力はどうなりますか?
a="ruby silver"
a.upcase # アルファベットを大文字にするメソッド
"HELLO" + " " + a

puts a #=>

正解は"ruby silver"です。
"HELLO RUBY SILVER"ではありません。(僕は最初ひっかかった)
upcaseメソッドは非破壊的メソッドのため、レシーバー(メソッドの呼び出し元)自体は変化しません。
破壊的メソッドは!がつくことがほとんどですが、配列を結合するconcatなど一部メソッドは!なしで破壊的メソッドなので要注意(concatはめっちゃ出る)。

以下の出力はどうなりますか?
a="ruby silver" #=> "ruby silver"
a.upcase #=> "RUBY SILVER"
"HELLO" + " " + a #=> "HELLO RUBY SILVER"

puts a #=> "ruby silver"(aの中身自体は変化していない)

このように、一生懸命勉強してupcaseメソッドの処理を知っていたとしても、問題に正解できるかどうかは別なのです。

まとめ

公式テキストの問題(全80問)が完璧に理解できていることがめっちゃ重要

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

公式テキスト1冊でRuby Silverに合格した時のメモ

Ruby Silverとは

Ruby技術者認定試験の一種です。
正式名称は
Ruby Association Certified Ruby Programmer Silver version 2.1:
となっており、短縮してRuby Silverと呼ばれています。

制限時間は90分で選択式の問題が50問出題されます。
全国にあるプロメトリックという会社のテストセンターでPCを操作して解答します。

Silverのレベルは以下の通り。

Rubyの文法知識、Rubyのクラスとオブジェクト、標準ライブラリの知識について、基本的な技術レベルを持つことを認定します。
(公式サイトより)

受験履歴

75点が合格ライン。
下記の通り、1回目で84点をとって無事合格!

受験回数 合否 点数
1回目 合格 84点

受験当時の状況

自社開発の企業に新卒で入社し、半年ほど経過していました。

情報系の大学出身で、学生時代は主にPythonを使用。
Rubyやjavaといったオブジェクト指向の言語はほとんど経験がなく、入社してRailsでの開発を始めたのが受験のきっかけとなりました。

わからないことが多くて悩んでいた時、先輩エンジニアに「Ruby Silverって試験があるよ〜」って教えていただいたのでその日の夜に申し込んだ記憶があります。

教材

使用した教材は、公式テキスト1冊のみです。
(3000円以上するので少しお高いですが、リファレンスとしての活用もできますので持っていて損はないかと)
51TGSmvhRsL.jpg

勉強のゴール

公式テキストに収録されている問題を完璧に解けるようになる

公式テキストに基礎力確認問題(30問)と模擬問題(50問)が収録されています。
試験では、これら80問から似たような意図の問題やまったく同じ問題が出題されます。

余談ですが、試験勉強において具体的なゴール設定は大切です。
「合格する」というゴールだど、抽象的すぎます。
試験直前までずっと不安な気持ちになってしまうため、「試験までにこれができたらOK」という具体的な行動のゴールを定めておきます。
もし落ちた場合は、ゴール設定が間違っていたかゴールに到達できなかったかのどちらかとなります。

勉強方法

制限時間や解答方式など、冒頭の試験概要は事前によく読んでおいてください。
基本サイクルは以下の通りです。

0, 暗記シート(A4の紙など)を作成
1, いきなり問題を解く
2, 問題解説を読み、覚えるべき項目を暗記シートに書く
→よく分からないときはirbで動作確認する
3, 解説を読んで必要だと思った知識の詳しいページを読み、暗記シートに追記する
4, 間違えた問題にレ点ををつける
5, 1-4を繰り返して収録されている80問をすべて解く
6, 暗記シートを覚える
7, レ点付きの問題を解き、解けたらレ点の横に○、間違えたらレ点を書き加える
8, ○がついていないレ点の問題を解く
9, 80問を何度も繰り返し解く
10, 試験直前は暗記シートだけ見る

(余裕がある方向け)
公式テキスト80問は何周やっても完璧、という状態で試験までに余裕があったら以下の公式模擬問題(50問)も解いておきましょう。
https://gist.github.com/sean2121/945035ef2341f0c39bf40762cd8531e0

大切なのは、収録されている80問を最初にすべて解くことです。
数百ページあるテキストを最初から読んでいたらいつまでたっても終わりませんし、膨大な量のメソッドやその返り値を手当たり次第に覚えていくのは非常に効率が悪いです。

まず問題を解いて、不足している知識を洗い出してから詳細のページを見に行けばいいです。
解説で十分事足りることもしばしば。

テキスト1冊だけで勉強するのは不安だと思います。
が、中途半端な状態にしてWEBのいろんな問題に手を出す方が危険です。

この試験は傾向がはっきりしていますので、80問きっちりできれば合格ラインは問題ないと感じます。
問題の出され方に慣れておきましょう。

例えば、以下の問題を解いてみてください。

以下の出力はどうなりますか?
a="ruby silver"
a.upcase # アルファベットを大文字にするメソッド
"HELLO" + " " + a

puts a #=>

正解は"ruby silver"です。
"HELLO RUBY SILVER"ではありません。(僕は最初ひっかかった)
upcaseメソッドは非破壊的メソッドのため、レシーバー(メソッドの呼び出し元)自体は変化しません。
破壊的メソッドは!がつくことがほとんどですが、配列を結合するconcatなど一部メソッドは!なしで破壊的メソッドなので要注意(concatはめっちゃ出る)。

以下の出力はどうなりますか?
a="ruby silver" #=> "ruby silver"
a.upcase #=> "RUBY SILVER"
"HELLO" + " " + a #=> "HELLO RUBY SILVER"

puts a #=> "ruby silver"(aの中身自体は変化していない)

このように、一生懸命勉強してupcaseメソッドの処理を知っていたとしても、問題に正解できるかどうかは別なのです。

まとめ

公式テキストの問題(全80問)が完璧に理解できていることがめっちゃ重要

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

【Ruby】メソッドジャンプをしたい!

はじめに

環境macOS Catalina10.15.5

Rubyで他の人が書いたソースコードを読むときにメソッドの定義場所がすぐわかるようにメソッドジャンプがしたいなと思い、いろいろ試してたどり着いた方法を書き記します

私はテキストエディタはAtomを主に使っていたのですが、RubyのコードをAtomでメソッドジャンプするのは、いろいろ試しましたがうまくいきませんでした、なので

結論 VSCodeを使うのが簡単

という事になりまりました

導入手順

1.こちらでVSCodeをダウンロード

https://azure.microsoft.com/ja-jp/products/visual-studio-code/

2.日本語化

  • メニューバーからviewを選択
  • command palette を選択
  • configure display languageを選択
  • install additional languageを選択
  • Japanese Language Pack for Visual Studio Codeのパッケージを探す
  • もう一度configure display languageを選択
  • jaを選択
  • VSCode再起動

3.パッケージのRubyをインストール

拡張機能の検索ボックスで検索できます

4.ジャンプの機能を有効化

インストールしたRubyの拡張機能の設定を開いて
Ruby:Intellisenseのところをfalseからrubylocateに変更

5.メソッドジャンプのやりかた

⌘を押しながらジャンプしたいメソッドをクリックすると定義場所にとべます!

ここまで15分程度だったのでとても簡単でした!

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