Ruby on Rails Learning Diary

学習したことの整理用です。

【Rails】【エラー】method: :deleteで指定しているのにshowアクションへ飛んでしまう

エラー発生

復習用に簡単なCRUD機能のついた記事投稿アプリを作成しています。
削除用のボタンを押しても削除されず、なぜかshow詳細ページへ遷移してしまう…。
method: :deleteで指定し、Cromeの検証画面でも正常にみえるのに…

f:id:Study-Diary:20201022191048p:plain
Crome検証画面

    = link_to 'delete',post_path(post),
                      class: "btn btn-danger",
                      method: :delete,
                      data: {confirm: 'Are you sure?' }
  def destroy
    @post.destroy!
    flash[:success] = 'You deleted your post!'
    redirect_to posts_path
  end
resources :posts

原因

(略)
//= require rails-ujs #これが抜けてた…

GitHub - rails/jquery-ujs: Ruby on Rails unobtrusive scripting adapter for jQuery

Unobtrusive scripting adapter for jQuery This unobtrusive scripting support file is developed for the Ruby on Rails framework, but is not strictly tied to any specific backend. You can drop this into any application to:

force confirmation dialogs for various actions; make non-GET requests from hyperlinks; make forms or hyperlinks submit data asynchronously with Ajax; have submit buttons become automatically disabled on form submit to prevent double-clicking. These features are achieved by adding certain "data" attributes to your HTML markup. In Rails, they are added by the framework's template helpers.

ハイパーリンクでGETリクエスト以外のものを生成してくれる。
軽く1時間は悩んでいたので解決してすっきり。

参考

[Rails 3] link_toのmethodが効かない!?|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

link_toで突然methodが効かなくなって困ってるあなたへ - Qiita

【Rails】雑多なメモ

all_day

昨日作った記事だけ取得するscopeを作りたかったとき、Date.yesterdayTime.current.yesterdayだと想定している挙動にならない…

all_dayを使う!

# NG
scope :yesterday_published, -> { where(published_at: Date.yesterday) }

# Good
scope :yesterday_published, -> { where(published_at: 1.day.ago.all_day) }

BETWEEN '2020-10-20 00:00:00' AND '2020-10-20 23:59:59というように、丸一日分を取ってきてくれる。

f:id:Study-Diary:20201021161834p:plain
all_day

参考

Railsで特定の日の新規ユーザ数などを取るクエリを作る - Qiita
DateAndTime::Calculations

特定の条件を満たすレコード数を取得するには?

countメソッドを使う。

f:id:Study-Diary:20201021165941p:plain
コンソール画面

癖でlengthを使ってしまうけど、countだと単純な数だけじゃなくて、要素を指定した上の数も取得できる。

【Rails】世界で一番分りやすく詳しいcountメソッドの使い方 | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

Action Mailer

Action Mailer関連で、簡単なはずなのにちょっと詰まったところ

送信メールのプレビューを見るには

送信したメールのプレビューは、letter_opener_webを使えばいいのですが、送信するメールのプレビューを見たいときは? study-diary.hatenadiary.jp

f:id:Study-Diary:20201021162726p:plain
rails g mailer UserMailer

メイラーを生成するときに、自動生成されるspec/mailers/previews/user_mailer_preview.rbを使う。

# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
  def report_summary #user.mailer.rbで作ったメソッド
    UserMailer.report_summary
  end
end

こんな感じで、メソッドを追加しておき、コメント部分のURLhttp://localhost:3000/rails/mailers/user_mailerへアクセスするとプレビュー画面が確認できる。

f:id:Study-Diary:20201021163718p:plain
preview画面

参考

『パーフェクト Ruby on Rails』P235

メール送信の挙動を確かめるには

cronjobで、毎日AM9時にメールを送信すると設定していたのですが、今すぐにメールを送信してみたいとき・・・ deliver_nowを呼び出す。
コンソールで、ArticleMailer.(メソッド名).deliver_nowを実行する。

f:id:Study-Diary:20201021164442p:plain
メールを送信するには

letter_opner_webで確認するとメールが送られている。

f:id:Study-Diary:20201021164707p:plain
letter_opener_web

参考

Action Mailer の基礎 - Railsガイド

【Rails】Active Storageを使って画像を複数枚アップロード&削除する

実装したいこと

