学習記録

アウトプット用に作りました

nil? blank? empty?の検証

nil? blank? empty?の使い分け

似てるけど全然違う、使い分けが曖昧だったのでまとめてみます。0、nil、""(空文字)、" "(空白)、{}(空ハッシュ)、[](空配列)を例にして検証してみます。

nil?

nilの場合のみtrueになります。
それ以外はfalseです。

pry(main)> 0.nil?
=> false
pry(main)> nil.nil?
=> true
pry(main)> "".nil?
=> false
pry(main)> " ".nil?
=> false
pry(main)> {}.nil?
=> false
pry(main)> [].nil?
=> false


empty?

空の文字列、配列、ハッシュの場合trueになります。
空白の時はfalseになります。
nil、0にはエラーが発生します。

pry(main)> 0.empty?
NoMethodError: undefined method `empty?' for 0:Integer
pry(main)> nil.empty?
NoMethodError: undefined method `empty?' for nil:NilClass
pry(main)> "".empty?
=> true
pry(main)> " ".empty?
=> false
pry(main)> {}.empty?
=> true
pry(main)> [].empty?
=> true


blank?

0の時はfalseになります。
その他は全てtrueになります。

 pry(main)> 0.blank?
=> false
pry(main)> nil.blank?
=> true
pry(main)> "".blank?
=> true
pry(main)> " ".blank?
=> true
pry(main)> {}.blank?
=> true
pry(main)> [].blank?
=> true

blank?の反対の使い方ができるpresent?というメソッドも紹介しておきます。

present?

!blank?と同じ意味です。

 pry(main)> 0.present?
=> true
pry(main)> nil.present?
=> false
pry(main)> "".present?
=> false
pry(main)> " ".present?
=> false
pry(main)> {}.present?
=> false
pry(main)> [].present?
=> false


nil? blank? empty?で0はどうなるのか

私は今回の実装でArticle.countで記事数が0ならという条件分岐を作ろう思っていました。しかし上記3つのメソッドを使おうとしてもinteger型の識別まではできませんでした。
最初はArticle.count == 0と実装していたのですが、他にも書き方としてzero?というメソッドもあるようです。
参考 : ruby: nil? empty? blank? どれも0をチェックしない - kinopyo blog

色々知識メモ

何時間後などの時間指定の方法

現在時から見て一時間後(未来)

DateTime.now.since(1.hour)

現在時から見て一時間前(過去)

DateTime.now.ago(1.hour)


content_tag

content_tagはビューでタグを記載するときに使います。セキュリティー面でとても良い書き方です。

content_tag(:i, nil, class: "fa fa-star")
=> <i class="fa fa-star"></i>

※ フォントアイコンを表示するときは、iタグで空要素を作って、class属性値にfaとアイコンごとのクラスをつけます。空文字だからnilを入れています。


改行させずにタグ同士を横並びに表示

この書き方だと星とハートのアイコンが縦に表示されます。

<i class="fa fa-star"></i>
<i class="fa fa-heart"></i>

これを横表記にしたいときは下記のようにdivタグで囲むように記載します。

<div>
  <i class="fa fa-star"></i>
  <i class="fa fa-heart"></i>
</div>

他にも横並びにすることができる書き方があります。

<i class="fa fa-star", style: 'display: inline'></i>
<i class="fa fa-heart", style: 'display: inline'></i>

参考 : 【CSS】displayの使い方を総まとめ!inlineやblockの違いは?

<div class="d-inline-flex">
  <i class="fa fa-star"></i>
  <i class="fa fa-heart"></i>
</div>

参考 : Bootstrap4に用意されているクラス【flex編】 | Webお役立ちネタ帳


I18n#lを使って時刻の表記をする

作成日時などの時刻表示を直感的にわかりやすくするために使います。

l(@tweet.created_at)
=> 2021/05/05 12:00:00 

デフォルトで出力すると上記のような表記になります。もし違う書き方にしたいときはconfig/locals/ja.ymlファイルに定義します。

ja:
  time:
    formats:
      long: "%Y年%m月%d日(%a) %H時%M分%S秒 %z"
      short: "%y/%m/%d %H:%M"
