wheneverを使用して記事数一覧のメール送信

2021.6.10 応用編10

 

<目標>

  • wheneverを導入して毎日am9:00に下記の内容を管理者にメールで送信させるメールの件名には「公開済記事の集計結果」と設定

 

流れ

参照

ActionMailerとWheneverの使い方とメール送信の定期実行(letter_opener)|moeno|note

Mailer作成

$ rails g mailer ArticleMailer

ActiveMailer::Baseを継承したApplicationMailerArticleMailer(自分で命名したもの)が生成される。

・Mailerを作成するとviewのディレクトリとテストも同時に作成される。

 

app/mailer/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
end
 
app/mailer/article_mailer.rb
 
class ArticleMailer < ApplicationMailer
def report_summary
@published_article_count = Article.published.count
@articles_published_at_yesterday = Article.published_at_yesterday
mail(to: 'admin@example.com', subject: '公開済記事の集計結果')
mail(to: '宛先のアドレス' , subject: '件名' )
end
end
 

 

Milerビューを作成する

"app/views/layouts/mailer.html.erb" "app/views/layouts/mailer.text.erb"

この2つのファイルがmailerを作成したタイミンで自動的に作成されている。

 

メール送信の定期実行

lib/tasks以下にcronを使用して、定期的に実行する処理のかたまり(rakeタスク)を作成していく

(lib/tasks/article_summary.rake)

namespace :article_summary do
desc '管理者に対して総記事数、昨日公開された記事数とタイトル
をメールで送信'
task mail_article_summary: :environment do
ArticleMailer.report_summary.deliver_now
end
end
ポイント

namespaceはfile名と揃える(自分で作成)

・"desc"はこのタスクの説明(自分で作成)

・"task" の "mail_article_summary:" は自分で命名していい。config/suchedule.rbにも同じ名前を記述することになる。

 

whenever

$ wheneverize . 

↑を作成中のアプリディレクトリ内で実行。config/suchedule.rbというファイルが作成され、ここに定期実行したいタスク設定(実行の間隔、時間)を書き込んで反映させる。

config/suchedule.rb

# Rails.rootを使用するために必要
require File.expand_path(File.dirname(__FILE__) + '/environment')
# cronを実行する環境変数
rails_env = ENV['RAILS_ENV'] || :development
# cronを実行する環境変数をセット
set :environment, rails_env
# cronのログの吐き出し場所
set :output, "#{Rails.root}/log/cron.log"
# rakeタスクを1時間ごとに実行
 
every 1.day, at: '9am' do
rake 'article_summary:mail_article_summary'
end

 

設定の反映

cronにデータを反映させる

 $ bundle exec whenever --update-crontab

 

letter_opener gem の使い方

開発環境で送信したメールをブラウザで確認することができる。

 

設定ファイル編集

(config/environment/development.rb)

config.action_mailer.delivery_method = :letter_opener_web
#↑メール送信方法を記述する (test.rbだけ :test にする。テストはテスト配信)
config.action_mailer.default_url_options = { host: 'localhost:3000' }

参照

Railsのconfig/enviroments配下を読んでみる - Qiita

(spec/mailers/previews/article_mailer_preview.rb)に記載されているURLでプレビューできる。

class ArticleMailerPreview < ActionMailer::Preview

le_mailer/report_summary #これ
def report_summary
ArticleMailer.report_summary
end
end

 

ルーティング編集

(route.rb)

if Rails.env.development? #開発時用の処理
get '/login_as/:user_id', to: 'development/sessions#login_as' #開発環境でログインする為
mount LetterOpenerWeb::Engine, at: '/letter_opener' #今回ここ追加
end

 

locaohost:3000/letter_opener 

送信されたメールをブラウザで確認できる。

 

ポイント

deliver deliver_now deliver_later 違い

ex) 今回の場合

(app/mailers/article_mailer.rb)

class ArticleMailer < ApplicationMailer
def report_summary
@published_article_count = Article.published.count
@articles_published_at_yesterday = Article.published_at_yesterday
mail(to: 'admin@example.com', subject: '公開済記事の集計結果')
end
end

このreport_summaryメソッドはActionMailer::MessageDeliveryオブジェクトを返す

pry(main)> ArticleMailer.report_summary.class
=> ActionMailer::MessageDelivery

ActionMailer::MessageDeliveryオブジェクト:

そのメール自身が送信対象であることをdeliver_nowやdeliver_laterに伝える。

Mail::Message(メールのメッセージに関連することを提供する)をラップしている。

Mail::Messageのに関して参照

Class: Mail::Message — Documentation for mikel/mail (master)

deliver

ex)

ArticleMailer.report_summary.deliver

Mail::Message#deliver(=オブジェクト自体)が呼ばれる。

Mail::Message#deliverを直接呼ぶのではなく、ActionMailer::MessageDeliveryのメソッドを呼ぶべき。

 

deliver_now(同期処理=メールを送信する)

→メールを送信し終えるまで、次の行は実行はされない。

railsコンソールやrakeタスクなど、すぐに処理を実行し終了まで待ちたい時に使う。

 

deliverとの違い

ソースコード

 

ActionMailer::MessageDelivery#deliver_now```rb def deliver_now processed_mailer.handle_exceptions do message.deliver end end ``` [View on GitHub](https://github.com/rails/rails/blob/master/actionmailer/lib/action_mailer/message_delivery.rb#L113-L117)

 

例外処理をしているなかで、Mail::Message#deliverを実行している。

→同じ同期処理のdeliverではなく、deliver_nowを使用すべき。

 

deliver_later(Active Jobを使用して非同期でメールを送信する)

→メールの送信処理を待たなくても次の行を実行できる。