以前、ActiveStorageを使って画像をアップロードする機能を実装しました。
今回は、画像を複数枚アップロード&削除機能を実装したいです。
参照する記事が少なく手こずってしまいました。。特に削除機能は、情報が少なくて苦労しました。

study-diary.hatenadiary.jp

複数枚の画像を一度にアップロードする

Model, Controller and Viewを設定していきます。

Model

複数枚の画像をアップロードしたいモデルにhas_many_attachedを追加します。

class Post < ApplicationRecord
  belongs_to :user
  has_one_attached :image
  has_many_attached :main_images #追加
(略)
end

Controller

main_images: []これで配列を渡しています。末尾に追加します。

(略) 
 private

def post_params
    params.require(:post).permit(:title, :body, :image, main_images: []) #最後追加
end

View

複数枚の画像が配列に入っているので、eachで回して表示します。

#入力フォーム部分(gem 'simple_form'使用)
= f.input :main_images, as: :file, hint: 'JPEG/PNG (1200x400)', input_html: { multiple: true }

#画像表示部分
- if @post.main_images.attached?
        - @post.main_images.each do |image|
          = image_tag image.variant(resize: '300x100').processed
躓いたところ

gem 'simple_form'を使っているときは、複数枚選択を設定するとき、input_html: { multiple: true }を使います。
multiple: trueだけだと、機能しません。

ruby on rails 4 - Using simple_forms simple_field_for with a multipart file uploader for Carrierwave - Stack Overflow

アップロードした画像を削除するには

これが悩んでしまいました。。この記事を参考にして、新しくアクションを作って…と実装しようとしたのですが、うまくルーティングが組めず撃沈しました。
新しく画像削除用のコントローラーを生成してシンプルに実装したらようやくうまくできました。

file upload - Rails 5.2 Active Storage purging/deleting attachments - Stack Overflow

Controller

class Post::AttachmentsController < ApplicationController

  def destroy
    image = ActiveStorage::Attachment.find(params[:id])
    image.purge
    redirect_to posts_path
  end
end

ちなみに、継承元を含めるコントローラーを生成するコマンド❯ rails g controller post::attachments

Rooting

(略)
    resource :post do
      member do
        resources :attachments, only: %i[destroy], controller: 'post/attachments'
      end
    end

View

      - if @post.main_images.attached?
        - @post.main_images.each do |image|
          = image_tag image.variant(resize: '300x100').processed
          = link_to '削除', post_attachment_path(image.id), method: :delete, class: 'btn btn-danger' #追加

【Rails】Active Storageを使って画像をアップロードする

実装したいこと

以前、gemCarrier Waveを使って画像アップロード機能を実装しました。
今回は試しにActive Storageを使用してみたいと思います。
初めて使ったのですが、すごく簡単でした。
ただし、ファイルの形式のバリデーションやデフォルト画像の設定などの機能はついてないので不便さも感じました。

study-diary.hatenadiary.jp

Active Storageとは

Rails5.2から提供されるようになったファイルアップロード機能

Active Storage の概要 - Railsガイド

実装の流れ

  1. Active Storageのセットアップ
  2. ファイルをアップロードしたいModelの設定
  3. Viewの設定
  4. Contorollerの設定

Active Storageのセットアップ

$ rails active_storage:install

このコマンドでマイグレーションファイルが生成されるので、ターミナルでrails db:migrateします。

# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
  def change
    create_table :active_storage_blobs do |t|
      t.string   :key,        null: false
      t.string   :filename,   null: false
      t.string   :content_type
      t.text     :metadata
      t.bigint   :byte_size,  null: false
      t.string   :checksum,   null: false
      t.datetime :created_at, null: false

      t.index [ :key ], unique: true
    end

    create_table :active_storage_attachments do |t|
      t.string     :name,     null: false
      t.references :record,   null: false, polymorphic: true, index: false
      t.references :blob,     null: false

      t.datetime :created_at, null: false

      t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
      t.foreign_key :active_storage_blobs, column: :blob_id
    end
  end
end

生成されるテーブルはそれぞれ、ActiveStorage::AttachmentActiveStorage::Blobモデルに紐付いています。

ActiveStorage::Blobは、アップロードした画像についての情報を管理するモデル

ActiveStorage::Attachment は、画像を扱う他のモデルとActiveStorage::Blobを結びつける中間テーブル的なモデル。ポリモーフィック関連となっている。

