20210221のRubyに関する記事は21件です。

【Ruby】オブジェクト指向設計の単一責任のクラスを設計する|インスタンス変数とアクセサメソッドに関して

オブジェクト指向設計実践ガイド~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方を読んでいて、インスタンス変数とアクセサメソッドの違いに関して興味を持ったのでまとめます。

詳細の内容は、本書の第二章の『単一責任のクラスを設計する』に書かれています。

オブジェクトとデータ

オブジェクトは「データ」と「振る舞い」を持ちます。
データは、任意のデータ(文字列、ハッシュ、配列など)をインスタンス変数内に保持します。

データへのアクセスは次のうちのどちらかの方法で行われます。

  • インスタンス変数を直接参照する
  • インスタンス変数をアクセサメソッドで包み隠す

それぞれに関してみていきましょう。

①インスタンス変数を直接参照する

Gearクラスにギア比を計算するratioメソッドを定義しています。

class Gear
  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    @chainring / @cog.to_f  # 破滅への道
  end
end

メソッドratioは何がダメなのでしょうか?
理由は、インスタンス変数を直接参照しているからです。

例えば、「@cog」のデータ(内容)を変更する場合を考えてみます。
@cog」を複数箇所で参照していた場合、全ての箇所で「@cog」のデータを変更する必要があります。

下記のサンプルコードをみてみましょう。

変更前
class Gear
  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    @chainring / @cog.to_f # @cogを使用
  end

  def ratio_second
    # 何らかの処理
    @chainring / @cog.to_f # @cogを使用
  end

end

予期せぬ調整が入った場合、下記2箇所でメソッドの変更が必要になります。

変更後
class Gear
  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def unanticipated_adjustment_factor # 予期せぬ調整
    2
  end

  def ratio
    @chainring / (@cog * unanticipated_adjustment_factor).to_f  # 変更が必要
  end

  def ratio_second
    # 何らかの処理
    @chainring / (@cog * unanticipated_adjustment_factor).to_f  # 変更が必要
  end

end

これは、データ(どこからでも参照される)から振る舞い(1箇所で定義される)へと変えることによって解決することができます。

独自のメソッドを用意し、振る舞いを変えて対応させます。
そして、クラス内では常にデータではなく、振る舞いに依存させます。

②インスタンス変数をアクセサメソッドで包み隠す

データではなく振る舞いに依存させるためには、このように書き換えます。

class Gear
  attr_reader :chainring, :cog  # 追加

  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    chainring / cog.to_f  # 変更
  end
end

この場合、サンプルコードはどのように変わるでしょうか?

変更前
class Gear
  attr_reader :chainring, :cog

  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    chainring / cog.to_f # cogメソッドを使用
  end

  def ratio_second
    # 何らかの処理
    chainring / cog.to_f # cogメソッドを使用
  end

end

メソッド(振る舞い)に依存させることによって、予期せぬ変更に対して変更箇所は1箇所で終了します。

変更後(予期せぬ変更が発生)
class Gear
  attr_reader :chainring, :cog

  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def cog
    @cog * unanticipated_adjustment_factor # ここのみを変更
  end

  def unanticipated_adjustment_factor # 予期せぬ調整
    2
  end

  def ratio
    chainring / cog.to_f  # ここは変更しなくて良い
  end

  def ratio_second
    # 何らかの処理
    chainring / cog.to_f  # ここは変更しなくて良い
  end

end

@kts_h さんからサンプルコードに関してご指摘をいただきました!ありがとうございます!
また、「歯車(cog)の数が不正確な場合」の記述例を挙げていただきました。
※詳細はコメント欄で確認できます。

用語

参考として使っているメソッドを簡単にまとめます。

attr_reader

attr_readerを使うと、Rubyは自動で下記メソッドをつくります。

# attr_readerによるデフォルトの実装
def cog
  @cog
end

参考:Module#attr_reader (Ruby 3.0.0 リファレンスマニュアル)

このcogメソッドは、コードの中で唯一cogが何を意味するか?を知っています。
このメソッドを実装することによって、データが振る舞い(1箇所で定義される)へと変わります。

そのため、データである@cogを変更する必要が出た場合は、「@cog」に関して複数箇所の変更をする必要がなく、コグについての独自メソッドを定義するだけでよくなります。

def cog
  if 条件
    @cog * 特殊な計算が発生
  else
    @cog
  end
end

initialize

Rubyのコンストラクタです。
コンストラクタはオブジェクト生成時に自動的に呼び出され、クラスのデータ初期化処理を行う特別なメソッドです。
参考:Object#initialize (Ruby 3.0.0 リファレンスマニュアル)

【おまけメモ】 attr_readerを利用時の挙動の違い

initialize という名前のメソッドは自動的に private に設定されます。
そのため、クラスの外からアクセスすることができません。

失敗例
class Gear
  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    @chainring / @cog.to_f
  end
end

gear1 = Gear.new(10, 20)
puts gear1.cog
実行結果
 `<main>': undefined method `cog' for #<Gear:0x00007fb49015a968 @chainring=10, @cog=20> (NoMethodError)

クラス外からアクセスするにはゲッター/セッターメソッドが必要です。

成功例
class Gear
attr_reader :cog

  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    @chainring / @cog.to_f
  end
end

gear1 = Gear.new(10, 20)
puts gear1.cog
実行結果
=> 20

attr_readerを使うとこのような挙動の違いが発生します。

【まとめ】データではなく、振る舞い(メソッド)に依存させる

振る舞い(メソッド)に依存させることによって、変更しやすいコードにすることができました。

インスタンス変数の隠蔽

インスタンス変数は常にアクセサメソッドで包み、直接参照しないようにすることが本書で推奨されています。

【疑問】 振る舞いではなく、データに依存させてみる

変更をしやすくするための解決方法として、振る舞いに依存させず、データを変えることによって対応させる方法はどうでしょうか?

class Gear
  def initialize(chainring, cog)
    @chainring = chainring
    if 条件
      @cog = cog * 特殊な計算が発生
    else
      @cog = cog
    end
  end

  def ratio
    @chainring / @cog.to_f
  end
end

initializeというメソッドはあくまでも「データの初期化という役割」を果たすため、「データの中身に対する処理」を持つのは適切ではないため、このパターンはアンチパターンなのではないでしょうか?

本書を読み進めていくうえで理解が深まり、アンチパターンであるという理由が言語化されることを期待します。

コードや内容に関して不備や改善点等ございましたら、ご指摘お待ちしております。

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

MiniMagickで画像がresizeできなかった時の対処法

PF作成中に生じたエラーです。オチは非常に簡単なものだったので前提条件は省きます。

erbファイルに以下の様に記述してサーバーを起動し、quiz_image(image_tagで表示されるはずの画像)を確認しようとしました。<%= image_tag quiz.quiz_image.variant(resize: '300×300'), class: 'card-img-top' %>だけ見てくだされば大丈夫です。

_quiz.html.erb
<% if quiz.quiz_image.attached? %>
  <%= image_tag quiz.quiz_image.variant(resize: '300×300'), class: 'card-img-top' %>
<% end %>
<div class="card-body">
  <h3 class="card-title"><%= quiz.title %></h3>
  <p class="card-text"><%= quiz.sentence %></p>
  <% quiz.tags.each do |tag| %>
    <%= tag.name %>
  <% end %>
  <%= link_to '問題詳細へ', quiz, class: 'btn btn-primary'  %>
</div>

しかし、画像が表示されません。そこでターミナルを確認すると以下の様に書いてありました。

MiniMagick::Error (`gm convert /var/folders/43/2kbncrm93jsb1tpd11tsys0r0000gn/T/ActiveStorage-24-20210221-35258-1rq06qb.jpeg[0] -auto-orient -resize 100×100 /var/folders/43/2kbncrm93jsb1tpd11tsys0r0000gn/T/image_processing20210221-35258-1uhkzzk.jpeg` failed with error:
gm convert: Option '-resize' requires an argument or argument is malformed.
):

最後の一文をざっくり翻訳すると「resizeメソッドの文法がなんかおかしいよ」とあります。(malformedとは奇形という意味です)

みたところ変なところはないのでとりあえずググってみました。すると以下の記事を発見しました。

https://askubuntu.com/questions/629808/resizing-with-imagemagick-error-message

どうやら
'300×300'
ではなく
'300x300'
が正しい様です。一見同じ様に見えますが'×(かける)'と'x(エックス)'が違います。

書き直して再確認するとうまく行きました。どうやらASCIIには×という文字は存在しない様で、xで代用しているみたいです。

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

Ruby on Rails: scopeはnilを返す使い方をしてはいけない

はじめに

Railsで開発している時、scopeを使うシーンは多いと思います。
しかし、実務で使用している際、予期しない動作をすることがあったので、調べたことをまとめておきます。

実行環境

  • macOS Catalina 10.15.7
  • ruby 2.6.6
  • Rails 5.2.4.3

事前準備

今回はnameというカラムを持つ、Userというモデルを使用していきます。
また、hogeという名前とfugaという名前を持つ2つのレコードが入っている想定です。

> User.all
=> User Load (1.1ms)  SELECT `users`.* FROM `users`
#<User id: 1, name: "hoge">,
#<User id: 2, name: "fuga">

scopeでUserを検索する

まずは通常通りscopeでUserを検索してみましょう。
scope自体の使い方の説明が主目的ではないので、簡単に特定の名前で検索するscopeを考えます。

class User < ApplicationRecord
  scope :find_hoge, -> () { find_by(name: "hoge") }
end

このscopeを使うことで、namehogeであるuserを検索することができます。
発行されるsqlをみても、きちんとnamehogeであるレコードを検索できていることが分かるかと思います。

> User.find_hoge
  User Load (0.8ms)  SELECT  `users`.* FROM `users` WHERE `users`.`name` = 'hoge' LIMIT 1
=> #<User: id: 1, name: "hoge">

scopeでnilを返してみる

さて、それでは次に結果がnilを返すscopeを作成してみます。

class User < ApplicationRecord
  ...

  scope :find_nobody, -> () { find_by(name: "nobody") }
end

上記のscopeではnamenobodyであるレコードを検索していますが、該当するレコードが無いためnilを返すと思われます。
実際に実行してみましょう。

> User.find_nobody
  User Load (1.4ms)  SELECT  `users`.* FROM `users` WHERE `users`.`name` = 'nobody' LIMIT 1