ユーザーはメール送信という時間のかかる処理を待たずに次の表示をみることができる。

 

ActionMailer::MessageDelivery#deliver_later```rb def deliver_later(options = {}) enqueue_delivery :deliver_now, options end ``` [View on GitHub](https://github.com/rails/rails/blob/master/actionmailer/lib/action_mailer/message_delivery.rb#L94-L96)

 

MailerのviewでHTMLタグなどを書かないこと

他のViewファイル同様レイアウトファイルが指定されているから、書いてしまうと重複になってしまう。

 

class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer' #ここで指定
end

 

(views/layouts/mailer.html.slim)

html
body
= yield

 

 

empty? blank? present? 違い

empty?

空かどうかを確認する。レシーバーがnilだとUndefineMethodになってしまう。

[1] pry(main)> .empty?
=> true
[2] pry(main)> ''.empty?
=> true
[3] pry(main)> nil.empty?
NoMethodError: undefined method `empty?' for nil:NilClass
from (pry):3:in `<main>'
[4] pry(main)> nil.respond_to? :empty?
=> false

 

blank?

空かどうかを確認する。nilに対しても使用できる。
[5] pry(main)> .blank?
=> true
[6] pry(main)> ''.blank?
=> true
[7] pry(main)> nil.blank?
=> true
[8] pry(main)> nil.respond_to? :blank?
=> true

 

present?

存在するか確認する。!blank?と同じ結果になる。nil対しても使用できる。
[9] pry(main)> [].present?
=> false
[10] pry(main)> ''.present?
=> false
[11] pry(main)> nil.present?
=> false
[12] pry(main)> nil.respond_to? :present?
=> true

 

 

 

 

 

 

 

 

トップ画面をスライダー形式に変更

2021.6.2  応用編9

 

 <目標>

  • ブログのトップ画像は複数枚の画像が一定間隔で切り替わるようにする
  • 切り替わる画像は、管理画面でアップロードと削除ができるようにする
  • faviconやog-imageに関しても、個別に削除できるようにする
  • main_imagesには、複数の画像を一度に登録できるようにする

 

エラーが出た点

カスタムバリデータ

もともとの実装だと、faviconとog-imageの保存は可能だが、複数の画像を保存した時にエラーになる。

こんな感じ

f:id:mmm_st:20210605124338p:plain

なぜか確認した...

f:id:mmm_st:20210605124508p:plain

valueに値は入っている。byte_sizeがだめなのか?

まず、bute_sizeって何か?

→アップロードされる画像のデータ量バイト数..?

(schema.rb)

create_table "active_storage_blobs", force: :cascade 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"], name: "index_active_storage_blobs_on_key", unique: true
end

 

違いを見てみる

byebug で確認

faviconとog-image→保存可能

main_images→エラー。違いは複数選択しているところか...?

f:id:mmm_st:20210605131558p:plain

 

f:id:mmm_st:20210605131611p:plain

画像を複数選択したときのvalue.byte_size対応がない...?

(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]
has_error = true unless validate_maximum(record, attribute, value)
end

if options[:content_type]
has_error = true unless validate_content_type(record, attribute, value)
end

record.send(attribute).purge if options[:purge] && has_error
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

def validate_content_type(record, attribute, value)
if value.content_type.match?(options[:content_type])
true
else
record.errors[attribute] << (options[:message] || 'は対応できないファイル形式です')
false
end
end
end

 

ここでギブアップ。回答を確認した。

考え的には間違っていなかった。複数選択した時にvalueにbyte_sizeカラムがないのでNoMethodErrorが発生している。

元のバリテーションだと、複数の画像を選択した時にbyebugで確認したvalueの中身のように正しいデータが引き渡されていない。

→一枚選択の場合と、複数選択の場合に分けて実装する必要がある。

 

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

def validate_each(record, attribute, value)
return if value.blank? || !value.attached?
valueが空欄ならtrue あればfalse attachmentedがないならtrue あるならfalse
#valueが空欄またはattachedがなければ以下を返す。has_error=falseを返して終了。
valueがないので、判断する必要なし。
valueあればreturnは実行されないので下まで実行される。

has_error = false

if options[:maximum] #もし画像が選択されたら
 if value.is_a?(ActiveStorage::Attached::Many) #複数の画像が保存された時
value.each do |one_each|
unless validate_maximum(record, attribute, one_value)
#value一つ一つにpeivateで作成したメソッドを当てていく
has_error = true #unless文なので、↑がfalseだったら代入する
break #ループする分から抜け出す。falseが出たら次のvalueに行かずエラー出す。
end
end
else #一枚の時
has_error = true unless validate_maximum(record, attribute, value)
#validate_maximumがtrueであれば、文の最後にあるfalseが返ってくる。
→unless文なので、false=trueになるから、has_error = true になる。
end
end

if options[:content_type]
if value.is_a?(ActiveStorage::Attached::Many)
value.each do |one_value|
unless validate_content_type(record, attribute, one_value)
has_error = true
break
end
end
else
has_error = true unless validate_content_type(record, attribute, value)
end
end

record.send(attribute).purge if options[:purge] && has_error
end

private

def validate_maximum(record, attribute, value)
if value.byte_size > options[:maximum] #bite_sizeがmaximumより大きかったら
エラーを末尾に追加する
record.errors[attribute] << (options[:message] || "は#{number_t
o_human_size(options[:maximum])}以下にしてください")
false
else
true
end
end

def validate_content_type(record, attribute, value)
if value.content_type.match?(options[:content_type])
true
else
record.errors[attribute] << (options[:message] || 'は対応できないファイル形式です')
false
end
end
end

 

参照サイト

【Rails 5.2】 Active Storageの使い方 - Qiita