l(@tweet.created_at, format: :short)
=> 2021/05/05 12:00

このようにカスタマイズすることもできます。

参考 : あなたはいくつ知っている?Rails I18nの便利機能大全! - Qiita

Active Storageで複数枚の画像を添付する

Active storageについては以前まとめたのでこちらを参照してください。
今回は画像を複数枚同時にアップロードできるようにしていきます、またアップロードされている画像を個別に削除できるような実装もしていきます。

複数枚の画像を添付する

has_many_attachedを使用して、画像とモデルの間に1対多の関係を設定します。多数の画像をアタッチできるようになります。
たとえばアプリケーションにMessageモデルがあるとします。メッセージごとに多数の画像を持たせるようにMessageモデルを定義します。

message.rb

has_many_attached :images


ビューとコントローラを書き換えて、imagesを受け取れるように実装していきます。※ビューではsimple_formとslim記法を使っています。

messages/edit.html.slim

= f.input :images, as: :file, hint: 'JPEG/PNG (1200x400)', input_html: { multiple: true }

  - if @message.images.attached?     
     - @message.images.each do |image|
       = image_tag image.variant(resize: "100x100").processed

{ multiple: true }は複数枚画像投稿の場合に追記することによって、複数枚画像を選択することができます。
input_html: { multiple: true }と記載しているのはsimple_formを使用しているのでinput_html:は必要な記述のようです。
参考 : 【Rails5】carrierwaveとsimple_formで複数の画像をアップロードする | ニートエンジニア
if文でimagesが添付されたらプレビューが表示されるように記述しました。
variantで画像を使用したいサイズに加工しています。
processedは保存されたサイズと加工しようとしているサイズが同じなら自身のインスタンスを返すという意味です。

messages_controller.rb

class MessagesController < ApplicationController
  def create
    message = Message.create!(message_params)
    redirect_to edit_message_path
  end

  private
  
  def message_params
    params.require(:message).permit(:title, :content, images: [])
  end
end

images: []と書く理由は、複数枚の画像がアップロードされた時に送られてくるパラメーターは配列に格納されているため、以下のようにストロングパラメーターも配列形式にする必要があるからです。
参考 : 【Rails6.0】Active Storageを用いた単数枚、複数枚画像投稿の実装手順をそれぞれ解説|TechTechMedia

これで複数枚の画像の添付ができるようになりましたが、バリデーションをつけていないので想定外の画像のタイプやサイズが送られてくる可能性があります。次で画像のバリデーションについて説明していきます。


Active Storageのバリデーション

Active Storageにはデフォルトのバリデーションがないため、自分でバリデーション設定をしなくてはいけません。 下記のようにcontent_typeで画像のファイルタイプ、maximumで画像のファイルサイズを設定できるようにしておきます。

message.rb

validates :images, attachment: { content_type: %r{\Aimage/(png|jpeg)\Z}, maximum: 524_288_000 }

上記で使用した2つのバリデーションで使用するメソッドを定義していきます。ここで使うのが個別のカスタムバリデーターです。バリデーションのメソッドをattachment_validator.rbに記入していきます。※ここではmaximumメソッドを用いて説明していきます。

validators/attachment_validator.rb

class AttachmentValidator < ActiveModel::EachValidator
  include ActiveSupport::NumberHelper

  def validate_each(record, attribute, value)
    return if value.blank? || !value.attached?

    has_error = false

    if options[:maximum]
      if value.is_a?(ActiveStorage::Attached::Many)
        value.each do |value|
          unless validate_maximum(record, attribute, value)
            has_error = true
            break   # breakは繰り返し終了
          end
        end
      else
        has_error = true unless validate_maximum(record, attribute, value)
      end
    end
  end

  private

  def validate_maximum(record, attribute, value)
    if value.byte_size > options[:maximum]
      record.errors[attribute] << (options[:message] || "#{number_to_human_size(options[:maximum])}以下にしてください")
      false
    else
      true
    end
  end
end