=> User Load (1.2ms)  SELECT `users`.* FROM `users`
#<User id: 1, name: "hoge">,
#<User id: 2, name: "fuga">

あれ?sqlが2回実行されていますね。
しかもallをした時と同様の挙動をしてしまっています。
どうやら結果もnilではないようです。

> User.find_nobody.nil?
  User Load (1.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`name` = 'nobody' LIMIT 1
=> false

原因

この挙動の原因は、scopeの仕様にあります。

scopeはメソッドチェインなどで複数の条件を結合して使用することが想定されているため、結果がnilとなった場合は該当scopeの検索条件を除外したクエリを発行するという仕様になっているようです。

今回のケースではscopeの中身を直接実行してみると、想定通りnilを返していることが分かります。

> User.find_by(name: "nobody")
  User Load (1.2ms)  SELECT  `users`.* FROM `users` WHERE `users`.`name` = 'nobody' LIMIT 1
=> nil

そのため、find_nobodyの条件を除外したクエリ、つまり検索条件の無いallの結果が返ってきてしまっているのです。

実際に、scopeを連結してみると、上記の仕様がよく分かるかと思います。
find_nobodyfind_hogeをチェインして使用してみましょう。

> User.find_nobody.find_hoge
  User Load (3.9ms)  SELECT  `users`.* FROM `users` WHERE `users`.`name` = 'nobody' LIMIT 1
  User Load (2.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`name` = 'hoge' LIMIT 1
=> #<User: id: 1, name: "hoge">

nobodyについて検索した後、hogeのみの条件で検索するクエリが発行されていますね。

対応策

ここまで見てきたように、nilを返す可能性がある場合はscopeで実装してしまうと予期しない結果となることがあるので、クラスメソッドで実装しておくのが良いかもしれません。

class User < ApplicationRecord
  ...
  ...

  def self.find_nobody
    find_by(name: "nobody")
  end
end

> User.find_nobody
  User Load (0.7ms)  SELECT  `users`.* FROM `users` WHERE `users`.`name` = 'nobody' LIMIT 1
=> nil

無事にnilが返ってきますね。

まとめ

scopeを使うことで頻繁に使用する処理などをまとめておけるのが便利です。
しかし、nilを返す場合には今回紹介したように想定と異なる動作をする場合があるので、注意する必要がありそうです。

参考

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

【Rails】ゲストログイン用のユーザー作成〜ポートフォリオに最適〜

アプリケーションにテストログイン機能をつけた話し

まだまだやりたいことはいっぱいあるけれど、AWSへのデプロイが終わったので見てもらうことを考えてゲストユーザーでのログイン機能をつけることにしました。

諸先輩方のアプリケーションを見てもついていない人はいないぐらい実装率が高いので今や当たり前の機能になっていると感じます。
そもそもログインして見てもらうのにわざわざ登録させるのも気が引けるし、せっかく時間を割いて見にきてくれるのだから、おもてなしの心があるべきだと思います。

ではdevise導入済みで話しを進めていきます。

1・app/controllers配下にusersがない場合

まずはコントローラーに新しいファイルを作成。

$rails g devise:controllers users

⬇️できたファイル

app
 ┣controllers
      ┣users
         ┗confirmations_controller.rb
         ┗passwords_controller.rb
         ┗registrations_controller.rb
         ┗sessions_controller.rb

users直下に4つのファイルができました。

2・routes.rbに記述

routes.rb
# devise_for :usersの部分に追加
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    sessions: 'users/sessions'
  }

〜略〜

# resources :users, only: [:show] がある場合はその下に記述
devise_scope :user do
 post 'users/guest_sign_in', to: 'users/sessions#new_guest'
end

3・sessions_controller.rbにてメソッドを定義

sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
  def new_guest
    user = User.guest
    sign_in user
    redirect_to root_path, notice: "ゲストユーザーとしてログインしました。"
  end
end

フラッシュメッセージはお好みで。

4・userモデルにメソッドを定義

user.rb
# モデルに定義なのでselfからスタート

def self.guest
  find_or_create_by!(email: 'test@test.com') do |user|
    user.password = SecureRandom.alphanumeric(10) + [*'a'..'z'].sample(1).join + [*'0'..'9'].sample(1).join
    user.nickname = 'ゲストユーザー'
    user.gender_id = '4'   ⬅️ActiveHashの入力
    user.genre_id = '9'    ⬅️ActiveHashの入力
  end
end

ActiveHashを利用している場合はuser.gender_id = '4'などの記述で問題ありませんでした。
ゲストログインも通常のユーザー登録をする流れと同じ流れを辿るので、バリデーションがかけられていると未入力項目に対してエラーが出てしまう。
ここでしっかりと項目をうめておく。
passwordは最初SecureRandom.urlsafe_base64としていましたがAWSへのデプロイ時にエラーになり、ログを見るとバリデーションが不正とのこと。
記述を変えたら本番環境でも成功しましたので、もし引っかかってしまったら試してみて下さい。

5・ビューファイルを変更

new.html.erb
<%= link_to 'ゲストログインはこちら', users_guest_sign_in_path, method: :post %>

ボタンを作る場所などレイアウトに合わせて適用。

参考にさせて頂きました

@take18k_tech
簡単ログイン・ゲストログイン機能の実装方法(ポートフォリオ用)

@rie1224
Railsでテストアカウントでの簡単ログインの実装方法

ありがとうございました。

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

Ruby on Rails における_pathと_urlの違い

現在、RailsTutorialを学習中の駆け出しエンジニアの者です。

railsヘルパーの一種であるpathとurlの違いを理解できていませんでしたので、自分なりに調べてみました。

_pathは相対パス、_urlは絶対パス

(例)
root_path => '/'
root_url  => 'http://www.example.com/'

new_user_path => '/users/new'
new_user_url => 'http://www.example.com/users/new'

相対パス

_pathは相対パスを示しています。相対パスはアプリケーションから見て、どのファイルやフォルダの位置にあるのかを示します。上の例であるnew_user_pathで説明しますと、viewフォルダの中のusersフォルダの中のnew.html.erbを呼び出していることになります。

絶対パス

_urlは絶対パスを指定しています。絶対パスとは、そのページをインターネットからアクセスする際のurlを示します。 なので、今いる位置に関係なく呼び出すことができますし、外部のサイトを呼び出すこともできます。

使用方法の違い

Railsの開発においての_pathと_urlの使用方法の違いを説明します

_pathの使用方法

_pathはredirect_to以外でよく使用します。

例えばlink_toなどで使用します。

_urlの使用方法

redirect_toのときに使用する。
なので、Railsにおいてはコントローラの際に使用することが多いです。

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

絵文字を含むファイルとその行数を再帰的にgrepしたい

英語/日本語で調べたけど中々見つからなかったので、同じようなことをやりたい人のご参考になれば嬉しいです :pray: :sparkles:

:three: 行まとめ

  • 絵文字を Font Awesome 等に置き換えたい! :bulb:
  • 絵文字を含むファイルと行数ってどこだっけ...? :sweat_drops:
  • 再帰的に絵文字を探す emoji-search を書いてみた :wrench: :dash:

:mag: emoji-search の実行結果の例

例えば未踏ジュニアの公式Webサイト (:octocat: mitou/jr.mitou.org) のルートディレクトリで emoji-search を実行すると、次のような結果が得られます。

╭─○ yasulab ~/jr.mitou.org
╰─○ emoji-search
alumni.md: ? 今年度より採択開始!
guideline.md: ※ 2021年度の応募は3月上旬から受け付ける予定です。 ?‍
README.md: どの箇所を更新するにせよ、Webブラウザが一番簡単だと思います...!! ?
_site/README.md: どの箇所を更新するにせよ、Webブラウザが一番簡単だと思います...!! ?
_site/guideline.html: ※ 2021年度の応募は3月上旬から受け付ける予定です。 ?‍
_site/download.html: 提案書の準備ができたら応募フォームへ! ?
_site/download.html: ※ 2021年度の応募は3月上旬から受け付ける予定です。 ?‍
mentors.md: ? 今年度より採択開始!
download.md: 提案書の準備ができたら応募フォームへ! ?
download.md: ※ 2021年度の応募は3月上旬から受け付ける予定です。 ?‍

:gem: emoji-searchコマンドの中身

当初はシェルスクリプトで書こうとしたのですが、簡単ではなさそうだったので断念 :sweat:

標準ライブラリだけで書きたいなーと思って色々試したところ、Ruby で書くのが早そうだったので、結果として下記のようなコードになりました。コメントも付けてみたので、コードを読むご参考になれば! :pray:

# Binaryファイルを弾くよ
require "open3"
def is_text_file?(filename)
  file_type, status = Open3.capture2e("file", filename)
  status.success? && file_type.include?("text")
end

# 絵文字を探していくよ
Dir.glob('**/*').each do |filename|
  # `.git`ディレクトリやBinaryファイルを調べると遅くなるのでスキップ
  next if filename.start_with? "."
  next unless is_text_file? filename

  File.open(filename) do |f|
    f.each_line do |line|
      # BMP 外の文字が含まれるか否かで探して、見つけたら出力するよ
      # 詳細: https://qiita.com/yusukeyamane/items/79764e36cc5bd89bc9a4#comment-e879ba227ab95b006017
      puts("#{filename}: #{line}") if line.match?(/[^\u0000-\uFFFF]/)
    end
  end
end

あとは bash/zsh などで emoji-search をエイリアスに設定して、

alias emoji-search="ruby /path/to/emoji-search.rb"

調べたい場所で emoji-search すると完成です :book: :mag: :dash: 今回は標準ライブラリのみで書いているので、Ruby が入っていれば大体使えるはず...!!

皆さんの絵文字ライフが快適になれば嬉しいです! :laughing: :sparkles:

:white_check_mark: オマケ: なんで /\p{Emoji}/ を使わないの?

他のQiita記事のコメント欄@scivola さんが解説されていますが、予想以上にマッチしてしまっていたため別のアプローチを採りました :sweat:

例えばコードの一部を次のように変更し、

- puts("#{filename}: #{line}") if line.match?(/[^\u0000-\uFFFF]/)
+ puts("#{filename}: #{line}") if line.match?(/\p{Emoji}/)

再度 emoji-search コマンドを実行すると、 # を含む部分なども引っかかってしまいました :eyes: :sweat_drops:(Ruby 2.7.2 で実行)

╭─○ yasulab ~/jr.mitou.org
╰─○ emoji-search

... (省略)

download.md: ## 提案書テンプレート {#template}
download.md: 提案書のテンプレートを以下からダウンロードして、PDFに変換後、[応募フォーム](#ready)からアップロードしてください。
download.md: <a href="https://jr.mitou.org/assets/other/mitoujr_application_2021.zip" class="button">Word・Pages を使う</a>
download.md: <a href="https://docs.google.com/document/d/1hjDYf2DbFBkXLyrAl9HKKc9sS40XbZ_iN2j-HKZXD9g/edit?usp=sharing" class="button" target="_blank">Google Docs を使う</a>
download.md: ## 提案書サンプル {#sample}
download.md: <a href="https://jr.mitou.org/assets/other/2020_application_samples.zip" class="button">過去の提案書を見る</a>
download.md: ## 準備できた? {#ready}
download.md: <p class="text-center">提案書の準備ができたら応募フォームへ! ?</p>
download.md: ※ 2021年度の応募は3月上旬から受け付ける予定です。 ?‍

他の正規表現もいくつか試したところ、今回の用途に限って言えばコメント欄にある /[^\u0000-\uFFFF]/ だと欲しい情報が得られたので、今回はその正規表現を使っています :pray: :sparkling_heart:

絵文字は奥が深いですね... :eyes: :thought_balloon:

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

絵文字が含まれるファイルを再帰的にgrepしたい

英語/日本語で調べたけど中々見つからなかったので、同じようなことをやりたい人のご参考になれば嬉しいです :pray: :sparkles:

:three: 行まとめ

  • 絵文字を Font Awesome に置き換えたい! :bulb:
  • 絵文字を含むファイルってどこだっけ...? :sweat_drops:
  • 再帰的に絵文字を探す emoji-search を書いてみた! :wrench: :dash:

:mag: emoji-search の実行例

例えば未踏ジュニアの公式Webサイト (:octocat: mitou/jr.mitou.org) のルートディレクトリで emoji-search を実行すると、次のような結果が得られます。

╭─○ yasulab ~/jr.mitou.org
╰─○ emoji-search
alumni.md: ? 今年度より採択開始!
guideline.md: ※ 2021年度の応募は3月上旬から受け付ける予定です。 ?‍
README.md: どの箇所を更新するにせよ、Webブラウザが一番簡単だと思います...!! ?
_site/README.md: どの箇所を更新するにせよ、Webブラウザが一番簡単だと思います...!! ?
_site/guideline.html: ※ 2021年度の応募は3月上旬から受け付ける予定です。 ?‍
_site/download.html: 提案書の準備ができたら応募フォームへ! ?
_site/download.html: ※ 2021年度の応募は3月上旬から受け付ける予定です。 ?‍
mentors.md: ? 今年度より採択開始!
download.md: 提案書の準備ができたら応募フォームへ! ?
download.md: ※ 2021年度の応募は3月上旬から受け付ける予定です。 ?‍

:gem: emoji-searchコマンドの中身

当初はシェルスクリプトのgrep/findなどで書こうとしたのですが、簡単ではなさそうだったので断念 :sweat:

標準ライブラリだけで書きたいなーと思って色々試したところ、Ruby で書くのが早そうだったので、結果として下記のようなコードになりました。コメントも付けてみたので、コードを読むご参考になれば! :pray: (grepコマンドは使っていませんがgrepっぽい見た目に近づけています)

# Binaryファイルを弾くよ
require "open3"
def is_text_file?(filename)
  file_type, status = Open3.capture2e("file", filename)
  status.success? && file_type.include?("text")
end

# 絵文字を探していくよ
Dir.glob('**/*').each do |filename|
  # `.git`ディレクトリやBinaryファイルを調べると遅くなるのでスキップ
  next if filename.start_with? "."
  next unless is_text_file? filename

  File.open(filename) do |f|
    f.each_line do |line|
      # BMP 外の文字が含まれるか否かで探して、見つけたら出力するよ
      # 詳細: https://qiita.com/yusukeyamane/items/79764e36cc5bd89bc9a4#comment-e879ba227ab95b006017
      puts("#{filename}: #{line}") if line.match?(/[^\u0000-\uFFFF]/)
    end
  end
end

あとは bash/zsh などで emoji-search をエイリアスに設定して、

alias emoji-search="ruby /path/to/emoji-search.rb"

調べたい場所で emoji-search すると完成です :book: :mag: :dash: 今回は標準ライブラリのみで書いているので、Ruby が入っていれば大体使えるはず...!!

皆さんの絵文字ライフが快適になれば嬉しいです! :laughing: :sparkles:

:white_check_mark: オマケ: なんで /\p{Emoji}/ を使わないの?

他のQiita記事のコメント欄@scivola さんが解説されていますが、予想以上にマッチしてしまっていたため別のアプローチを採りました :sweat:

例えばコードの一部を次のように変更し、

- puts("#{filename}: #{line}") if line.match?(/[^\u0000-\uFFFF]/)
+ puts("#{filename}: #{line}") if line.match?(/\p{Emoji}/)

再度 emoji-search コマンドを実行すると、 # を含む部分なども引っかかってしまいました :eyes: :sweat_drops:(Ruby 2.7.2 で実行)

╭─○ yasulab ~/jr.mitou.org
╰─○ emoji-search

... (省略)

download.md: ## 提案書テンプレート {#template}
download.md: 提案書のテンプレートを以下からダウンロードして、PDFに変換後、[応募フォーム](#ready)からアップロードしてください。
download.md: <a href="https://jr.mitou.org/assets/other/mitoujr_application_2021.zip" class="button">Word・Pages を使う</a>
download.md: <a href="https://docs.google.com/document/d/1hjDYf2DbFBkXLyrAl9HKKc9sS40XbZ_iN2j-HKZXD9g/edit?usp=sharing" class="button" target="_blank">Google Docs を使う</a>
download.md: ## 提案書サンプル {#sample}
download.md: <a href="https://jr.mitou.org/assets/other/2020_application_samples.zip" class="button">過去の提案書を見る</a>
download.md: ## 準備できた? {#ready}
download.md: <p class="text-center">提案書の準備ができたら応募フォームへ! ?</p>
download.md: ※ 2021年度の応募は3月上旬から受け付ける予定です。 ?‍

他の正規表現もいくつか試したところ、今回の用途に限って言えばコメント欄にある /[^\u0000-\uFFFF]/ だと欲しい情報が得られたので、今回はその正規表現を使っています :pray: :sparkling_heart:

絵文字は奥が深いですね... :eyes: :thought_balloon:

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

アセットパイプラインについて解説

アセット(assets)とは

railsのapp/assetsディレクトリにはimages、javascripts、stylesheetsの3つのディレクトリがあります。
それぞれ画像ファイル、javascriptファイル、cssファイルを置くためのディレクトリです。

これらの事をアセット(assets)と呼びます。

アセットパイプラインとは

アセットパイプラインはstylesheetsディレクトリ以下の複数のファイルを1つに結合し、1個のcssファイルを生成する事をいいます。アセットパイプラインの最大の特徴はファイルの変換と結合です。

sassとは

SassとはCSSを拡張したスタイルシート言語のことです。
ブラウザではSassを理解できないため、サーバー側でsassをcssに変換してやる必要があります。この処理の事をコンパイルと呼びます。よく使用される拡張子.scssはsassの一種です。scssはapp/assets/stylesheetsに配置します。

なぜファイルを結合する必要があるのか

なぜ複数のファイルを結合するかというと、ブラウザとサーバー側のHTTP通信の回数を減らすためです。これによってサーバーへの負荷を減らすことができます。

アセットパイプラインの利用方法

stylesheetsディレクトリのapplication.cssを開くと以下の記述があると思います。

/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_tree .
 *= require_self
 */


まずはこれらを解説していきます。
コメント中の*はディレクティブと呼ばれ、アセットパイプラインの設定を行なっています。

 *= require_tree .

はアセットパイプラインが処理するファイルの設定範囲を設定しています。.(ドット)はstylesheetsディレクトリ以下の全ファイルを処理対象として指定しています。

ここを対象のディレクトリにしたい場合は

 *= require_tree ./対象のディレクトリ名

とします。

*= require_self

次に*= require_selfですが、これはself=自分自身、つまりはapplication.css自身をアセットパイプラインの処理対象とする事を宣言しています。

スタイルシートを目的別に分ける方法

まずstylesheetディレクトリにadmin.cssを作成します。

*
*= require_tree ./admin
*/

/config/initializers/assets.rbに以下を記述します。

Rails.application_config.assets.precompile += %w( admin.css)

これによりadmin.cssがプリコンパイルの対象に加わります。

次にstylesheetsディレクトリにadminディレクトリを作成し、layout.scssを作成しスタイルを記述します。

続いてviews/lauouts/application.html.erbをadmin.cssに変更します。

sample_app/app/views/layouts/admin.erb

<!DOCTYPE html>
<html>
  <head>
    <title><% title_helper %></title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'admin', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

link_tagをadminに変更しています。

/application.controller

layout :set_layout

private def set_layout
    if params[:controller].match(%r{\A(admin)/})
      Regexp.last_match[1]
    else
      "customer"
    end
end

上記によりset_layoutメソッドはadminという文字列を返します。この文字列がレイアウトとして使用されます。あとはこれを繰り返すことで利用別ごとにスタイルシートを作成できます。

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

【Rails】ActionTextに検索機能を適用する方法(検索機能を自作する)

はじめに

ActionTextとは、Rails6から追加された機能で下記写真のようにリッチテキストを編集するためのエディタが簡単に作れるようになります。
スクリーンショット 2021-02-21 12.40.10.png
しかし、一方で課題もありActionTextを適用していると検索機能を簡単に実装できるgemであるransackを使用できなくなってしまいます。

そのため、今回はgemを使わず自作で検索機能を作成し、ActionTextを適用しているカラムも検索できるようにしていきたいと思います。

今回の課題

postsテーブルのtitleカラムとcontentカラムに検索機能を適用したいためransackを導入。しかし、ActionTextを適用したcontentカラムにはransackが適用されず検索できない。

原因は何か

まず今回の検索対象であるPostモデルを確認します。

app/models/post.rb
# == Schema Information
#
# Table name: posts
#
#  id         :bigint           not null, primary key
#  category   :integer          default("knowhow"), not null
#  status     :integer          default("draft"), not null
#  title      :string           not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#  user_id    :bigint
#
# Indexes
#
#  index_posts_on_user_id  (user_id)
#
class Post < ApplicationRecord
  has_rich_text :content

  belongs_to :user

  validates :title, presence: true
  validates :title, length: { minimum: 2, maximum: 30 }
  validates :content, presence: true
end

contentカラムにActionTextを適用するためにhas_rich_text :contentを記述しています。
ここで重要なのはPostモデルには「contentカラム」が存在していないということです。

ではcontentカラムのデータはどこに格納されているのでしょうか?
実際に、下記のように投稿して確かめてみます。
スクリーンショット 2021-02-21 13.32.03.png
コンソールを確認すると以下のようになっています。
スクリーンショット 2021-02-21 13.30.14.png
「QiitaにActionTextに関する記事の投稿」というデータはpostsテーブルのtitleカラムに格納されており、「ActionTextを使用しているとransackが使えない?」というデータはaction_text_rich_textsテーブルのbodyカラムに格納されていることがわかります。

実際に、action_text_rich_textsテーブルを確認すると以下のようになっています。

db/migrate/create_action_text_tables.action_text.rb
# This migration comes from action_text (originally 20180528164100)
class CreateActionTextTables < ActiveRecord::Migration[6.0]
  def change
    create_table :action_text_rich_texts do |t|
      t.string     :name, null: false
      t.text       :body, size: :long
      t.references :record, null: false, polymorphic: true, index: false

      t.timestamps

      t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
    end
  end
end

つまり、今回postsテーブルのcontentカラムに検索機能を適用できなかったのは、postsテーブルにcontentカラムが存在せず、他のテーブルにデータが渡っていたことが原因であると推測できます。

解決方法

原因は「contentカラムのデータがaction_text_rich_textsテーブルに渡っていたこと」だと推測できたので、action_text_rich_textsのテーブルをpostsテーブルに内部結合し該当カラムを取り出せるようにすれば解決できるのではないかと考えました。
画像.png


結論

Postモデルに以下を記述することでcontentカラムを検索することができるようになりました。
ちなみに今回はSQLの知識も使っているのでこちらの記事を参考にしていただけると幸いです。

app/models/post.rb
class Post < ApplicationRecord
  has_rich_text :content

  scope :search, -> (search_param = nil) {
    return if search_param.blank?
    joins("INNER JOIN action_text_rich_texts ON action_text_rich_texts.record_id = posts.id AND action_text_rich_texts.record_type = 'Post'")
    .where("action_text_rich_texts.body LIKE ? OR posts.title LIKE ? ", "%#{search_param}%", "%#{search_param}%")
  }
end

では、細かく分解して内容を確認していきましょう。

① scope :search, -> (search_param = nil) {}

まずはscopeについてです。
scopeはActiveRecordの機能の一部で、モデルに定義するとクラスメソッドのように呼び出せます。
もちろんコントローラに直接書くこともできるとは思いますが、ファットコントローラを避けるために今回はPostモデルにsearchというscopeを定義しています。

scope :スコープ名, -> { 条件式 }

引数にはsearch_params = nilを指定しています。

② return if search_param.blank?

この文章は検索フォームに何も入力されていない状態で検索が実行された場合に早期リターンできるように定義しています。

returnに何も記述していないのは、未入力で検索が実行された場合にnilを返すためです。

なぜあえてnilを返しているかというと、scopeメソッドはクラスメソッドとは違って、nilの場合にallメソッドが実行されるからです。

これにより、未入力状態で検索が実行されてもPost.allしている状態になるので全ての投稿を表示したままにできます。

③ joins("INNER JOIN action_text_rich_texts ON action_text_rich_texts.record_id = posts.id AND action_text_rich_texts.record_type = 'Post'")

ここではjoinsメソッドで引数内をPostモデルに内部結合しています。
内部結合しているテーブルはaction_text_rich_textsテーブルです。

またON句を使用して結合条件を定義しています。
今回定義した結合条件は以下の通りです。

結合条件の内容
 action_text_rich_texts.record_id = posts.id 

record_idpostsidカラムが一致すること

 action_text_rich_texts.record_type = 'Post'

racord_typePostモデルと紐づいていること
④ .where("action_text_rich_texts.body LIKE ? OR posts.title LIKE ? ", "%#{search_param}%", "%#{search_param}%")

ここではwhereメソッドを使用して、テーブル内の条件に一致したレコードを配列の形で取得しようとしています。

このままではわかりづらいので引数の中身をさらに2つに分解して考えてみます。

contentカラム(bodyカラム)の検索
.where( action_text_rich_texts.body LIKE ?, "%#{search_param}%" )
titleカラムの検索
.where( posts.title LIKE ? , "%#{search_param}%" )

ここでは3つポイントがあります。

1. [モデル].where( [カラム名] LIKE [パターン] )

LIKE述語はカラムのデータが、指定したパターンと一致した場合にTrueを返します。Trueが返された行は検索の対象となります。

2. LIKE ? , "[値]"

「?」はプレースホルダーと呼ばれるもので、第2引数の値を「?」で置き換えています。

3. %#{search_param}%

ここでは検索フォームに入ってきた値をRubyで埋め込んでいます。

また「%」は「0文字以上の任意の文字列」という意味の特殊記号なので、「%」で囲ってあげることにより曖昧な検索が可能となっています。


これらの処理によって、検索フォームに入力された値に合致するものをtitleカラムやcontentカラム(bodyカラム)から検索できるようになりました!

その他

検索機能のコントローラやビューの該当箇所は以下の通りです。

コントローラー
  def index
    @posts = Post.order(created_at: :desc)
    @posts = Post.search(params["q"]).order(created_at: :desc)
  end
ビュー
= form_tag posts_path, method: :get, class: 'ui action input fluid' do
  = text_field_tag :q, '', placeholder: 'キーワードで検索'
  = button_tag :class => 'ui icon button' do
    %i.search.icon

#ビューにはSemantic UIとHamlを使用しております

あとがき

僕は自作アプリの作成を決めた時、ransackを使用して検索機能を実装しようと考えていました。
そのため、今回の検索機能の自作はある種事故的なものではありましたが、以前会社の研修で学んだSQLの復習にもなったし、ロジックを考えて組み立てていく過程はとても楽しかったです!

ActionTextを使用している方がどれほどいらっしゃるかはわかりませんが、この記事がなんらかの参考になれば幸いです!

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

[memo]テストコード準備と実行

テストコードの準備

# Gemを追加 Gemfile
# group :development, :testというグループの中に記述
% gem 'rspec-rails', '~> 4.0.0'

# ターミナル
% bundle install

# ターミナル
% rails g rspec:install

# .rspecに設定を追加
--require spec_helper
--format documentation

テストコードを記述するファイルを用意

# ターミナル
# Userモデルのテストファイルを生成
% rails g rspec:model user

テストコードの実行

# ターミナル
% bundle exec rspec spec/models/user_spec.rb

FactoryBotのGem導入

# Gemfile
# group :development, :test do へ記述
gem 'factory_bot_rails'

# ターミナル
% bundle install

インスタンスの生成を切り出すファイルを作成

FactoryBotの記述を格納するディレクトリfactoriesと、
Userモデルに対するFactoryBotのファイル
users.rbを、以下のように手動で作成

spec > factories > users.rb

※この操作は、テストコードを記述するファイルを生成した後からFactoryBotを導入するときに必要です。
FactoryBot導入後はテストコードを記述するファイルを生成すると同時に、自動生成されます。

FactoryBotの記述を編集

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    nickname              {'test'}
    email                 {'test@example'}
    password              {'000000'}
    password_confirmation {password}
  end
end

ランダムな値の生成ができるFakerを導入

# Gemfile
# group :development, :test do へ記述
gem 'faker'

# ターミナル
% bundle install

FactoryBotの記述をFakerを使ったものへ編集

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    nickname              { Faker::Name.initials(number: 6) }
    email                 { Faker::Internet.free_email }
    password              { '000000' }
    password_confirmation { password }
  end
end

▼Fakerのリストは公式Github
https://github.com/faker-ruby/faker

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

【 Ruby on Rails 6.0 】heroku + Mysqlで本番環境にデプロイするよ

始めに

railsアプリをherokuを使って本番環境にデプロイする過程をまとめます。
herokuとMysqlの環境でデプロイを考えている人の助けになれば幸いです。
ちなみに今回はGithub Desktopを導入しているrailsアプリを作成済みという前提で話を進めていくので、gitとアプリ作成の説明は省略します。。

開発環境

  • macOS Catalina
  • Ruby 2.6.5
  • Ruby on Rails 6.0.0
  • heroku
  • Mysql
  • Github Desktop

目次

1.Gemfileの設定
2.config/datebase.ymlの設定
3.config/environments/production.rbの設定
4.bin以下のフォルダの設定
5.herokuの登録・設定
6.おまけ

1. Gemfileの設定

デプロイ予定であるherokuではPostgreSQL(略してpg)というデータベースがデフォルトでインストールされているため通常はpgを用いますが、今回はMysqlをデータベースとして使います。

まず、gem 'sqlite3'と、書いてある箇所があると思うのですが、それをコメントアウトします。

Gemfile
# sqlite3

代わりにMysqlをインストールします。

Gemfile
# 記述場所は本番環境でも適用されるように末尾にでも書きましょう。
gem 'mysql2', '>= 0.5.3' # バージョン0.5.3
php
$ bundle install

これでGemfileは完了です。

2. config/datebase.ymlの設定

Gemfileの設定でこのアプリは本番環境でMysqlを使います、という宣言は完了しました。
しかし実際にデータベースと接続する記述はまだ完了していません。
その接続の設定をする箇所がconfigフォルダの中にあるdatabase.ymlというファイルです。

ファイル内の下の方にあるproduction環境についての設定を、以下のように変更してください。

config/database.yml
production:
  <<: *default
  database: app_production
  username: root
  password: <%= ENV['DATABASE_PASSWORD'] %> # 環境変数を設定していない場合は空欄で
  socket: /var/lib/mysql/mysql.sock

これでデータベースの接続の記述は完了です。

3. config/environments/production.rbの設定

Railsは本番環境での動的な画像の表示がデフォルトでオフになっています。
画像を表示するために以下の記述をfalseからtrueに変更してください。
この記述により、assets以下のフォルダから動的にコンパイルしながらページを読み込みます。

config/environments/production.rb
#デフォルトでfalseとなっている以下の箇所をtrueに変更
  config.assets.compile = true

この工程を踏まないと、全ての設定が完了してブラウザを開くと"We're sorry, but something went wrong."というエラーと遭遇してしまいますので注意してください。

4. bin以下のフォルダの設定

アプリ内にbinというフォルダがあるのですが、その中のファイル全て(bundle,rails,rake,setup,spring, update,yarn)の一番上に

#!/usr/bin/env ruby

という記述があると思います。ここの記述が、

#!/usr/bin/env ruby 2.3.1

などバージョン指定されてしまっているファイル全てのバージョンを削除してください。つまり、上の記述から

#!/usr/bin/env ruby

このように数字を削除しましょう。(今回は2.3.1の部分)

ここまででrailsアプリ側の設定は終了です。
Github Desktop上でmasterにpushしておきましょう。
ここからはターミナルを主に操作していきます。

5. herokuの登録・設定

AWSだとRailsアプリはデプロイが難しいことが多いのですが、このherokuを使うと比較的簡単にデプロイすることができます。
機能的には物足りない部分もあるのですが、その場合は有料版を使えば問題なくサービスを立ち上げることができます。就活用にポートフォリオを管理している場合、アプリ起動時間は重要だと思うので、あまりにも遅い場合はその期間だけでも、有料版を使うようにしたほうがいいと思います。

herokuに登録

会員登録が完了したら、今度はherokuの機能を自分のPCに紐付けましょう。cliをダウンロードします。
https://devcenter.heroku.com/articles/heroku-cli

ターミナル
$curl https://cli-assets.heroku.com/install-ubuntu.sh | sh

$echo 'PATH="/usr/local/heroku/bin:$PATH"' >> ~/.profile

# インストールしたherokuのバージョンが確認できたら成功です
$heroku --version
>>heroku/7.0.47 darwin-x64 node-v10.1.0

PCからherokuにログイン

ターミナル
$heroku login

herokuに登録したemailとpasswordの入力

ターミナル
Enter your Heroku credentials:
Email: ~~~~~@example.com  (herokuに登録したアドレス打ってください)
Password: **********  (herokuに登録したパスワード打ってください)
Logged in as ~~~~~@example.com

無事、ログインしたら、以下のコマンドを打ってください。
このコマンドはherokuとアプリを紐づけるコマンドなので、最初の一回だけで大丈夫です。

ターミナル
$heroku create 任意の名前 (任意の名前に入力する文字列はアプリのurlになるものです記号は使用不可)

「Name is already taken...」というエラーが表示されたら、残念ながらそのアプリ名は他の方にurlを使われているので名前を変更してもう一度createしてみましょう。

herokuのDB設定

heroku上でMysqlを使うにはクレジットカードの登録が必要です。
といっても無料プランでの登録なので料金はかかりませんので安心してください。
クレジットカードの登録はherokuのアカウントから行います。

https://signup.heroku.com/login

カードを登録したら、下記のコマンドを入力します。

ターミナル
$ heroku addons:create cleardb:ignite

もしカード登録前に上のコマンドをうった場合はこのようなエラーが返って来ます。

ターミナル
    Please verify your account to install this add-on plan (please enter a credit card) For more
    information, see https://devcenter.heroku.com/categories/billing Verify now at
    https://heroku.com/verify

Herokuアプリの環境変数設定

以下のコマンドで、ClearDBのURLが確認できます。

ターミナル
$ heroku config
=== <アプリの名前> Config Vars
CLEARDB_DATABASE_URL: mysql://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true

上記コマンドで表示されたそれぞれの値を変数に設定しましょう。

ターミナル
$ config:add DB_NAME='<データベース名>'

$ heroku config:add DB_USERNAME='<ユーザー名>'

$ heroku config:add DB_PASSWORD='<パスワード>'

$ heroku config:add DB_HOSTNAME='<ホスト名>'

$ heroku config:add DB_PORT='3306'

$ heroku config:add DATABASE_URL='mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true'

最後のDATABASE_URLは先程のGemfileで'mysql2'をインストールしているので、mysql2://で始める必要があります。
設定が終わったら以下のコマンドで確認しましょう。

ターミナル
$ heroku config

# 実行結果
=== <アプリの名前> Config Vars
CLEARDB_DATABASE_URL: mysql://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true
DATABASE_URL:         mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true
DB_HOSTNAME:          <ホスト名>
DB_NAME:              <データベース名>
DB_PASSWORD:          <パスワード>
DB_PORT:              3306
DB_USERNAME:          <ユーザー名>

ここまでくればもう少しです!

Herokuへデプロイ

ローカルリポジトリをHerokuへpushします。

ターミナル
$ git push heroku master

次に本番環境にマイグレーションします。

ターミナル
$ heroku rub rails db:migrate

この時、Rubyのバージョンによっては、RubyのバージョンとHerokuのStack(HerokuのOSイメージ)のバージョンが合わずにエラーが出ることがあります。(2.6.5ではエラー出ました)

なのでRubyのバージョンを新しくするか、以下のコマンドでstackのバージョンをHeroku-16からHeroku-18に変更する必要があります。(2021年2月現在は18がデフォルトのようです。)
(Heroku公式ドキュメント)

ターミナル
$ heroku stack:set heroku-18 # herokuのバージョン変更

エラーが出たらもう一度、$ heroku rub rails db:migrateしてみましょう。

以下のコマンドを実行すると、ブラウザでアプリケーションが開きます。

ターミナル
$ heroku open

これでブラウザに表示されるはずです!

6. おまけ

アプリケーションの情報を確認したいときのコマンド

ターミナル
$ heroku apps:info

ここで出てくるWeb URLが完成したURLがブラウザに打ち込むURLです。

herokuでデプロイしたDBにSequelProで接続する

heroku configのコマンドで接続情報が出てくるので確認

ターミナル
$ heroku config

# 実行結果
=== <アプリの名前> Config Vars
CLEARDB_DATABASE_URL: mysql://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true
DATABASE_URL:         mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true
DB_HOSTNAME:          <ホスト名>
DB_NAME:              <データベース名>
DB_PASSWORD:          <パスワード>
DB_PORT:              3306
DB_USERNAME:          <ユーザー名>

上の情報を入力しましょう。
スクリーンショット 2021-02-21 16.10.56.png

・ポートは空欄でも大丈夫なようですが、一応3306を入力
・標準を選択

終わりに

最後まで読んでいただきありがとうございました!
お役に立てれば幸いです。。

参考記事

【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】
Herokuへのデプロイ方法【Heroku+Rails+MySQL】
mysqlを使ったRailsアプリをHerokuにデプロイする流れ
Rails4 + MySQL のアプリケーションを Heroku で動かすまで
Heroku で 'DATABASE_URL' を変更する方法
herokuでデプロイしたDBにSequelProで接続してみる

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

JavaScriptで画像プレビュー機能を実装するシンプルなコード

Ruby on RailsのアプリでJavaScriptを使ってプレビュー機能を実装するコードを解説します。

こんな感じで画像ファイルを選択すると、プレビューが表示される機能を目指します。
Image from Gyazo

実行環境

Rails 6.0.3.1
macOS Catalina バージョン10.15.7

ビューのコード

ビューはきわめてシンプルです。

app/views/messages/_form.html.erb
<%= form_with model: @message, id: 'new_message', local: true do |f| %>
  <%= f.text_field :content, placeholder: 'type a message' %>
  <%= f.file_field :image %>
  <%= f.submit '送信' %>
  <div id="image-list"></div><%# プレビューを表示する部分 %>
<% end %>
<%# ↓投稿編集ページ用の画像表示箇所↓%>
<%= image_tag @message.image, id: 'image' if @message.image.present? %>

JavaScriptのコード

コードは以下の通りです。流れをシンプルにして説明すると、

  • 画像ファイルが選択されるとその画像ファイルに対してURLが生成
  • 生成したimg要素のsrc属性にそのURLをセット
  • プレビュー表示用のdiv要素の中に子要素のdiv要素とimg要素を追加する

この流れでプレビュー画像を表示します。

app/javascript/packs/preview.js
// プレビュー表示機能は新規投稿("/new/")か投稿編集("/edit/")ページでのみ有効にする
if (document.URL.match( /new/ ) || document.URL.match( /edit/ )) {
  document.addEventListener('DOMContentLoaded', function(){
    // プレビューを表示するための要素を取得
    const ImageList = document.getElementById('image-list');

    const createImageHTML = (blob) => {
      // 画像を表示するためのdiv要素を生成
      const imageElement = document.createElement('div');
      // 表示する画像を生成
      const blobImage = document.createElement('img');
      // img要素のsrc属性の値をセット
      blobImage.setAttribute('src', blob);

      // 生成したHTMLの要素をブラウザに表示させる
      imageElement.appendChild(blobImage);
      ImageList.appendChild(imageElement);
    };

    document.getElementById('message_image').addEventListener('change', function(e){
      // 画像が表示されている場合のみ、すでに存在している画像を削除する(編集ページ用)
      const imageContent = document.querySelector('img');
      if (imageContent){
        imageContent.remove();
      }

      // 発火したイベントeの中の、targetの中の、filesという配列に格納された画像を変数に代入
      const file = e.target.files[0];
      // 画像のURLを生成
      const blob = window.URL.createObjectURL(file);

      createImageHTML(blob);
    });
  });
}

ビューファイルとJavaScriptのファイルを見比べやすいように横並びにした画像を用意しました。
Image from Gyazo

最後に、application.jsに以下の記述を追記して、turbolinksはコメントアウト(または削除)し、preview.jsを読み込むことを忘れないようにしましょう。

app/javascript/packs/application.js
require("@rails/ujs").start()
// require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
require("./preview") //このコードを追記
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby 3.0.0 から 2.7.2 にダウングレードさせる。

エラー

Unicorn が起動できない問題にぶつかった。

[User@ip-address app_name]$ bundle exec unicorn_rails -c /var/www/rails/app_name/config/unicorn.conf.rb -D -E production
master failed to start, check stderr log for details

ヒントになったサイト

https://teratail.com/questions/323548

解決した方法
Rubyのバージョンを2.7.2、unicornのバージョンを5.4.1にダウングレードしたら動きました。
どうやらRubyの3.0.0にunicornが対応していないらしいです。

[User@ip-address app_name]$ cd log
[User@ip-address log]$ tail unicorn.log
I, [2021-02-21T04:10:46.311290 #15000]  INFO -- : Refreshing Gem list
/home/User/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/unicorn-5.8.0/lib/unicorn.rb:80: [BUG] Segmentation fault at 0x0000000000000001
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux]

-- Control frame information -----------------------------------------------
I, [2021-02-21T04:14:21.956504 #15066]  INFO -- : Refreshing Gem list
/home/User/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/unicorn-5.8.0/lib/unicorn.rb:86: [BUG] Segmentation fault at 0x0000000000000000
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux]

-- Control frame information -----------------------------------------------

エラーログを見てみると、確かに 3.0.0 が邪魔をしている様子。

バージョン確認

[User@ip-address app_name]$ ruby -v
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux]
[User@ip-address log]$ unicorn -version
unicorn v5.8.0

Gemファイル編集

[User@ip-address app_name]$ vim Gemfile #rubyを2.7.2に、unicornを5.4.1に書き換える。

bundle install

[User@ip-address app_name]$ bundle install
Your Ruby version is 3.0.0, but your Gemfile specified 2.7.2

おや、エラーだ。Gemfileを書き換えただけではダメみたい。

rbenvでインストールされているバージョン確認する

[User@ip-address app_name]$ rbenv versions
* 3.0.0 (set by /var/www/rails/app_name/.ruby-version) 

rbenv で ruby 2.7.2 をダウンロードする。

[User@ip-address rails]$ rbenv install 2.7.2
Downloading ruby-2.7.2.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.2.tar.bz2
Installing ruby-2.7.2...
Installed ruby-2.7.2 to /home/User/.rbenv/versions/2.7.2

再度rbenvでバージョン確認する

[User@ip-address app_name]$ rbenv versions
  2.7.2
* 3.0.0 (set by /var/www/rails/app_name/.ruby-version)

##rbenvで2.7.2に置き換える。
[User@ip-address rails]$ rbenv global 2.7.2
[User@ip-address app_name]$ rbenv local 2.7.2
[User@ip-address app_name]$ rbenv versions
* 2.7.2 (set by /var/www/rails/app_name/.ruby-version)
  3.0.0

bundle install

[User@ip-address app_name]$ rbenv rehash
[User@ip-address app_name]$ bundle install
Fetching gem metadata from https://rubygems.org/............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Fetching rake 13.0.3
Installing rake 13.0.3
Fetching concurrent-ruby 1.1.8
Installing concurrent-ruby 1.1.8
Fetching i18n 1.8.9
Installing i18n 1.8.9

通った。

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

Mysql2::Error::ConnectionErrorについて

現在プログラミングスクールにてフリマアプリを作成しております。
モデル単体テストコードを実施した際に遭遇した下記エラー。苦戦したので備忘録として。

「Mysql2::Error::ConnectionError: Lost connection to MySQL server during query」

63e0767931af109eefa7956d9696b158.png

上図のように、テストの途中でエラーが発生してるんですね.
でもこれ、コード的には特に問題ないと思うのです。それゆえに悩みました。
また、何度かテストを実施すると、図よりテスト成功ログが増えたりもしまして、これはおかしいと思いました。

エラー内容的には「Mysqlへ繋がらなかったよ。繋ぐまで時間がかかりすぎてタイムアウトしちゃったよ」
という意味合いのようですね。他の方の記事をみてみたところ、「Mysqlの処理量がテスト途中でいっぱいいっぱいになってしまって、時間までに処理できませんでした」ということのようですね

今回行っているモデルの単体テストコードですが、単体ではあるのですが、そのテストには複数モデルが必要だったのです。下記のように、item_orderだけではなく、それに付随するitem_id、user_idも必要なため、それぞれFactoryBotを追加しました。(これを追加するまではテスト通ってたのです)

qiita.rb
require 'rails_helper'

describe ItemOrder do
  before do
    user = FactoryBot.create(:user)
    item = FactoryBot.create(:item)
    @item_order = FactoryBot.build(:item_order, item_id: item.id, user_id: user.id)
  end

  describe '商品購入手続き' do
    context '商品購入が成功する時' do
      it '全ての項目が入力されていれば登録できる' do
        expect(@item_order).to be_valid
      end

つまり、明らかにこれが原因です。itemとuserから引っ張ってくるデータ量が多すぎたと思われます。
テスト途中にbinding.pryを差し込みまくればテストが通った、という記事も拝見しましたが、私はそれでもテスト成功しませんでした。

そこで登場するのが、sleepメソッドです。
sleepメソッドは、処理速度を指定時間に合わせて行ってくれます。
今回は元々の指定時間に合わせて行ったところ、それに間に合わず、途中で失敗してしまっている、と考えられます。
そこで下図のようにsleepメソッドを挿入してみました。「一つの処理に1秒かける」、という設定ですね。

sleep.rb
describe ItemOrder do
  before do
    user = FactoryBot.create(:user)
    item = FactoryBot.create(:item)
    @item_order = FactoryBot.build(:item_order, item_id: item.id, user_id: user.id)
    sleep(1)
  end

すると・・・・
b269dbf0d4d12c209d6d4f8f5b5a328a.png

成功しました!!!
今回の厄介な所は、コード自体に誤りはなかった、という所ですね。
コードに何か誤りがあるせいでエラーが発生してしまう、と考えていた私にとっては衝撃な点でした。
そもそもテスト中に処理落ちしないテストコードを書ければいい話なのかもしれませんが・・・・それは今後精進していきます!
改善点やダメ出しあれば是非お願いします!

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

初心者が AtCoderコンテスト で失敗した!

結論

結論から先に述べさせていただきますと、

初心者にとって
問題を正確に素早く理解する力が一番重要。

背景/環境/結果

・AtCoderコンテスト(ABCクラス)に初挑戦。
・使用言語は Ruby (既存のメソッドが多くてABCクラスでは便利かなと)。
・VSCode でコードを書いて、提出用フォームにコピペする方法。
・A ~ F の問題がある中で、A,B,C提出。
・D問題で読解ミスをして、そのまま終了。

読解力/理解力

X を n 進法表記の数とみなして得られる値

正しい理解

X = 22 としたとき、

「22」 を 3進法表記の数とみなすのであれば「8」となります。

4進法表記の数とみなすのであれば「10」となります。

10進法表記の数とみなすのであれば「22」となります。

文章通りに数値を当てはめれば何も難しくありません。

間違った理解

ですが、私は30秒かけて頭に入れた結果、
「X を n 進法表記にして得られる値」と勘違いしました。

X = 22 としたとき

「22」を 3進法表記にすると「211」となります。

4進法表記にすると「112」となります。

10進法表記にすると「22」となります。

想定される値とは異なる値が出てきます。

反省点

参加する前は、「どのように問題を解くかの発想力」 や 「どのようにコードを書いていくかの知識力」 が重要なのだと思っていました。上位の方々はここの部分を競い合っているのだと思います。

ただ私のような初心者は、まず土台を固める必要があると知りました。
その土台とはずばり文章理解力です。さらに細かく分けると、
・問題を正しく理解する力
・問題を素早く理解する力
です。

ここの基礎がある上で、「発想力」や「知識力」を活かすことができると反省しました。

次はもう少しうまくやりたいです。

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

AtCoderコンテスト反省会 初心者の部

いきなりですが30秒で答えてください

X を n 進法表記の数とみなして得られる値を A とします。
X = 22、 n = 3 のときの A を求めてください。

結論

Aは「211」になりましたか?
それとも「8」になりましたか?
(上記以外になった人はもう一度解きなおしてみてください。)

結論から先に述べさせていただきます。
初心者にとって、問題を正確に素早く理解する力が一番重要だと感じました。

背景/環境/結果

・AtCoderコンテスト(ABCクラス)に初挑戦。
・使用言語は Ruby (既存のメソッドが多くてABCクラスでは便利かなと)。
・VSCode でコードを書いて、提出用フォームにコピペする方法。
・A ~ F の問題がある中で、A,B,C提出。
・D問題で読解ミスをして、そのまま終了。

読解力/理解力

X を n 進法表記の数とみなして得られる値

正しい理解

X = 22 としたとき、

「22」 を 3進法表記の数とみなすのであれば「8」となります。

4進法表記の数とみなすのであれば「10」となります。

10進法表記の数とみなすのであれば「22」となります。

文章通りに数値を当てはめれば何も難しくありません。

間違った理解

ですが、私は30秒かけて頭に入れた結果、
「X を n 進法表記にして得られる値」と勘違いしました。

X = 22 としたとき

「22」を 3進法表記にすると「211」となります。

4進法表記にすると「112」となります。

10進法表記にすると「22」となります。

想定される値とは異なる値が出てきます。

反省点

参加する前は、「どのように問題を解くかの発想力」 や 「どのようにコードを書いていくかの知識力」 が重要なのだと思っていました。中上位の方々はここの部分を競い合っているのだと思います。

ただ私のような初心者は、問題理解力がマストです。
さらに細かく分けると、
・問題を正しく理解する力
・問題を素早く理解する力
です。

ここの基礎がある上で、「発想力」や「知識力」を活かすことができると反省しました。

次はもう少しうまくやりたいです。

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

Rails6.1ハンズオン(2)~フロント寄りのこと編~

はじめに

Rails6.1ハンズオン(1)の続きです。

環境は前回を参考にしてください。

コードはGithubにあげています。章ごとにコミットしてますので、参考にしていただければ幸いです。

やること

  • erbからhamlにする
  • Bootstrap 5.0.0.beta2(2020/2/XX時点)を入れる
  • Font Awesomeを入れる
  • i18nを使って日本語化
  • 見た目をいい感じに整える

実装

3-1. erbからhamlにする

Gemfileに

gem 'haml-rails'

を追加。bundle install。

rails haml:erb2haml

で、プロジェクト内のerbをhamlに一斉変換できる。(最後に元のerbを消していいか聞かれるのでyを入力)

3-2. Bootstrap 5.0.0.beta2を入れる

参考:

Use Bootstrap 5 with Ruby on Rails 6 and webpack step by step

ターミナルで以下のコマンドを入力

yarn add bootstrap@next
yarn add @popperjs/core

app/views/layouts/application.html.hamlのstylesheet_link_tagの行を以下に置換

= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'

app/javascript/stylesheets/application.scssを作成

@import "bootstrap";

を追加。

app/javascript/packs/application.js

import "bootstrap"
import "../stylesheets/application"

を追加。

3-3. Font Awesomeを入れる

yarn add @fortawesome/fontawesome-free

app/javascript/stylesheets/application.scss

@import '~@fortawesome/fontawesome-free/scss/fontawesome';

を追加。

app/javascript/packs/application.js

import '@fortawesome/fontawesome-free/js/all'

を追加

3-4. 日本語化

参考:

[初学者]Railsのi18nによる日本語化対応 - Qiita

config/application.rbに以下を追加

config.time_zone = 'Tokyo'
config.i18n.default_locale = :ja
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]

config/locales/model.ja.ymlを作成

ja:
  activerecord:
    models:
      community: コミュニティ 
      comment: コメント
    attributes:
        community:
          id: ID
          title: タイトル
          owner_name: 作成者
        comment:
          id: ID
          author_name: 投稿者
          content: 内容
  attributes:
    created_at: 作成日
    updated_at: 更新日

svenfuchs/rails-i18n

↑ここからconfig/locales/ja.ymlを作成

locales/views/以下にもyamlファイルを作りますが、詳しくはgithubにて...

3-5. 見た目を色々整える

githubを見てください

こんな感じになりました↓
localhost_3000_communities.png

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

Rails6.1ハンズオン(2)~見た目をそれっぽくする

はじめに

Rails6.1ハンズオン(1)の続きです。

環境はこれを参考にしてください。

コードはGithubにあげています。章ごとにコミットしてますので、参考にしていただければ幸いです。

やること

  • erbからhamlにする
  • Bootstrap 5.0.0.beta2(2020/2/XX時点)を入れる
  • Font Awesomeを入れる
  • i18nを使って日本語化
  • 見た目をいい感じに整える

実装

3-1. erbからhamlにする

Gemfileに

gem 'haml-rails'

を追加。bundle install。

rails haml:erb2haml

で、プロジェクト内のerbをhamlに一斉変換できる。(最後に元のerbを消していいか聞かれるのでyを入力)

3-2. Bootstrap 5.0.0.beta2を入れる

参考:

Use Bootstrap 5 with Ruby on Rails 6 and webpack step by step

ターミナルで以下のコマンドを入力

yarn add bootstrap@next
yarn add @popperjs/core

app/views/layouts/application.html.hamlのstylesheet_link_tagの行を以下に置換

= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'

app/javascript/stylesheets/application.scssを作成

@import "bootstrap";

を追加。

app/javascript/packs/application.js

import "bootstrap"
import "../stylesheets/application"

を追加。

3-3. Font Awesomeを入れる

yarn add @fortawesome/fontawesome-free

app/javascript/stylesheets/application.scss

@import '~@fortawesome/fontawesome-free/scss/fontawesome';

を追加。

app/javascript/packs/application.js

import '@fortawesome/fontawesome-free/js/all'

を追加

3-4. 日本語化

参考:

[初学者]Railsのi18nによる日本語化対応 - Qiita

config/application.rbに以下を追加

config.time_zone = 'Tokyo'
config.i18n.default_locale = :ja
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]