ActiveStorageについて|moeno|note

 

今回の課題のポイント

画像削除の処理はparamsで条件分岐をせずにActiveStorage::attachmentから取得していること。

paramsを用いると、

@site.favicon.purge if params[:status] == 'favicon' 

という感じで全ての要素に対して実装しなければならなくなる。

actuve_storage_attachmentsテーブルにはrecord_typeカラムがある。=idが一意になっている。これを利用して削除対象のリソースを取得する。

 (app/controllers/admin/site/attachments_controller.rb)

class Admin::Site::AttachmentsController < ApplicationController
def destroy
authorize(current_site)
image = ActiveStorage::Attachment.find(params[:id])
image.purge
redirect_to edit_admin_site_path
end
end

 

f:id:mmm_st:20210610164634p:plain

 

swiper

jQueryプラグイン

CSSとJSを使用することで、画像などをスライドできる機能を実装するもの。

↓このサイトを見るとイメージが湧きやすい

Swiper公式サイトのデモ

ガリガリコード

 

導入方法

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

Getting Started With Swiper

Swiperをインストールする

今回はnpmでインストールした

$ npm install swiper

nodes_modules配下のディレクトリにファイルが追加される

 

f:id:mmm_st:20210607194810p:plain

 

追加されたファイルを読み込む

(assets/javascripts/application.js)

//= require swiper/swiper-bundle.js
//= require swiper.js #今回はこれいらない

(assets/stylesheets/application.css.scss)

@import 'swiper/swiper-bundle';

 

node_modulesをassetsに追加する

(config/initializers/assets.rb)

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

 

HTML

(app/views/layout/_header.html.slim)

header
.swiper-container #クラスを指定する
.swiper-wrapper
- if current_site.main_images.present?
- current_site.main_images.each do |main_image|
= image_tag url_for(main_image), class: 'swiper-slide'
- else
= image_tag '/images/cover.jpg', class: 'swiper-slide'
.container.blog-title
h1 = link_to current_site.name, root_path
p.lead = current_site.subtitle

$(document).ready(function() { ❶
new Swiper('.swiper-container', { ❷
loop: true, ❸
autoplay: { ❹
delay: 3000, ❺
},
})
})

❶画像などを除いて、HTML=DOMの読み込みが終わったらfanction()の中の処理を実行する

❷クラス指定

❸最後に達したら先頭に戻る

❹自動的にスライドを開始(再生)させる

❺スライド間の感覚をミリ単位で指定。(今回は3秒)

参照

jQueryの基本 - $(document).ready - Qiita

スライダープラグイン Swiper(v5)の使い方 / Web Design Leaves

 

ActiveStorage

variantメソッド   processedメソッド

(app/views/admin/sites/edit.html.slim)

 
= simple_form_for [:admin, @site], url: admin_site_path do |f|
省略
 
= f.input :main_images, as: :file, input_html: {multiple: true},
hint: 'JPEG/PNG (1200x400)' #複数アップロードのオプション htmlの属性なので、
「input_html:」オプションをつけて指定している
 
- if @site.main_images.attached?
.main_images_box
- @site.main_images.each do |main_image|
.main_image
= image_tag main_image.variant(resize:'300x100').processed
= link_to '削除', admin_site_attachment_path(main_image.id),
method: :delete, class: 'btn btn-danger'

variant : 

(app/models/site.rb)モデルでhas_many_attachedなどを指定して、

class Site < ApplicationRecord
 
has_many_attached :main_images
 
validates :main_images, attachment: { purge: true, content_type:
%r{\Aimage/(png|jpeg)\Z}, maximum: 524_288_000 }

view側でこのようにサイズをその都度指定できる。

variant(resize:'300x100').processed

 

processed : すでにそのサイズで保存されている画像であれば、変換処理は行われず、即時にURLが返される。

 

参照

Active StorageのVariantの指定方法いろいろ - Qiita

 

 

 

 

 

(app/views/layouts/application.html.slim)

赤文字の部分bodyの下にあったのでheadbに追加

doctype html
html
head
= render 'layouts/meta_tags'
= stylesheet_link_tag 'application', media: 'all'
= yield 'stylesheets'
= javascript_include_tag 'application'
body
= render 'layouts/header'
section.container.page-content
省略

 

purgeメソッド

ファイルを削除したいときに使用する

(app/controllers/admin/site/attachments_controller.rb)

class Admin::Site::AttachmentsController < ApplicationController
def destroy
authorize(current_site) #権限確認
image = ActiveStorage::Attachment.find(params[:id])
image.purge
redirect_to edit_admin_site_path
end
end

 

validatesでpurge: trueにしておく必要がある

 

validates :og_image, attachment: { purge: true, content_type:
%r{\Aimage/(png|jpeg)\Z}, maximum: 524_288_000 }
validates :favicon, attachment: { purge: true, content_type:
%r{\Aimage/png\Z}, maximum: 524_288_000 }
validates :main_images, attachment: { purge: true, content_type:
%r{\Aimage/(png|jpeg)\Z}, maximum: 524_288_000 }

 

 

ActiveStorage::attachmentについて

Active Storageでキー名を指定するには - Qiita

 

 

 

RSpec

attach_fileメソッド

アップロードのinput要素にテスト用画像を添付することができる。第1引数:locator
第2引数:アップロードする画像のパス
第3引数:オプション

参照

Method: Capybara::Node::Actions#attach_file — Documentation for capybara (3.35.3)

Rails RSpec による画像添付結合テスト - Qiita

 

capybaraのlocatorについて


capybaraにおける要素の指定は「locator文字列」によって操作対象の要素を指定する。
locatorには「id」「name」「innerHTML(一般的にHTMLに含まれているタグ名のこと)」などを指定することができる。