value.is_a?(ActiveStorage::Attached::Many)は継承関係を遡ってどのクラスに属しているか調べています。一枚の画像を受け取るレコードの時にもこのメソッドを使えるように、条件分岐を使って定義しています。
validate_maximumメソッドではvaluebyte_sizeがmessageモデルで定義したバイト数を超えている場合はエラーメッセージが出るように定義しています。
参考 : ActiveStorageのバリデーション - Qiita


Active Storageの削除機能

最後にアップロードした画像を削除する方法について説明していきます。私はmessagesコントローラのdestroyアクションに画像削除の記述をしていたのですが、これだとmessageを削除する定義を実装することができなくなってしまいます。またmessagesコントローラに画像削除用に独自のアクション名を増やすこともできますが、RESTに基づく7つのactionのみのほうがわかりやすく管理しやすいです。なので新たにControllerを作成してdestroyアクションを追加する方法が一番良いです。

まず最初に画像を削除するためのコントローラを作成します。admin/commentsディレクトリ配下にattachmentsコントローラを作成します。

$ rails g controller admin::comment::attachments destroy

ここで作成されたビューファイル、ルーティングの記述などの不要なものは削除しておきます。

そしてルーティングはadminとcommentsのresourceにネストさせて定義すると関連が分かりやすくなります。

routes.rb

namespace :admin do
  resource :comments do
    resources :attachments, only: %i[destroy]
  end
end

しかしこのままだとルーティングエラーが出てしまします。どのようなルーティングになっているか確認すると、下記のようになっていました。
f:id:kimura34:20210505212956p:plain

attachmentsコントローラはadmin/commentsディレクトリの配下にあるので、admin/attachments#destroyではおかしいですよね。

namespace :admin do
  resource :comments do
    resources :attachments, only: %i[destroy], controller: 'comments/attachments'
  end
end

このようにコントローラを指定してあげるとadmin/commentsディレクトリを指定することができるのでエラーは解決しました。 f:id:kimura34:20210505213811p:plain 他にもresourcesにmodule: :commentsと記載する方法もあります。

次に作成したattachmentsコントローラのdestroyアクションに画像の削除をするための定義をしていきます。

class Admin::Comments::AttachmentsController < ApplicationController
  def destroy
    image = ActiveStorage::Attachment.find(params[:id])
    image.purge
    redirect_to root_path
  end
end

ActiveStorage::Attachment.find(params[:id])は全てのActive Storageで管理している画像ファイルのIDとパラメータで送られてきたIDの一致するものを探しています。
image.purgeで探し出した画像を削除しています。purgeはActive Storageで使う削除メソッドです。

最後に削除ボタンを実装していきます。

messages/edit.html.slim

= f.input :images, as: :file, hint: 'JPEG/PNG (1200x400)', input_html: { multiple: true }

  - if @message.images.attached?     
     - @message.images.each do |image|
       = image_tag image.variant(resize: "100x100").processed
          = link_to '削除', admin_comments_attachment_path(image.id), method: :delete, class: %w[btn btn-danger]

admin_comments_attachment_path(image.id)でパスを送るときに画像IDを指定しています。

Swiperを使ってスライダー機能を実装する

Swiperとは

画像をスライダー形式に表示する機能を実装するものです。公式サイトを見るとたくさんのスライダーのデザインの見本コードをあり、参考にすることで簡単に実装することができます。
参考 : Swiper Demos


Swiperを導入

まず最初にCSSとJSでSwiperを適用するための設定を行う必要があります。この設定を行うための方法として以下の3つがあります。

①Swiperの公式サイトのライブラリを使用する
②ファイルをダウンロードせずに使用する(CDN)
③npm、yarnのJSのパッケージマネージャを使用する

Swiper.jsの公式サイトではCDNとnpmを使用した実装の説明がされています。私はnpmで試したのですがnode_modules配下にswiperのファイルがなぜか作成されなかったので、yarnというJSのパッケージマネージャを使って実装していきました。


yarnを使ってSwiperを導入

homebrewにyarnをダウンロードしていなかったので、まず最初にyarnのダウンロードを行いました。

$ brew install yarn

次にyarnにswiperを導入して、インストールを行います。
(Gemfileに追加してbundle installしているのと同じイメージです)

$ yarn add swiper
$ yarn install 