config/locales/model.ja.ymlを作成

ja:
  activerecord:
    models:
      community: コミュニティ 
      comment: コメント
    attributes:
        community:
          id: ID
          title: タイトル
          owner_name: 作成者
        comment:
          id: ID
          author_name: 投稿者
          content: 内容
  attributes:
    created_at: 作成日
    updated_at: 更新日

svenfuchs/rails-i18n

↑ここからconfig/locales/ja.ymlを作成

locales/views/以下にもyamlファイルを作りますが、詳しくはgithubにて...

3-5. 見た目を色々整える

githubを見てください

こんな感じになりました↓
localhost_3000_communities.png

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

【Rails】業務未経験者がRailsを使用して情報資産共有アプリを開発したので共有します

facebook_cover_photo_1.png

はじめに

こんにちは!2020年10月よりRubyやRailsを中心に学習しているyutaro(@yutaro48)と申します。
このたび、Railsを使用して情報資産共有アプリ「KnowHow」を開発しましたので紹介させていただきます。

アプリ概要

情報資産共有アプリ「KnowHow」を作成しました。
top-page.gif

このアプリでできることは以下の3点です!


① ノウハウをアウトプットする

誰でも簡単にアウトプットできます。Markdownにも対応しています。下書きとして一時保存することもできます。
first.gif