[locatorに指定できる値]
・対象の要素のid属性の値
・対象要素のname属性の値
・対象要素のinnerHTMLの値
・対象の要素に対応する<label>の値(innerHTML)

 

例えば
attach_file ファイルセクレタにファイルを設定する
公式によるとattach_file ([locator], paths, options)
→attach_file (input要素のname属性とか、id属性とか, 画像へのパス, オプションあればオプション)って感じの構造になる。


admin_sites_spec.rbの

attach_file('site_main_images', 'spec/fixtures/images/runteq_man_top.jpg')

attach_file('site[main_images]', 'spec/fixtures/images/runteq_man_top.jpg')

こっちてもいけた


最初は'site_main_images'がdivタグのclass属性を指定してるのか謎だったけど、locatorの仕組みを理解したので納得できた!!!

site_main_images input要素のidを指定している。

site[main_images] input要素のnameを指定している。

 参照

Capybaraメモ - Qiita

 

%w()

配列を作成する。今回は画像を複数枚これで指定した。

Rubyで%記法(パーセント記法)を使う - Qiita

 

 

 

 

 

 

埋め込みメディアタイプにTwitter追加

2021.6.1 応用編8

 APIではなくローカルで作成する。ローカルで作成することによって、処理時間の短縮を狙う。

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

 

<目標>

ツイートのURLを登録して動的にツイートを埋め込めるようにする。

youtubeも同じ形で実装

 

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

ある一つのモデルが他の複数のモデルに属していることを、1つの関連付けだけで表現できる。

確認ポイントも見るととてもわかりやすい。

Active Record の関連付け - Railsガイド

 

流れ

youtube

 IDのみ抽出する。

(embed.rb)

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

最初はgoogleyoutube投稿された全ての動画の全てに割り振ったURLの末尾の11桁のvideo_idと呼ばれる数字を抽出した。だが開始時間を指定したurlになると、末尾の11桁が変わってしまうので、splitを使用して「/」で区切りをつけて「last」=最後の部分のみ抽出する。

https://www.youtube.com/embed/<split_id_from_youtube_url>という感じになる。

(views/shared/_embed_youtube.html.slim)

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

 参照

content_tag | Railsドキュメント

 

 

twitter

(views/shared/_embed_twitter.html.slim)

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

参照

scriptタグとは?どの位置で読み込むのが良いのかを解説します | ウェブカツ公式BLOG 

 

参照

【Rails】YoutubeとTwitterをAPIを使わずに記事に埋め込む - Ruby on Rails Learning Diary

rails学習 YouTubeとTwitterをローカルで埋め込む - Qiita

 

RSpec

js: true

JavaScriptを使用しないと操作できない処理をテストする。

Ajaxを使用した処理や、クリックイベントによるモーダル画面の表示など、JavaScriptなしては実行できない操作がある場合にこのタグをつける。

 

sleep 秒数

JavaScriptの処理が完了しないうちにテストが先に進んでしまうとテストが失敗するので、JavaScriptの処理が終わるまでテストを停止する。

参照

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

 

 

 

アイキャッチの表示サイズ・位置指定

2021.5.27

応用編7

 

アイキャッチ

投稿ページの記事タイトルの下に表示される画像のこと

 

Active Storage gem について

ファイルアップロードを簡単に実装できるgem。rails 5.2から導入されている。

 

使い方

1

gemfileに追加インストール後、

$ rails active_storage:install

↑active_storage_blobsactive_storage_attachmentsという2つのテーブルを作成するマイグレーションファイルを作成する

$ rails db:migrate

active_storage_blobs は実際にアップロードしたファイルが保存されるテーブル

active_storage_attachments 中間テーブル(今回でいえばarticle.rbとの)

 

2モデルに対して Active Storage用の設定を追加する

<一つのファイルを選択する>

has_one_attached :カラム名

<複数のファイルを選択する>

has_many_attached :カラム名

< 今回>

articleモデルに「eye_catch」というカラムで保存したい。

(article.rb)

has_one_attached :eye_catch

 

ポイント

上記をすれば、わざわざarticleテーブルにカラムを追加する必要がないので便利

 

3controllerでストロングパラメーターを定義すればOK

(articles_controller.rb)

def article_params
params.require(:article).permit(
:title, :description, :slug, :state, :published_at, :eye_catch,
:category_id, :eyecatch_align, :eyecatch_width, :author_id, tag_ids:
)
end

 

4view部分

こんな感じで使用できる

= image_tag article.eye_catch_url(:lg),
class: 'img-fluid', width: article.eyecatch_width

 

今回の実装の流れ

アイキャッチの横幅用と位置用のカラムを作成する

add_column :articles, :eyecatch_width, :integer
add_column :articles, :eyecatch_align, :integer, default: 0, null: false

eyecatch_width (横幅用のカラム)のデフォルトを100にするが、カラムにデフォルトを追加すると変更するときに大変になるので、ここでは設定しない。

ayacatch_align (位置用のカラム) defaul:0やnul:falseを追加することで、記事作成でバリテーションエラーとなる。

 

enumとvalidatesを追加

(article.rb)

enum eyecatch_align: { left: 0, center: 1, right: 2 } #enum設定
validates :eyecatch_width, numericality: { less_than_or_equal_to: 700,
greater_than_or_equal_to: 100 }, allow_blank: true
#100~700を指定

allow_blank:true 値が空の場合はバリデーションを実行しない。
属性の値がblank?に該当する場合にバリデーションがパスする。blank?に該当する値にはnilと空文字も含まれる。

ここの部分について

numericality: { less_than_or_equal_to: 700, greater_than_or_equal_to: 100 }
numericality: { in: 100..700 } #これで設定できるようになったらしい