アップロードしたファイルの保存先設定

config/environments/development.rbでファイル管理場所を指定できます。

  #33行目あたり
  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

今は、localが指定されています。localとはどこなのか更にみていきます。
コメント部分にあるように、config/storage.ymlを確認します。

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

service: Diskでローカル環境にファイルが保存される設定になっています。
今回はデフォルトの設定のまま進めていきます。

Model

今回は、Postモデルで画像を扱えるようにします。
各投稿に1つの画像を添付できるようにしたいので、has_one_attachedメソッドを使います。
多数添付したいときは、has_many_attachedを使います。

class Post < ApplicationRecord
  belongs_to :user
  has_one_attached :image #ここ

(略)
end

Views

入力フォームを整えていきます。

= form_with model:@post, local: true do |f|
  (略)
  .form-group
    = f.label :image
    = f.file_field :image, class: 'form-control'
  = f.submit nil, class: 'btn btn-primary'

画像を表示したい部分には以下のコードを使います。

= image_tag post.image.variant(resize_to_limit: [300,200]) if post.image.attached?

image_tagは、画像が添付されていないときはエラーになるので、ifで判定文をつけてあげます。

variant(resize_to_limit: [300,200])でサイズを指定しています。
この機能を使いたい場合は、Gemfileの以下のgemコメントアウトを外す必要があります。

# Use Active Storage variant
gem 'image_processing', '~> 1.2'

Contoller

Strong Parameterで弾かれないように、imageのパラメーターも許可するように設定しておきます。

(略)
def post_params
    params.require(:post).permit(:title, :body, :image)
end

完成

f:id:Study-Diary:20201017200342p:plain
投稿フォーム
f:id:Study-Diary:20201017200400p:plain
投稿したもの

【Rails】YoutubeとTwitterをAPIを使わずに記事に埋め込む

実装したいこと

記事投稿アプリで、YoutubeTwitterを記事に埋め込みたい。
URLを入力したら、埋め込み用に変換して、記事に反映させたい。
Slim記法が苦手で、HTMLに反映させるのに手こずってしまいました。。便利ツールを使ってようやくできた。(後述)

データの流れ

  1. ユーザーがYoutubeTwitterを選択し、入力フォームにURLを入力する。
  2. ユーザーの入力した値を受け取り、保存する。
  3. ユーザーが入力した値を記事に埋め込める形式に変換する。
  4. Viewに反映される。

実装の流れ

  1. ユーザーの入力値を受け取るカラムを用意する。
  2. 入力フォームを用意する。
  3. 埋め込みに対応してくれるhtmlを返すviewを用意する。
  4. 受け取ったURLを変換するメソッドを準備する。

カラム準備

ユーザーが入力した値を保存するためのカラムを用意します。

embed_typeYoutubeTwitterを選択します。
identifierでURLを保存します。

class Embed < ApplicationRecord

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

  validates :identifier, length: { maximum: 200 }

end

入力フォーム

YoutubeTwitterかを選択し、identifierを入力できるように設定します。
(gem 'simple_form'を使っています。)

(略)
.box-body
    = f.input :embed_type, collect: Embed.embed_types_i18n.invert, include_blank: false
    = f.input :identifier
(略)

View

  1. まず、YoutubeTwitterの公式を調べて、どういう形式のHTMLで表示すれば埋め込みに対応できるのか調べます。
  2. 1で調べたHTMLを返すように、Slim記法で設定します。

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

Twitter公式
タイムラインを埋め込む方法
Overview | Docs | Twitter Developer Twitter Publish

HTMLをSlimに変換できる
HTML2slim

Youtubeのケース

上記のYoutube公式の動画にも丁寧に解説がありますが、埋め込みたいYoutube動画ページで、「共有」→「<> 埋め込む」を選択すると、埋め込み用のHTMLを用意してくれます。

f:id:Study-Diary:20201017135831p:plain
Youtube用埋め込みHTML

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

私はSlimを使っているので、このHTMLをSlim記法へ変換します。
ただし、とても苦手なので上記のサイトを使っています…

YoutubeのURLを切り取る

Youtubeの埋め込み用URLは以下の構成になっています。

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

最後の/までは固定値として設定し、ユーザーの入力したURLのうち、最後の/以下だけを受け取り、反映させていきます。