これで必要なファイルがnode_modulesディレクトリ配下に作成されました。

次にapplication.css.scssとapplication.jsのマニフェストファイルにnode_modulesの読み込みたいswiperのファイルの名前を書きます。
ディレクトリのnode_modulesの部分は省略できるので、swiper/からファイルの名前を記入することができます。

assets/stylesheets/application.css.scss

@import 'swiper/swiper-bundle';
assets/stylesheets/application.js

//= require swiper/swiper-bundle.js

導入したnode_modules配下のファイルを読み込むようにするための設定をconfig/initializers/assets.rbに追記します。

Rails.application.config.assets.paths << Rails.root.join('node_modules')


Swiperを実装する

HTML、CSS、JSを使いたいスライダーのデモを真似して書いていきます。私は自動で画像が切り替わる設定をしたかったのでAutoPlayを適応しました。

HTML

HTMLファイルの構造は基本的に下記のように書きます。クラス名を変更したり、省略するとCSSやJSの設定ができなくなるので注意します。

<div class="swiper-container">
  <div class="swiper-wrapper">
    <!-- スライダーに表示したい内容 -->
    <div class="swiper-slide">Slide 1</div>
    <div class="swiper-slide">Slide 2</div>
    <div class="swiper-slide">Slide 3</div>
    ...
  </div>
  <!-- ページネーション(省略可) -->
  <div class="swiper-pagination"></div>

  <!-- ナビゲーションボタン(省略可) -->
  <div class="swiper-button-prev"></div>
  <div class="swiper-button-next"></div>

  <!-- スクロールバー(省略可) -->
  <div class="swiper-scrollbar"></div>
</div>

CSS

公式の書き方を参考にしつつ、適宜Swiperのデザインを調整していきます。

assets/stylesheets/application.css.scss

.swiper-container {
      img {
      width: 100%;
      height: 400px;
     }
}

.swiper-slide {
      text-align: center;
      font-size: 18px;
}

JS

assets/stylesheets/ディレクトリの配下に新しくJSファイルを自分で作成します。

$ touch assets/stylesheets/hoge.js

この作成したファイルに公式を参考にして書いていきます。

var swiper = new Swiper(".swiper-container", {
        spaceBetween: 30,
        centeredSlides: true,
        autoplay: {
          delay: 2500,
          disableOnInteraction: false,
        },
});

そしてこのJSを読み込みたいHTMLファイルに読み込む旨の表記を行います。スライダーを導入しているHTMLファイルです。

<%= javascript_include_tag "hoge.js"  %>

参考 : Railsで特定のページのみJavaScriptを読み込む方法を現役エンジニアが解説【初心者向け】 | TechAcademyマガジン

しかしこれだけではSwiperは機能しませんでした。JSの読み込む場所に問題があったようです。「自作JSの読み込みはbodyの最後に書いたほうがよくて、外部ライブラリはheadに書く方がいいという決まり事がある」ということなので少しコードを変えてみました。
もともとbodyに書いていたJSのjavascript_include_tagの記述を全部headタグに移すことでswiperを起動させることができました。
また自作のJSファイルであるhoge.jsの読み込みをスライダーを導入しているHTMLファイルに直接書いていましたが、application.html.erbファイルのbodyタグ内に記載しても動作に問題はありませんでした。
参考 : 自作JSの読み込みはbodyの最後に書いたほうがいいのに、なんで外部ライブラリはheadにあるんでしょうか? - Qiita

参考

Swiper - The Most Modern Mobile Touch Slider

RailsでSwiperを導入する方法(Swiperは2020年7月にバージョンアップし、従来と設定方法が変わりました!) - Qiita

swiperをyarnで導入して、画像をスライダー形式にする! - Qiita

課題10で参照したサイト

昨日などの日付の表し方

Rails で1日前とか1週間前とか - /var/www/yatta47.log

ActiveRecordで日付・時刻の範囲検索をシンプルに書く方法|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

ActionMailerのプレビュー

RailsのAction Mailer Previewsについて | 日々雑記

nil? present? blank?の違い

Railsでnil? blank? empty? present?を使いこなそう|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