Add validate numericality in range by mpapis · Pull Request #41022 · rails/rails · GitHub

 参照

Railsバリデーションまとめ - Qiita

 

 

controller部分

(admin/articles_controller.rb)

def article_params
params.require(:article).permit(
:title, :description, :slug, :state, :published_at, :eye_catch,
:category_id, :eyecatch_align, :eyecatch_width, :author_id, tag_ids:
) #追加
end

 

view部分

 

(articles/edit.html.erb)

simple_formとenum_help(日本語化のため)という2つのgemを使用することで↓の一文を作成することができる。

= simple_form_for @article, url: admin_article_path(@article.uuid) do |f|
.box-body
.
.
省略
= f.input_field :eyecatch_align, as: :radio_buttons

このようなHTMLが作成される。

f:id:mmm_st:20210601090826p:plain

simple_form_forに渡した第一引数のモデルインスタンス@articleから、

input_fieldに渡した第一引数の:eyecatch_align情報を読み取ってフィールドを生成している。

ラジオボタンのHTML生成を指示しているのはas: :radio_buttons部分。

他にもデフォルトで指定できる。

参照

https://github.com/heartcombo/simple_form#available-input-types-and-defaults-for-each-column-type

「右寄せ」「中央」「左寄せ」と日本語化して表示されているのはenum_helpのおかげ。

simple_formと組み合わせることで自動的に翻訳される。READMEに詳細あり。

 

= f.input :eyecatch_width, placeholder: '100'
#デフォルトの100。ここでvalidatesでallow_blank: trueにしているので、blankの際は
100が入る。

 

 

(shared/_article.html.slim)

- if article.eye_catch.attached?
section class="eye_catch text-#{article.eyecatch_align}" #enumを使用し位置指定
= image_tag article.eye_catch_url(:lg), class: 'img-fluid',
width: article.eyecatch_width #eyecatch_widthを使用して横幅指定

 

 

RSpec

attach_file

アップロードのinput要素にテスト用の画像を添付する。

第一引数にアップロードするinput要素のname属性の値、第二引数にアップロードする画像のパス、第三引数にオプション(今回はなし)を設定する。

Module: Capybara::Node::Actions — Documentation for jnicklas/capybara (master)

Rails RSpec による画像添付結合テスト - Qiita

 

have_css

指定されたCSSクレターに一致する要素が存在するかどうか確認

Method: Capybara::RSpecMatchers#have_css — Documentation for jnicklas/capybara (master)

 

$("[属性 $= '値’ ]")

ex) img[src$=’test.jpg’]

指定した値が属性の値と後方一致する要素

参照

jQuery リファレンス:[ 属性名 $= '値' ]

 

choose

ボタンを見つけて、チェック済みとしてマークする

参照

Method: Capybara::Node::Actions#choose — Documentation for jnicklas/capybara (master)

 

rand関数

擬似乱数をを生成する。引数に整数を与えると0からその整数値未満の整数を返す。

ex) rand(100) → 0から100

 

気になったところ

expect(page).to have_selector('section.eye_catch.text-left'),
'アイキャッチが「左寄せ」で表示されていません'

ここ

'section.eye_catch.text-left'

HTMLはこんな感じ

f:id:mmm_st:20210601100902p:plain

 

「classにeye_catch.text-leftクラスなんてないし、eye_catchとtext-left両方を検証してるのか...?」

と思ったので、テストの形を変えながら確認してみた。

'section.eye_catch.text-left'

 section → sectionタグ内に範囲を狭めている。なくても成功

. → クラスを指定してる

section.text-left.eye_catch → これでもで成功

結論

sectionタグ内のeye_catchクラスとtext-leftクラスを検証している。

 

 

 

 

「<<」について

「<<」について

この二箇所「=」と「<<」なぜわざわざ使い分けているのか理解したい。

trait :with_author do
transient do
sequence(:author_name) { |n| "test_author_name_#{n}" }
sequence(:tag_slug) { |n| "test_author_slug_#{n}" }
end

after(:build) do |article, evaluator|
article.author = build(:author, name: evaluator.author_name, slug: evaluator.tag_slug)
end
end

 

trait :with_tag do
transient do
sequence(:tag_name) { |n| "test_tag_name_#{n}" }
sequence(:tag_slug) { |n| "test_tag_slug_#{n}" }
end

after(:build) do |article, evaluator|
article.tags << build(:tag, name: evaluator.tag_name, slug: evaluator.tag_slug)
end
end

 

「=」のところを「<<」に変更し、ターミナルで確認してみたところ...