最初私は、embed.identifier.last(11)で末尾の11字だけ切り取って実装していたのですが、これだと開始位置を指定したURLを貼り付けられた場合に動かなくなってしまいます。
なので、以下のように、/以下を取得するメソッドを作っておきます。

splitメソッドで、/で区切りをつけた配列を返して、そのlast末尾部分を取得しています。

(略)
  def split_id_from_youtube_url
    # YoutubeならIDのみ抽出
    identifier.split('/').last if youtube?
  end
ruby:
  embed = local_assigns[:embed]
  width = local_assigns[:width] || 853
  height = local_assigns[:height] || 480


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


/ Youtube公式に乗っている埋め込み用のHTMLを出力する形にする。
/ #{embed.identifier.last(11)}でIDを切り取ると、開始秒指定されたときにOUT

Twitterのケース

上記の公式に乗っているように進めていきます。
貼り付け用に変換してくれる公式ツールがあるので、ここにツイートを貼り付けて埋め込み用HTMLを生成します。
Twitter Publish

<blockquote class="twitter-tweet">
<p lang="en" dir="ltr">A huge thank you to all of the bosses who are making voting a priority for their employees and giving people the time they need to vote. I’m doing that for my team 😘😘 Please share your voting plans with me!</p>&mdash; Lady Gaga (@ladygaga) 
<a href="https://twitter.com/ladygaga/status/1317133489829937152?ref_src=twsrc%5Etfw">October 16, 2020</a></blockquote> 
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

これの無駄な部分を省いて、この部分があれば良さそう

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

これを上記サイトでSlimへ変換して整えればOK

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

泥臭くやってしまったけど、もっとスマートな方法があるかもしれないです。。

RSpecメモ

# 更新ボタンが同じページにいくつかあるとき… 
page.all('.box-footer')[0].click_button('更新する')

# Youtubeの埋め込みを確認したいとき
expect(page).to have_selector("iframe[src='https://www.youtube.com/embed/ojdbDYahiCQ']")

使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita

【RSpec】画像の登録と表示についてテストする

実装したいこと

画像投稿機能について、前回実装した内容をテストしたい
画像選択の仕方と、画像の位置をどう判定するのか悩みました。

study-diary.hatenadiary.jp

前提

RSpecFactory Botは設定済みです。

Active Storageセットアップ

Active Storageのサービスはconfig/storage.ymlで宣言します。アプリケーションが使うサービスごとに、名前と必要な構成を指定します。

Active Storage の概要 - Railsガイド

# Diskサービスを使うと宣言(ローカルのディスクを使う)
test:
  service: Disk
  root: <%= Rails.root.join('tmp', 'storage') %>

他には、例えばAmazon S3サービスを使うと宣言することもできます。

System Spec

画像の選択は、attach_file 'article[eye_catch]', "#{Rails.root}/spec/fixtures/image/画像名.jpg"で指定します。
spec/fitures/imageに画像をセットしておきます。

expect(page).to have_selectorで画像の位置と画像が表示されているか判定します。

require 'rails_helper'

RSpec.describe "AdminArticlesEyecatches", type: :system do
  let!(:article) { create :article }
  let(:user) { create :user }

# 画像選択までは終えておく。
  before do
    login_as(user)
    visit articles_path
    attach_file 'article[eye_catch]', "#{Rails.root}/spec/fixtures/image/画像名.jpg"
  end

  describe 'アイキャッチ横幅調整機能' do
    context '横幅100〜700px(正常値)に指定した場合' do
      it '記事の更新に成功。プレビュー画面でアイキャッチが表示される' do
      (略)
        expect(page).to have_selector("img[src$='画像名.jpg']")
      end
    end

  describe 'アイキャッチ位置を変更' do
    context 'アイキャッチ位置を「左寄せ」に設定した場合' do
      it 'アイキャッチが左寄せで表示される' do
        choose '左寄せ'
  (略)
        expect(page).to have_selector('section.eye_catch.text-left')
      end
    end
  end
end

attach_file

#attach_file(locator = nil, paths, make_visible: nil, **options) ⇒ Capybara::Node::Element Find a descendant file field on the page and attach a file given its path.
Module: Capybara::Node::Actions — Documentation for jnicklas/capybara (master)

参考

Ruby on Rails のテストフレームワーク RSpec 事始め - Qiita

【Rails】ユーザーの入力値を画像表示に反映させる

実装したいこと