ruby: nil? empty? blank? どれも0をチェックしない - kinopyo blog

ActionMailerをwheneverを使って実行する

Rails メール自動配信機能をActionMailerとwheneverを使用して実装する - Qiita

メールを送るための設定(deliver)

RailsのActionMailerでハマったところ - give IT a try

letter_openerを動かすための設定

Railsで、Action Mailerとletter_opener_webを初めて使いました - めるノート

モデルのポリモーフィック関連付け

ポリモーフィック関連付けを使うと、ある一つのモデルが他の複数のモデルに属していることを、一つの関連付けだけで表現することができます。


ポリモーフィック関連付け

市内の学校の情報を管理するアプリを想定します。

f:id:kimura34:20210429135918p:plain

まず最初に赤枠で囲った部分を説明していきます。
TeacherモデルとStudentモデルは両方ともSchoolモデルと関連しています。
Schoolモデルのschoolable_typeにはTeacher、Studentのクラス名が入ります。schoolable_idにはteacher、studentの各idが入ります。
これをポリモーフィック関連で実装していきます。ではモデルを定義していきましょう。

class School < ActiveRecord::Base
  belongs_to :schoolable, polymorphic: true, dependent: :destroy
end
class Teacher < ActiveRecord::Base
  has_one :school, as: :schoolable, dependent: :destroy
end
class Student < ActiveRecord::Base
  has_one :school, as: :schoolable, dependent: :destroy
end

Schoolモデルは、複数のモデルTeacher、Studentに属していることを1つの関連付けのschholableだけで表現できています。

@teacher.schoolとすればTeacherモデルからschoolの情報を取得できます。また@school.schoolableとするとTeacherかStudentの中の指定した誰かを取得することができます。
参考 : Active Record の関連付け - Railsガイド


中間テーブルとポリモーフィック関連を組み合わせる

今回の場合、CityモデルはSchoolモデルを中間テーブルとしてTeacherとStudentモデルと関連を持っています。 f:id:kimura34:20210429141130p:plain

この時のCityモデルの定義は下記のようになります。

city.rb

has_many :schools
has_many :teachers, through: :schools, source: :schoolable, source_type: 'Teacher'
has_many :students, through: :schools, source: :schoolable, source_type: 'Student'

through中間テーブルのモデルを指定しています。
source中間テーブルの先の関連モデルを指定しています。
source_typeポリモーフィック関連のソースのタイプ、つまりSchoolのschoolable _typeを指定しています。

またTeacher、StudentモデルでもCityモデルへの関連を定義します。

has_one :city, through: :schools

これにより@teacher.cityで先生のcity情報を取得できるし、@city.studentsでcityの生徒情報を扱うことができるようになります。
参考 : Active Record の関連付け - Railsガイド

APIを使わずにTwitterとYouTubeの埋め込み処理を行う

APIを使わずにTwitterYouTubeのURLを入力したら埋め込み用に変換されて、記事に反映することが今回の最終目標です。

準備

ユーザーが入力するメディアタイプとURLの値を保存するためのカラムを用意します。
identifierはURLを保存するためのカラムです。

insert.rb

enum insert_type: { youtube: 0, twitter: 1 }

validates :identifier, length: { maximum: 200 }


Twitterの埋め込み方法

タイムラインの埋め込みではなく、ツイートのURLを入力フォームに入力してツイートを表示する実装をしていきます。

公式Twitterに埋め込み用のURLを生成してくれるツールがあるのでそれを使用していきます。それでは公式のサイトを参考にして進めていきます。

ウェブサイトやブログにツイートを埋め込む方法

まず最初に何でもいいのでツイートを開いて下記の画像の赤丸のついているリンクをクリックします。 f:id:kimura34:20210427231646p:plain

するとこのような画面に移ります。 f:id:kimura34:20210427231836p:plain

生成されたコードをコピーすると下記のような埋め込み用のHTMLが生成されました。