「NoMethodError:undefined method `each' for #Tag:0x00007f989520ccd8」というエラーが表示された。

eachってことは配列?と思い調べてみたが分からず、質問してみたところ、

 

「=」と「<<」の違いをちゃんと理解していなかった

「=」は代入

左辺を全て右辺の値で上書きする。

「<<」は追加

ActiveRecord::Relationに値を追加する。

 

今回のarticle.tagsは配列=正確にいうとActiveRecord::Relation

→article.tagsを左辺で上書きするのではなく、配列に追加するということをしたかったので、「<<」が使用されている。

 

has_many関連付けを宣言したクラスでは「<<」というメソッドが利用できる

collection<<(object,...)という形で、追加できる

class Article < ApplicationRecord
has_many :tags, through: :article_tags

このように定義されているので、

tags<<(object,...)ができる

参照

https://railsguides.jp/association_basics.html#has-many%E3%81%A7%E8%BF%BD%E5%8A%A0%E3%81%95%E3%82%8C%E3%82%8B%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89

 

 

Pundit 権限管理

2021.5.24

応用編6

<現在>

Punditは入っている

<目標>

権限設定の記述を見つけ、ライターにタグ・著者・カテゴリーの一覧表示・編集・削除を出来ないようにする。

権限エラーが発生した際に、403エラーのページを表示させる(403.htmlは、public配下に新たに作成する)

  Pundit gem

各権限管理

リソースに対して、どのユーザーであれば処理が許可されるのかを定義する。

参照

pundit/README.md at master · varvet/pundit · GitHub

<流れ>

(app/polices/application_policy.rb)このファイルで定義されるクラスを継承して、コントローラごとの認可ルールを作成していく。

class ApplicationPolicy
attr_reader :user, :record

def initialize(user, record)
#ここで定義されるuserはデフォルトでcurrent_userが引数に割り当てられるよう
になっている。recordは対応するモデルのインスタンスを手動で割り当てる。
@user = user
@record = record
end

def index?
false
end

def show?
false
end

def create?
false
end

def new?
create?
end

def update?
false
end

def edit?
update?
end

def destroy?
false
end

def scope
Pundit.policy_scope!(user, record.class)
end

class Scope
attr_reader :user, :scope

def initialize(user, scope)
@user = user
@scope = scope
end

def resolve
scope
end
end
end

 

今回の権限をつけたい部分が、STIなので、ここにまとめて作成する。

モデル名_policy.rbでファイル作成

モデル名Policyでクラス名を定義

def アクション名?で認可ルール(policy)を記述

def アクション名?の返り値によって認証するか判断している。falseだとアクションは拒否されて、Pundit::NotAuthorizedErrorが投げられる。

||」について参照

演算子式 (Ruby 3.0.0 リファレンスマニュアル)

(app/polices/taxonomy_policy.rb) 

class TaxonomyPolicy < ApplicationPolicy
def index?
user.admin? || user.editor?
end

def create?
user.admin? || user.editor?
end

def update?
user.admin? || user.editor?
end

def destroy?
user.admin? || user.editor?
end
end

 

403エラーページを表示する

2つ方法がある。

GitHub - varvet/pundit: Minimal authorization through OO design and pure Ruby classes

❶config/application.rbに↓を記述(今回)

config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] =
:forbidden

(public/403.html.erb)

<!DOCTYPE html>
<html>
<head>
<title>Forbidden(403)</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
</head>

<body>
<p>このページへのアクセス権限がありません。</p>
</body>
</html>

❷rescue_fromメソッドを使用して例外処理を記述する

ポイント

❶ヘッダーやフッターの共有ページについて

・今回はpublic以下の静的なページを表示させる為、ヘッダーやフッターの共有ページは不要。

・共通のヘッダー等のレイアウトを表示させたい場合は、Controllerでrescue処理を記載し、renderまたはredirect_toでapp/views以下のテンプレートファイルを表示させる。

❷開発環境で403エラーページを表示させるか

rescue処理をする場合は、if Rails.env.production?などの制限がないと開発中に他の機能で権限エラーが発生した時に、開発用のエラーページが表示されなくなってしまう。

今回のように、config/application.rbへの記載であれば、development.rbconfig.consider_all_requests_localtrueであるProduction環境のみに静的ページを表示することができる。

もし実装できているか確認する際は、development.rbconfig.consider_all_requests_localfalseにしてwriterでloginし、確認すると表示される。

 

 参照

config.consider_all_requests_local | Railsドキュメント

【Rails5】rescue_fromによる例外処理:アプリ固有のエラーハンドリングとエラーページ表示 - Qiita

 

 

RSpec

driven_by(:rake_test)でステータスコードをテストする

これがないと、この後の、have_http_status()「Capybara::NotSupportedByDriverError:Capybara::Driver::Base#status_code」というエラーになる

before do
driven_by(:rake_test)
end

 

ステータスコードについて

参照

ステータスコードとは?今さら聞けないステータスコードの基礎知識 | SEO研究所サクラサクラボ

 

 

初めて使用した

参照

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

(公式)

`have_http_status` matcher - Matchers - RSpec Rails - RSpec - Relish

 

 

have_http_status()

レスポンスのステータスコードを検証する

driven_by(:rake_test)を使用しないとエラーになるので注意!

expect(page).to have_http_status(403), 'ライターがカテゴリー
編集ページのアクセスに成功しています'

 

 hsve_selector

特定のタグやCSS要素、IDに特定の文字列が表示されていることを検証する

expect(page).not_to have_selector("input[value=#{category.name}]")

 HTML

<input class="form-control" type="text" value="1">

 

 

 

 

 

 

form object 検索機能を追加する

2021.5.21

応用編5 検索機能を追加する

form objectを使用して、検索機能を作る

Active Model 、scopeも使用していく

<現在>

・セレクトボックスのカテゴリー検索

・フリーワードによるタイトル検索

 

<目標>

・著者、タグをセレクトボックスにより選択できるようにする

・コンテンツ(記事内容)はフリーワード検索

・コンテンツは下書きに記事でも検索できるようにする

 

form object

Form_withまたはform_forのmodelオプション(:urlや:methodみたいなやつ?)にActiveRecord以外のオブジェクトを渡すデザインパターンのこと。

モデルと1対1で紐づかないフォームを作成しようとするとFat Controllerになりがち。FormObjectを使ったパターンを適用するとそれを解消することができる。

利点

・DBを使わないフォームでも、ActiveRecordを利用した場合と同じ方法を利用することができるので可読性が増す。

・他の箇所に分散されがちなロジックをform object内に集めることができる。

一応、ActiveRecordを利用する場合にも使うことができる

 

参照

form objectを使ってみよう - メドピア開発者ブログ

 

 Active Model

ActiveRecordからDBに依存する部分を除いた振る舞いを提供しているライブラリのこと。→DBを利用しないフォームでもAcriveRecordを用いた時と同じ記述ができる。

 

ActiveRecordを継承しないクラスでもActiveRecordと同じような便利メソッドが使える。

ex)validatesなど

参照

Active Model の基礎 - Railsガイド

 

ActiveModel::Attributes

ActiveRecordでしか使えなかった一部の機能がActiveModelでも使用できるようになった。

今回のポイントはActive::AtributesがRasil5.2から使用できるようになったこと。

クラスメソッドattributeに属性名と型を渡すと、attr_accessorと同じように属性が使えるようになる。

 

参照

ActiveModel::Attributes が最高すぎるんだよな。 - Qiita

[Rails] ActiveModel::Attributesの使い方(配列化やネストしたhashの取り扱いなども) - Qiita

ActiveModel::Attributesを使う - Qiita

 

Attribute API

これについて理解しておくと、Active::Atributesの新機能が理解しやすい。

Attribute APIのおかげで、ActiveRecordを操作する際にクラス属性の型を意識しなくても、指定の型へ変換してくれている。

 

勝手にstring型とかboolean型とかを指定の方に変換して、railsを動かしてくれている。

ex) 

id = '1' #文字列

user = User.new(id: '1')

User.id #=>1 数値に変換されている

 
これが今までActive Model で使用することができなかったが、5.2からしようすることができるようになった。
 

 

参照

【Rails】「ActiveModel::Attributes」が便利という話 - 日々の学びのアウトプットするブログ

 

 

 

scope

モデルのスコープ機能とは、モデル側であらかじめ特定の条件式に対して名前をつけて定義し、その名前でメソッドのように呼び出すことができる仕組みのこと。

 

class モデル名 < ApplicationRecord
scope :スコープの名前, -> {条件式 (sql文)} #基本形
scope :スコープの名前, -> (引数){条件式 (sql文)} #引数あり
end

 

(app/models/blog.rb)

class Blog < ApplicationRecord
scope :published, -> { where(published: true) }
end

(blogモデルに定義したスコープのpublishedを使用する場合)

Blog.published

 

scopeのmerge

「別のテーブルと繋いだ時にも検索に同じscopeを使用することができる」

ex)まず、sentencesテーブルに結合後も、検索に同じscope(ここで言ったら(where~))を使用することができる。

scope :body_contain, ->(body) { joins(:sentences).merge(wh
ere('sentences.body LIKE ?', "%#{body}%")) }

参照

【Rails】 | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

scopeの条件はjoinsで絞ろう - Qiita

Ruby on Railsのscopeメソッドで検索を効率化する

 

 

<既存のファイルを読み解いた>

category.rbがSTIだから、form objectあった

検索部分は(app/views/articles/index.html.slim)

h3.box-title 記事一覧
.box-tools
.ul.list-inline
li
= form_with model: @search_articles_form,#モデル指定
scope: :q,
#「送るパラメーターをどのハッシュでまとめるか」を指定検索
単語を”q”=>{…}の中にまとめることができる。
キーワードを入力するフィールドが2個3個と増えると、
”q”=>{"フィールド名”=>"検索単語",”フィールド名”=>”検索単語”,...} って感
じでparamsがまとまってくれる。
url: admin_articles_path,#URL指定 admin/articles#index get
method: :get, #get指定データ取得のため
html: { class: 'form-inline } #html指定 do |f|
 
 
=> f.select :category_id, #カテゴリー検索 セレクトボックス作成
Category.pluck(:name, :id) ,#pluck = 指定したカラムのレコードの配列を取得 
モデル.pluck(カラム名[,...])
:nameを消して確認すると、セレクトボックスにidしか表示されなかった。
:nameがあることで、カテゴリー名を使用して検索できる
{ include_blank: '〇〇' }, 検索部分に表示される(本文、カテゴリとか)
class: 'form-control#html指定
 
 
.input-group #検索部分のdiv class= "input-group"のところ
= f.search_field :title, class: 'form-control#検索ボックス作成
 
span.input-group-btn #検索ボタン
= f.submit '検索', class: %w[btn btn-default btn-flat]
 

 

セレクトボックス

参照

select | Railsドキュメント

f.select(メソッド名(今回はカテゴリー検索)、要素(配列orハッシュ)[オプションorHTML属性,イベント属性]

 

検索ボックス

参照

search_field | Railsドキュメント

f.search_field(メソッド名(今回はタイトル検索になってる),[オプション])

 

 (app/controllers/admin/articles_controller.rb)
def index
authorize(Article)

@search_articles_form = SearchArticlesForm.new(search_params)
#検索部分で入力されたtitle検索とcategory_idセレクトボックスの結果を代入
@articles = @search_articles_form.search.order(id: :desc).page(params[:page]).per(25)
検索部分を代入したものから、重複のないデータだけ集めてrelationに代入(searchで)
それを@articlesに追加
25個ずつ表示順番指定
end
 
def search_params
params[:q]&.permit(:title, :category_id)
end
 

(App/forms/search_article_form.rb)

class SearchArticlesForm
include ActiveModel::Model #ActiveModel導入
include ActiveModel::Attributes #attribute導入→2つでActiveModel::Attributes追加

attribute :category_id, :integer #category属性追加
attribute :title, :string #title属性追加

def search #articles_controller.rbで使用されている
relation = Article.distinct
#モデル.distinct重複のない値を1つにまとめる

relation = relation.by_category(category_id) if category_id.present?
#Category_idが存在するなら、重複のない値から、その値に
合うものを探し代入(ここはscope↓で定義済み)
title_words.each do |word|
relation = relation.title_contain(word)
wordに似ている言葉が入っていれば(ここはscope↓で定義済み)代入
end
relation #relationを返す
end

private

def title_words #上のtitle_words.each do |word|で使用
title.present? ? title.split(nil) :
titleが存在するのであれば、titleを先頭と末尾の空白を除き、
空白文字で分解する 存在しなければ
end
end
 
(article.rb)
scope :by_category, ->(category_id) { where(category_id: category_id) }
scope :title_contain, ->(word) { where('title LIKE ?', "%#{word}%") }

 

目標に向けて実装する

著者検索については、上の流れを確認すれば、実装可能なので省略。

 

タグ選択

最初は、著者選択と同じ形式で作成していたが、うまく動かなかった。

まず、articleテーブルとarticle_tagsテーブルの関係性を確認していなかった。

article_tagsテーブル

class ArticleTag < ApplicationRecord
belongs_to :article
belongs_to :tag
end

articleテーブル

class Article < ApplicationRecord
has_many :article_tags
end

 

ここを内部結合してからwhereで同じtag_idを検索しなければいけなかった。

scopeで内部結合し、検索する。

 

(App/forms/search_article_form.rb)

relation = relation.by_tag(tag_id) if tag_id.present?

 

(artucle.rb)

#間違いscope :by_tag, ->(tag_id) { where(tag_id: tag_id) }
scope :by_tag, ->(tag_id) { joins(:article_tags).where(article_tags: {tag_id
: tag_id} ) }
scope :スコープ名, ->(引数) { 結合(くっつけたいデーブル).検索(結合先のテーブル名
:カラム名と引数のtag_idがあっているか)

 

(app/controllers/admin/articles_controller.rb)

def search_params
params[:q]&.permit(:title, :category_id, :author_id, :tag_id)
end

 

参照

【Rails】 joinsメソッドのテーブル結合からネストまでの解説書 | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

他の方の質問

 

コンテンツ(記事内容)フリーワード検索

最初にscopeを間違いのように作成した。下書きに記事でも検索できるようにしなければならないので、ターミナルを確認したところ、

下書きはこのような形で、sentenceのbodyに格納されていることがわかった。

sentence"=>{"body"=>"<p>あああ</p>"}

公開される時にarticleのbodyに保存されるのか?と解釈した。

scopeで結合してみた。

 

(artucle.rb)

#間違いscope :body_contain, ->body) { where('body LIKE ?', "%#{body}%") }
scope :body_contain, ->(body) { joins(:sentences).merge(where('sentences.
body LIKE ?', "%#{body}%")) }
scope :スコープ名, ->(引数) { 結合(くっつけたいデーブル).merge(別のテーブルと
結合した時にも検索に同じscopeを使用することができる)(検索(結合先のテーブルの
bodyと引数のbodyにLIKE検索))}

 

(App/forms/search_article_form.rb)

body_words.each do |word|
relation = relation.body_contain(word)
end
private
def body_words
body.present? ? body.split(nil) : []
end

 

(app/controllers/admin/articles_controller.rb)

def search_params
params[:q]&.permit(:title, :category_id, :author_id, :tag_id, :body)
end

 

 

RSpec

ex)で使うところの全体部分

 

transient

本来のクラスには存在しないが、Factoryでのみ使用したい一時的な変数を定義する。
そこで定義されたものは実際のmodelにはセットされないしattributes_forでも出力されない。
何のために使うかというと作成時に挙動を変更するためのフラグや追加データとして利用するのが一般的。

ex)tag_nameとtag_slugという実際に作成するデータと直接関係ない新しいattributeを定義している。

transient do
sequence(:tag_name) { |n| "test_tag_name_#{n}" }
sequence(:tag_slug) { |n| "test_tag_slug_#{n}" }
end

参照

https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#transient-attributes

 

after build

生成したインスタンスがbuildされた直後に自由にインスタンスを修正することができる。コールバックみたいな感じ。

第二関数にevaluatorを渡すことで、transientブロック内の変数にアクセスすることができる。

ex)生成したインスタンスがbuildされた直後に、transientブロック内で定義されたattribute渡している。

after(:build) do |article, evaluator|
article.author = build(:author, name: evaluator.author_name, slug: evaluator.tag_slug)
end

 

evaluator

factory_botのcallbackとして渡されているオブジェクトで、第一引数のarticleインスタンスも、transientで定義した属性もハッシュの形で返してくれるもの。

そして、tansientで定義した属性を取り出したい時にevaluatorが必要になり、第二引数に置いて呼び出す。

参照

https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#with-callbacks

 

(article.rb)

FactoryBot.define do
factory :article do
sequence(:title) { |n| "title-#{n}" }
sequence(:slug) { |n| "slug-#{n}" }
category
end
 
trait :with_author do
transient do
sequence(:author_name) { |n| "test_author_name_#{n}" }
sequence(:tag_slug) { |n| "test_author_slug_#{n}" }
end

after(:build) do |article, evaluator|
article.author = build(:author, name: evaluator.author_name, slug: evaluator.tag_slug)
end
end
end

 

FactoryBot.define do
factory :author do
sequence(:name) { |n| "test_author_#{n}" }
sequence(:slug) { |n| "test_author_slug_#{n}" }
end
end

 

letで使用しているところ

let(:article_with_author) { create(:article, :with_author, author_name: '伊藤') }

 <流れ>

article_with_authorなので、記事と著者を作成する。

create(:article で、articleのtitleとslugを作成する。

:with_author でauthor_nameとtag_slugを作成。今回author_nameは指定されている伊藤になる。

after(:build)があるので、article.authorを作成する。そこに、transientで作成したものを代入する。

 

fill_in 

フォームへの入力をシュミレートする。