② ノウハウを検索する

自分が求めるノウハウを検索することができます。ブックマークをして他の人のノウハウを自分のものにできます。
閲覧履歴表示やカテゴリー表示も実装しています。
third.gif

③ 気になる人をフォローする

気になる人をフォローすることができます。フォローした人のノウハウはタイムラインに表示されます。
second.gif

作成背景

「新卒社員 × 在宅勤務」によって顕在化した課題を解決するために作成

僕は2020年4月に某総合電機メーカーに新卒入社しました。
しかし、コロナウイルスの大流行により入社直後から在宅勤務になり、緊急事態宣言明けも在宅勤務が基本となりました。(緊急事態宣言後も用事がある時以外は基本在宅勤務という形態)
このように「新卒入社 × 在宅勤務」という特殊な状況下で私や同期は以下の3つの課題に直面しました。

課題1 会社ノウハウを学ぶ(社員の経験から学ぶ)機会の減少

会社独自のノウハウは属人的であり、先輩社員や同期との雑談などから学ぶことが多いが、リモート環境では雑談などから学ぶ機会がほとんどなくなってしまう。
そのため、学べることが限定的になってしまうという課題がある。

課題2 自身の学びをアウトプットする機会やレビューしていただく機会の減少

リモート環境では、自身の学んだことや経験したことを言語化したり、
他の社員の方からレビューしていただく機会が少なくなってしまう。
そのため、PDCAを回しづらい状況になってしまうという課題がある。