<blockquote class="twitter-tweet">
<p lang="ja" dir="ltr">わあ!ちょうちょはどこへ飛んでいくんだろう?
<a href="https://t.co/BKmfg8wFy6">https://t.co/BKmfg8wFy6</a> 
<a href="https://t.co/YliyCv4LEQ">pic.twitter.com/YliyCv4LEQ</a>
</p>&mdash; ダッフィーといっしょ【公式】 (@WithDuffy_TDS) 
<a href="https://twitter.com/WithDuffy_TDS/status/1385420681635004417?ref_src=twsrc%5Etfw">April 23, 2021</a>
</blockquote> 
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

このHTMLの余分な情報の部分を削って、ツイートのURLの部分だけを抜き取ってみます。

<blockquote class="twitter-tweet">
<a href="https://twitter.com/WithDuffy_TDS/status/1385420681635004417?ref_src=twsrc%5Etfw"></a>
</blockquote> 
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

上記のコードをもとに、TwitterのURLを送るフォームから入力されたデータをinsert.identifierを使って出力されるように実装します。 ※ slim表記になっています。

.insert-twitter
  blockquote.twitter-tweet
    a href="#{insert.identifier}"
script async="" charset="utf-8" src="https://platform.twitter.com/widgets.js" 

参考 : 【Twitter】埋め込み処理をAPIに投げずにローカルで行う - mizuff_diary


Youtubeの埋め込み方法

YouTubeの埋め込みも公式のサイトをみて進めていきたいと思います。

動画と再生リストを埋め込む - YouTube ヘルプ

動画を検索して共有ページを開いて、埋め込むを選択します。 f:id:kimura34:20210427235443p:plain

すると下記のような埋め込み用のHTMLが生成されるのでこれを編集していきます。 f:id:kimura34:20210427235754p:plain

<iframe width="560" height="315" 
src="https://www.youtube.com/embed/yOAwvRmVIyo" 
title="YouTube video player" frameborder="0" 
allow="accelerometer; autoplay; clipboard-write; 
encrypted-media; gyroscope; picture-in-picture" 
allowfullscreen></iframe>

これを見ると埋め込みURLの部分の記述は下記のようになっているのがわかります。

https://www.youtube.com/embed/動画のID

埋め込みURLと動画のURLを比較して見ると最後の動画のIDが一緒なので、フォームで送られたURLのIDの部分だけを抜き取って埋め込みURLとして出力すれば良いですね!

https://www.youtube.com/embed/yOAwvRmVIyo
https://youtu.be/yOAwvRmVIyo

splitメソッド

ここでURLの下11桁を抜き取る時に少し躓いたので説明していきます。/以下を抜き取るためにsplitメソッドを使っていきます。
/で区切った配列を返してlastを使って末尾の部分を取得しています。

https://youtu.be/yOAwvRmVIyo.split('/').last
=> yOAwvRmVIyo

insertモデルでURLの下11桁を抜き取るメソッドを定義しておきます。ビューが複雑になるのを防ぎます。

def split_id_from_youtube
    identifier.split('/').last if youtube?
end

また下記のサイトを参考にしてYouTube出力画面を実装していきます。※ slim表記になっています。

YouTube 埋め込みプレーヤーとプレーヤーのパラメータ  |  YouTube IFrame Player API

.insert-youtube
  = content_tag 'iframe', nil, width: width, height: height, src: "https://www.youtube.com/embed/#{insert.split_id_from_youtube}", \
    frameborder: 0, gesture: 'media', allow: 'encrypted-media', allowfullscreen: true


RSpecメモ

  • なんでjs: trueが使われているのかについて、ブロック追加の画面がModalウィンドウだったので、JSをテストする必要があるから。
it 'プレビューした記事にYouTubeが埋め込まれていること', js: true do
  • YouTubeのURLが適切に埋め込まれているか確認するための書き方。
expect(page).to have_selector("iframe[src='https://www.youtube.com/embed/yOAwvRmVIyo']")
  • Twitterの埋め込みの時にsleep 1を使っている理由は、Json parce処理でUPDATE完了前にプレビューしないよう待機している。
# ページ内の一番最初の"更新する"を押す
page.all('.box-footer')[0].click_button('更新する')
sleep 1 
click_on('プレビュー')

Twitterの埋め込みの時にwidgets.jsを読み込んでいるので少し時間がかかる。なので少し時間をおく必要がある。