記事投稿アプリで、記事のアイキャッチ画像を設定できます。

前提

記事投稿の機能は実装済です。
今回は画像の表示を変更する部分のみの実装です。
ファイルのアップデートはActive Storageを使用しています。

どう実装していけばいいか

どうやって実装するか全く思いつかず、数時間悩んでしまいました。。シンプルにデータの流れと入出力を順を追って考えたら何とか実装できました。

  1. ユーザーが入力フォームに値を入力する
  2. 入力値を受け取って、保存する。
  3. 画像を表示するときに、保存した値を反映させる。

実装の流れ

  1. ユーザーの入力値を保存するためのカラムを追加する。
  2. 入力フォームを追加する。
  3. 画像表示部分のclassを動的に変更する。

カラム追加

f:id:Study-Diary:20201016113033p:plain
カラム追加

ユーザーの入力した「画像幅」・「画像位置」を受け取るカラムをそれぞれ作成します。
それぞれ、eyecatch_widtheyecatch_positionとしました。

ターミナルで、rails db:migrateします。

enum設定

画像位置については、左寄せ・中央寄せ・右寄せの3択で設定できるようにします。

enum eyecatch_position: { left: 0, center: 1, right: 2 }

gem 'enum_help'を使用しているので、翻訳ファイルを設定しておきます。

ja:
  enums:
    article:
      eyecatch_position:
        left: '左寄せ'
        center: '中央寄せ'
        right: '右寄せ'

Validation設定

ユーザーの入力値は、100~700pxに限定します。
空欄を許可しておかないと、アイキャッチ画像を設定せずに更新したいときにもバリデーションエラーになるので必須の設定です。
私は最初2行で設定してしまったのですが、1行で設定できるんですね。スマート

# BAD
validates :eyecatch_width, allow_nil: true,
                             numericality: { greater_than_or_equal_to: 100, message: 'は100以上の値にしてください' }
validates :eyecatch_width, allow_nil: true,
                             numericality: { less_than_or_equal_to: 700, message: 'は700以下の値にしてください' }

# GOOD
validates :eyecatch_width, numericality: { less_than_or_equal_to: 700, greater_than_or_equal_to: 100 }, allow_blank: true

入力フォーム作成

画像幅を入力するフォームと、画像位置を選択するラジオボタンを設置していきます。

      = simple_form_for @article, url: article_path(@article) do |f|
        .box-body
          - if @article.eye_catch.attached?
            = image_tag @article.eye_catch_url(:thumb), class: 'img-thumbnail'
            br
            br
          = f.input :eyecatch_width
          = f.input_field :eyecatch_position, as: :radio_buttons
     
# 以下NG ラジオボタンが重なってしまった
          = f.input :eyecatch_width
          = f.input :eyecatch_position, as: :radio_buttons

f:id:Study-Diary:20201016115655p:plain
入力フォーム

gem 'simple_form'

以上のコードのように、簡単にフォームを作れるgemです。

GitHub - heartcombo/simple_form: Forms made easy for Rails! It's tied to a simple DSL, with no opinion on markup.

画像表示部分のclassを動的に変更する。

これが難しかったです。
Boot Strapを使用しているので、class名を変えれば、画像の位置を変えられそうです。

最初、article.eyecatch_positionの値をApplicationHelperで判定して、class="float-right" class="mx-auto d-block" class="float-left"を返す…と実装したのですが、それよりも、class="text-right" class="text-center" class="text-left"を使用すれば、enumの値をそのまま使用できます。

画像幅は、保存した値をarticle.eyecatch_widthでそのまま取ってこれます。

# BAD
  - if article.eye_catch.attached?
    section.eye_catch class="#{eyecatch_position_class(article)}" #eyecatch_position_classというApplicationHelperメソッドを呼んでいた
      = image_tag article.eye_catch_url(:lg), style: "width: #{article.eyecatch_width}px;"


# GOOD
  - if article.eye_catch.attached?
    section class="eye_catch text-#{eyecatch_position}"
      = image_tag article.eye_catch_url(:lg), class: 'img-fluid', width: article.eyecatch_width

Controller

Strong Parameterで入力できる値を追加します。

  def article_params
    params.require(:article).permit(:eye_catch, :eyecatch_width, :eyecatch_position)#ここ
  end

参考

『パーフェクト Ruby on Rails』P225
『現場Rails』P306