課題3 同期社員と情報共有する機会の減少

新卒社員の中では、同期が現在どのような仕事をしているか気になっている人が多い。通常であれば雑談や飲み会などの機会に情報共有をすることが多いと思う。
しかし、コロナ禍という状況下ではそのような機会が持てなくなってしまっているという課題がある。


これらの課題を解決したいと思い、情報資産共有アプリ「KnowHow」の作成に至りました。

機能一覧

上記の課題を解決するために実装した機能は以下の通りです。

基本機能
機能 Gem
ログイン機能 devise
ゲストログイン機能
プロフィール機能
課題1(会社ノウハウを学ぶ機会の減少)を解決するための機能
機能 Gem
検索機能
ブックマーク機能(Ajax)
カテゴリー別表示機能
閲覧履歴表示機能
ページネーション機能 kaminari

検索機能を実装し、自分が知りたいノウハウを検索できるようにしました。
カテゴリー別表示機能、閲覧履歴表示機能やページネーション機能を実装し、検索しやすいようにしました。
またブックマークをすることで他者のノウハウを自分のものにできます。
これらにより、様々な社員の経験から多くを学ぶことができるようになります。

課題2(自身の学びをアウトプットする機会やレビューしていただく機会の減少)を解決するための機能
機能 Gem
ノウハウのアウトプット機能(CRUD)
コメント機能(CRUD)
下書き保存機能
マークダウン投稿機能

ノウハウのアウトプット機能を実装し、自身の学びや経験をアウトプットできるようにしました。また、MarkDown投稿機能や下書き機能を実装し、アウトプットの体験価値向上を狙っています。
また、コメントで他の社員からレビューしていただくことも可能です。
これらにより、自身の経験についてPDCAを回すことができるようになります。

課題3(同期社員がどのようなことをしているのか情報共有する機会の減少)を解決するための機能
機能 Gem
フォロー機能
タイムライン表示機能

同期などが現在どのような仕事をしているかを確認するためにフォロー機能を実装しました。フォローした人のノウハウはタイムラインに表示されます。
これらにより、同期や気になる社員の動向を確認することができるようになります。

使用技術と選定理由

使用技術
  • 開発環境
    • macOS Catalina(10.15.6)
    • Visual Studio Code
  • フロントエンド
    • Haml/Sass/JavaScript(jQuery)
    • Semantic UI
    • Bootstrap
  • バックエンド
    • ruby 2.6.5p114
    • Rails 6.0.3.4
  • インフラ
    • PostgreSQL(13.0)
    • Heroku
選定理由

バックエンドにRailsを採用した理由は2つあります。

1つ目は、Qiita、note、GitHubなどプログラミング学習の際の知識のインプットやアウトプットに使用したサービスにRailsが使用されていたからです。
今回作成した「KnowHow」は社内ノウハウのインプットやアウトプットをテーマにしたアプリであるため、開発前段階からこれらのアプリの特徴を取り入れようと考えていました。
そのため、バックエンドはRailsで作成することに決めました。

2つ目は、様々な情報にアクセスしやすいからです。
PHP,Pythonなど他のプログラミング言語も検討しましたが、日本国内のコミュニティや参考資料が多く学習しやすいRubyとフレームワークRailsを選択しました。
Rubyの学習によって得た知見を活かし、他の言語も学んでいきたいと思っております。

データベース設計

draw.ioを使用して作成
KnowHow.png

インフラ構成図

(AWSにデプロイでき次第掲載いたします)

工夫した点

(設計面)

  • 実際に同期にヒアリングを行い課題設定を行なったこと
  • ユーザーのストレスがないようにUI/UXをとにかくシンプルにしたこと
  • 「誠実さ」を表現するためにベースカラーを青に設定したこと

(可読性向上)

  • 部分テンプレート化
  • デコレータを使用し、Modelの肥大化を回避
  • 基本7アクションしか使わない(チームメンバーが理解しやすいように)

(保守性向上)

  • 部分テンプレート化
  • 「namespace」や「scope」を使用したルーティングの整備

(その他)

  • 「チーム開発」を意識して、毎回ブランチを作りプルリクベースで開発
  • commitを細かく行ったこと

苦労した点

検索機能の実装(ActionTextを使用した場合)

検索機能はransack(gem)を使用すれば簡単に実装できますが、Rails6で導入されたActionTextを使用しているとransackによる検索が機能しなかったため、SQLを使用して自作で検索機能を作成しました。
とても苦労したと同時に「gemに頼らない機能実装」はとても良い経験だったので、別途記事にまとめました。
また、以前会社で学んだSQLの復習にもなりました!

今後やりたいこと

  • AWSにデプロイ
  • セキュリティの強化
  • テストのサンプル数を増やす
  • レスポンシブ対応
  • Vue.jsの導入
  • Docker / CircleCIの導入

感想

私は、前職在職時に自分自身や同期が本気で悩んでいた課題をどうにか解決できないかと思い、本アプリの開発を決めました。
しかし、自分で考えたWebアプリを形にするということは想像以上に大変でした。
「この処理はどこに書けばいいのか?」「この機能は本当に必要なのか?」「このUI/UXで使いやすいのか?」と常に自問自答を繰り返しながら開発していました。
ですが、開発を振り返ってみると大変だった記憶ではなく、楽しかった記憶や充実感ばかりが思い出されます。
なにより、エラーに直面しながらも自分自身のやりたいことを形にできた時の感動は一入でした。

このアプリ自体はこれで完成ではなく、インフラ面や機能面など課題がてんこ盛りなので、これからも改善を重ねていきたいと思っております!

最後までご覧いただきありがとうございました?‍♂️
これからも自身の学習したことや気づいたことなどあったらアウトプットしていきますので、もしよろしければLGTMフォローよろしくお願いいたします!

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

【Vision API(LABEL_DETECTION)】seeds.rbにも反映させる

はじめに

今回は、先日初めて実装したGoogle Vision APIの機能について自身の備忘録として残しておきたいと思います。
本記事では、Google Cloud Platformの登録につきましては省略させて頂いておりますので、ご了承ください。

Google Vision APIとは

Google社が提供している、画像解析ツールのことを指します。
画像の中に写りこんでいる文字やロゴを認識したり、不適切な画像でないか判断してくれたりと、種類は様々あります。
その中で、今回私が取り組んだのは、LABEL_DETECTIONというもので、画像を認識し自動でラベル(タグ)を発行してくれるという機能になります。
こちらを活用し、写真付きの投稿記事に自動でタグが付くよう実装しております。

見本

「Food」「Cake decorating」「Cake」の茶色の部分が今回作成していく自動生成されるラベルになります。
※デフォルト検出が英語になっており、日本語にて検出する方法は現在模索中です。
※また、cssにてデザインは作成しておりますが、今回デザインにつきましてのご説明は記載しておりませんので、ご了承くださいますようお願い致します。

スクリーンショット 2021-02-21 0.26.54.png

今回使用するモデル

※必要最低限のカラムのみで構成しております。

モデル名 カラム名
User id
name
モデル名 カラム名
Tweet id
user_id
image_id
title
content
モデル名 カラム名
Tag tweet_id
name

※こちらのTagモデルが、LABEL_DETECTIONの機能で発行されるタグ用のモデル。
そして、nameのカラムの部分が発行されるタグ名に当たります。

config/initializers/refile.rb

上記のディレクトリの階層に、refile.rbというファイルを作成します。

config/initializers/refile.rb
Refile.backends['store'] = Refile::Backend::FileSystem.new('public/uploads/')


Gemfile

Google API keyの情報が必要になり、keyの値は個人情報になりますので、環境変数に置き換えて使用します。
そのため、以下のgemを追加しbundle installします。

Gemfile.
gem 'dotenv-rails'


.env

もし、.envファイルを今回初めて作成されるようであれば、ご自身のアプリケーション直下に作成してください。
また、GitHubにpushする際に公開されてしまいますと、個人情報の流出に繋がりますので、必ず.gitignoreファイルに「/.env」を追記しましょう。

.env
GOOGLE_API_KEY="ご自身のGOOGLE_API_KEYをコピペする"
.gitignore
/.env


lib/vision.rb

上記のディレクトリの階層に、vision.rbというファイルを作成します。
基本的には、こちらをコピペしていただければ動作するかと思います。

lib/vision.rb
require 'base64'
require 'json'
require 'net/https'

module Vision
  class << self
    def get_image_data(image_file)
      # APIのURL作成
      api_url = "https://vision.googleapis.com/v1/images:annotate?key=#{ENV['GOOGLE_API_KEY']}"

      # 画像をbase64にエンコード
      base64_image = Base64.encode64(open("#{Rails.root}/public/uploads/#{image_file.id}").read)

      # APIリクエスト用のJSONパラメータ
      params = {
        requests: [{
          image: {
            content: base64_image
          },
          features: [
            {
              type: 'LABEL_DETECTION'
            }
          ]
        }]
      }.to_json

      # Google Cloud Vision APIにリクエスト
      uri = URI.parse(api_url)
      https = Net::HTTP.new(uri.host, uri.port)
      https.use_ssl = true
      request = Net::HTTP::Post.new(uri.request_uri)
      request['Content-Type'] = 'application/json'
      response = https.request(request, params)
      response_body = JSON.parse(response.body)
      # APIレスポンス出力
      if (error = response_body['responses'][0]['error']).present?
        raise error['message']
      else
        # take(3)の部分が取り出すラベル数になります。
        # 必要に応じて変更してください。
        response_body['responses'][0]['labelAnnotations'].pluck('description').take(3)
      end
    end
  end
end


config/application.rb

先ほど作成したlib以下のファイルを読み込むために、以下の1行を追記してください。
config.paths.add 'lib', eager_load: true

config/application.rb
module (アプリケーション名)
  class Application < Rails::Application
    config.load_defaults 5.2
    # 追加
    config.paths.add 'lib', eager_load: true
  end
end


tweet_controller.rb

tweet_controller.rb
  def create
    tweet = Tweet.new(tweet_params)
    tweet.save
    # ラベルを作成するために追記
    tags = Vision.get_image_data(tweet.image)    
    tags.each do |tag|
      tweet.tags.create(name: tag)
    end
    redirect_to tweets_path


tweet/index.html.erb

最後にビューにラベルを取得するための記述を追加します。
※ 投稿した記事が保存される時にラベルは自動生成されるので、ビューで変更を加えるのはラベルを表示させたい画面のみになります。

tweet/index.html.erb
<% @tweets.each do |tweet| %>
 <% tweet.tags.each do |tag| %>
  # lib/vision.rbで指定した数分だけラベルを取得
  <%= tag.name %>
 <% end %>
 <%= tweet.title %>
 <%= tweet.user.name %>
<% end %>


今回一番書きたかったポイント

長々と書いてしまいましたが、一番記事として残しておきたかった部分はこちらからになります...
こちらまでお目通しいただけた方がおりましたら、長々とお付き合いいただきありがとうございます。

seeds.rbでデータを作成しても、このままでは反映されない!

実はデプロイ後を想定して、seeds.rbには以下のような形で、数十件分のtweetを記述していました。
しかし、seeds.rbでデータを作成する際は、コントローラーもアクションも呼び出されないため、ラベルは発行されません。

seeds.rb
# 一部抜粋
Tweet.create!(
 user_id: 1,
 title: "隣町のケーキ屋!",
 content:"おしゃれで可愛いケーキがいっぱい。",
 image: File.open('./app/assets/images/tiramisu.jpg')
)


解決方法

①まず、tweet.contoroller.rbに追記したラベル作成(4〜8行目)の部分を削除します。

②ラベル作成の記述をtweetモデルに記述します。
意味:
● after_create :create_tags
  tweetが生成された後にtagsを生成する

● def create_tags以降
  実際にラベルを生成する記述

(補足)
先ほどのコントローラーの記述では、vision_tagsの部分はtagsのみになっております。
変更している理由は以下の通りです。
開発環境では、全てtagsのみで問題なかったのですが、デプロイ後の本番環境でseeds.rbを読み込もうとすると、任意で命名してるtagsの部分と、テーブル名のtagsの部分(tags.create...)で判別がつかない事によりエラーの原因になってしまったようでした。
※現状、開発環境と本番環境での差異については詳しく分かっておりません。

models/tweet.rb
belongs_to :user
has_many :tags, dependent: :destroy
attachment :image

# コールバックを使用
after_create :create_tags

def create_tags
  # Vision APIのLABEL保存の記述
  vision_tags = Vision.get_image_data(self.image)
  vision_tags.each do |tag|
    tags.create(name: tag)
  end
end

● コールバック

何かのイベント発生時に毎回実行されるコードのことを指します。
「〜をする前に実行する」、「〜をした後に実行する」、これらを定義するものになります。

モデルとコントローラーどちらへの記述が良いのか

結論からいうと、モデルが良いそうです!
(メンターさんより教わった情報を参考にさせて頂いております。)
コントローラーはなるべくスマートに最低限にを意識し、モデルをうまく活用すると良いとご教授いただきました。

終わり

今回は以上になります。
最後の部分に繋げるべく、一から長々と執筆してしまいました。
大変長くなってしまい、読みづらい部分も多いかと思います。申し訳ございません。
私自身もプログラミング初心者ですが、同じ様な立場の方に少しでも参考になれば幸いです。
また、もし内容に誤りなどがございましたら、ご指摘いただけますと幸いです。

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

[Ruby on Rails] devise導入後にログアウトできなくなるエラー

誰でも簡単にユーザー管理機能が作れるGemの
deviseを導入した後のエラーの解決方法を
備忘録として書き留めておきます。

  

本来ならログアウトする際にpathの記入と
methodの記入だけでログアウトできるはずが、
エラーが立ち塞がった。

いつもは下記の記述ででエラーなんか起きたことなかったのに...

<%= link_to 'ログアウト', destroy_user_session_path, method: :delete, class: "logout" %>

  

エラー内容はスクショし忘れましたが、
翻訳に履歴が残っていました。

Could not find devise mapping for path "/users/sign_out". This may happen for two reasons: 1) You forgot to wrap your route inside the scope block. For example: devise_scope :user do get "/some/route" => "some_devise_controller" end 2) You are testing a Devise controller bypassing the router. If so, you can explicitly tell Devise which mapping to use: @request.env["devise.mapping"] = Devise.mappings[:user]
パス「/ users / sign_out」のデバイスマッピングが見つかりませんでした。 これは2つの理由で発生する可能性があります:1)スコープブロック内にルートをラップするのを忘れた。 例:devise_scope:user do get "/ some / route" => "some_devise_controller" end 2)ルーターをバイパスするDeviseコントローラーをテストしています。 その場合、使用するマッピングをDeviseに明示的に指示できます。@ request.env ["devise.mapping"] = Devise.mappings [:user]

正直なんのこっちゃわかりません。(泣)

  
とりあえず、

解決方法

config/initializers/devise.rb
config.sign_out_via = :get

大体269行目の記述。
ここは元々、deleteと書かれていたのを
getに直すことで、読み込みが上手く行くようになりました。

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