Wataruの技術備忘録

Rails学習での気づきや学びを記録していく備忘録的技術ブログです。

YouTubeとTwitterの埋め込み

実現したい内容

ブログアプリに投稿する記事にメディア(YoutubeTwitter)を埋め込めるようにする

  • 埋め込めるメディアとしてYouTubeTwitterをプルダウン選択出来る

  • Twitterを選択し、適切なURLが入力されたとき、ツイートが表示されるようにする

  • Youtubeを選択し、適切なURLが入力されたとき、動画が表示されるようにする

  • 埋め込まれた記事ブロックのヘッダーアイコンには、埋め込んだメディアのアイコンが表示される

ER図

f:id:voyag:20210117154830p:plain
ER図

  • EmbedモデルとSentenceモデル、MediaモデルはArticleBlockモデルとポリモーフィックで関連付けされている

Embedモデルにenumを設定する

class Embed < ApplicationRecord
  has_one :article_block, as: :blockable, dependent: :destroy
  has_one :article, through: :article_block

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

  validates :identifier, length: { maximum: 200 }

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

split_id_from_youtube_urlについては後述します。

embed_typeのプルダウン選択

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

Embed用のURLに変換

YouTubeのURLには、閲覧用のURLと埋め込み用のURLの2種類があり、共通しているのは、/以下は固有のIDが存在しているということです。 そこで、以下のメソッドを定義し、IDのみを取得します。

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

IDが取得できたら、埋め込み用のURLに加工します。

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

当初、src: "https://www.youtube.com/embed/#{embed.split_id_from_youtube_url}",の部分を
src: "https://www.youtube.com/embed/#{embed.identifier[17,11]}",としていましたが、
これだと開始位置を指定したURLの場合にIDをすべて取得できないという欠陥がありました。

Twitterの埋め込み表示

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

FontAwesomeのfaviconを指定

埋め込まれたメディアがyoutubetwitterによって、表示されるアイコンを切り替えます。

module ArticleBlockDecorator
  def box_header_icon
    if sentence?
      '<i class="fa fa-edit"></i>'.html_safe
    elsif medium?
      '<i class="fa fa-image"></i>'.html_safe
    elsif embed?
      blockable.youtube? ? '<i class="fa fa-youtube-play"></i>'.html_safe : '<i class="fa fa-twitter"></i>'.html_safe
    end
  end

アイコンには、FontAwesomeのfaviconを使用しています。

画像の表示サイズ、位置の調整

実装したい内容

  • 画像サイズ(横幅)を指定できるようにし、指定サイズで表示させる

  • 画像の横幅は100px〜700pxにする

  • 横幅が100px以下または700px以上ならば、バリデーションエラーを発生させる

  • 画像の表示位置を「左寄せ」「中央」「右寄せ」から選択出来るようにし、指定の位置で表示させる

アクティブストレージ

画像ファイルはモデル(Userモデルとします)に、has_one_attachedマクロで1対1の関係に設定します。

class User < ApplicationRecord
  has_one_attached :image
(略)

(参考:Active Storage の概要 - Railsガイド

画像の表示サイズの設定

Imageカラムの追加

画像ファイルのサイズを指定するために、Userモデルにimage_widthカラムを追加します。

$ rails g migration add_image_width_to_users
class AddImageWidthToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :image_width, :integer
  end
end
バリデーションの追加

画像の横幅は100px〜700pxの間にするので、バリデーションを設定しておきます。

class User < ApplicationRecord
(略)
  validates :image_width, numericality: { only_integer: true, greater_than_or_equal_to: 100, less_than_or_equal_to: 700 }
フロント周りの修正
.row
  .col-md-4
    .box.box-solid.box-info
      .box-header
        h3.box-title 情報
      = simple_form_for @user, url: user_path(@user.uuid) do |f|
        .box-body
          = f.input :image, as: :file
          - if @user.image.attached?
            = image_tag @user.image_url(:thumb), class: 'img-thumbnail'
            br
            br
          = f.input :image
.container.user-content
  h1.title = user.title
  - if user.image.attached?
    section.image
      = image_tag user.image_url(:lg), class: 'img-fluid', width: user.image_width
(略)
  • width: user.image_widthで画像の表示幅を入力した値に変更
controllerの修正

Usersコントローラーのuser_paramsに:imageを渡します。

(略)
def user_params
  params.require(:user).permit(:name, :email, :password, :password_confirm, :image)
end

画像の位置の設定

image_alignカラムの追加

画像の表示位置を指定するために、Userモデルにimage_alignカラムを追加します。

$ rails g migration add_image_align_to_users
class AddImageWidthToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :image_align, integer
  end
end
enumの設定
class User < ApplicationRecord
(略)
  enum image_align: { left: 0, center: 1, right: 2 }
(略)
ja:
  enums:
    user:
      image_align:
        left: '左寄せ'
        center: '中央揃え'
        right: '右寄せ'
フロント周りの修正
.row
  .col-md-4
    .box.box-solid.box-info
      .box-header
        h3.box-title 情報
      = simple_form_for @user, url: user_path(@user.uuid) do |f|
        .box-body
          = f.input :image, as: :file
          - if @user.image.attached?
            = image_tag @user.image_url(:thumb), class: 'img-thumbnail'
            br
            br
          = f.input :image
          = f.input_field :image_align, as: :radio_buttons #追加
.container.user-content
  h1.title = user.title
  - if user.image.attached?
    section class="image text-#{user.image_align}" #classを追加
      = image_tag user.image_url(:lg), class: 'img-fluid', width: user.image_width
(略)
controllerの修正

Usersコントローラーのuser_paramsに:image_alignを渡します。

(略)
def user_params
  params.require(:user).permit(:name, :email, :password, :password_confirm, :image, image_popsition)
end

Punditの使い方

インストール

Gem
gem "pundit"

Policies

Punditでは、app/policies配下に×××_policy.rbを作成し、管理する。
docに掲載されている例はこのようになっている。

class PostPolicy
  attr_reader :user, :post

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

  def update?
    user.admin? || post.published?
  end
end
  • Postはモデル名

  • user:Punditはコントローラー内でcurrent_user methodを読み込む。

  • model object(post):権限を確認したいmodel objectを定義する。

  • classはquery methodを実行する。(上記の例では、update?)controller actionに?をつけたmethod名で定義する。

  • ApplicationPolicy内ではmodel objectrecordと呼ばれる。

  • PolicyはApplicationPolicyを継承する。

class PostPolicy < ApplicationPolicy
  def update?
    user.admin? || record.published?
  end
end

Controller

def update
  @post = Post.find(params[:id])
  authorize @post
  if @post.update(post_params)
    redirect_to @post
  else
    render :edit
  end
end
ControllerとPolicyの挙動
  1. PostControllerでauthorizeが実行される。

  2. authorize methodはPostモデルに対応するPostPolicyクラスを探し、PostPolicyクラスをインスタンス化する。

  3. さらにインスタンス化したPostPolicyクラスにcurrent_userrecordを渡す。

  4. controllerのaction名(update)から、policyのupdate?が呼び出される。

authorizeはchainできる

def show
  @user = authorize User.find(params[:id])
end

# return the record even for namespaced policies
def show
  @user = authorize [:admin, User.find(params[:id])]
end

view側の設定

<% if policy(@post).update? %>
  <%= link_to "Edit post", edit_post_path(@post) %>
<% end %>

scope

特定のユーザーがアクセスしたときに、異なる種類の一覧を表示させたいような場合、Punditのpolicy scopeを呼び出すclassを定義できる。

class PostPolicy < ApplicationPolicy
  class Scope
    def initialize(user, scope)
      @user  = user
      @scope = scope
    end

    def resolve
      if user.admin?
        scope.all
      else
        scope.where(published: true)
      end
    end

    private

    attr_reader :user, :scope
  end

  def update?
    user.admin? || record.published?
  end
end
  • Scopeという名前のclassを定義し、policy classにネストさせる。

NoAuthorizedError

認可の判定がfalseであった場合、NoAuthorizedErrorが発生する。
イメージは以下の通り。

unless PostPolicy.new(current_user, @post).update?
  raise Pundit::NotAuthorizedError, "not allowed to update? this #{@post.inspect}"
end
rescue_fromメソッド

rescue_fromメソッドでエラーを補足する。

class ApplicationController < ActionController::Base
  include Pundit

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized
    render file: Rails.root.join('public/403.html'), status: 403
  end
end

また、application.rbに以下を記載し、403エラーページを表示させる。

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

403.htmlはpublicディレクトリに作成。

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

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

postgresqlでDBの作成ができなかった話

はじめに

Railsで新規アプリを作成した際に、DBの作成に失敗しました。
DBはPostgreSQLに指定して作成していたのですが、エラーの原因はPostgreSQLのバージョンの違いによるものでした。
今回は、その際の対処方法をまとめておきたいと思います。

環境

Rails new

$  rails new sample_app -d postgresql  
  • -d postgresqlでDBを指定

rails db:create

$  rails db:create

could not connect to server: No such file or directory
        Is the server running locally and accepting
        connections on Unix domain socket "/tmp/.s.PGSQL.5432"?
Couldn't create 'sample_app_development' database. Please check your configuration.
rails aborted!
PG::ConnectionBad: could not connect to server: No such file or directory
        Is the server running locally and accepting
        connections on Unix domain socket "/tmp/.s.PGSQL.5432"?
/~/sample_app/bin/rails:9:in `<top (required)>'
/~/sample_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
Tasks: TOP => db:create
(See full trace by running task with --trace)

上記のようなエラーが発生しました。

postgresql.log

エラーの原因を特定するために、PostgreSQLのログを確認することにしました。
ログは以下のディレクトリから確認することができます。
file:///usr/local/var/log/postgres.log

ログの内容はこのようになっていました。

FATAL:  database files are incompatible with server
DETAIL:  The data directory was initialized by PostgreSQL version 12, which is not compatible with this version 13.1.

データディレクトリがversion 12であるため、version 13.1との互換性がないことが原因でした。

PostgreSQLのバージョンアップ

バージョンアップの前に、サービスを停止しておきます。

$ brew services stop postgresql

PostgreSQLをバージョンアップします。

$ brew postgresql-upgrade-database

サービスを再起動します。

$ brew services restart postgresql 

再度、rails db:create

PostgreSQLのバージョンアップができたので、再度DBを作成してみます。

$ rails db:create
> Created database 'sample_app_development'
> Created database 'sample_app_test'

無事、DBを作成することができました。

ログを確認してみると、このようになっており、エラーが解消されました!

LOG:  starting PostgreSQL 13.1 on x86_64-apple-darwin19.6.0, compiled by Apple clang version 12.0.0 (clang-1200.0.32.27), 64-bit
LOG:  listening on IPv6 address "::1", port 5432
LOG:  listening on IPv4 address "127.0.0.1", port 5432
LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
LOG:  database system was shut down at 2021-01-06 01:03:11 JST
LOG:  database system is ready to accept connections

withinで範囲指定する#RSpec

idを振って、withinで範囲指定する

例えば、記事投稿アプリに複数の記事があり、それぞれに「編集」というリンクがあったとします。
このとき、

click_link '編集'

だと、どの記事の編集ボタンかが分かりません。
そこで、idを付与することで識別できるようにします。

tr.edit-preview-delete-area id="article-#{article.id}"

システムスペックではwithinで範囲指定します。

within("#article-#{waiting_article.id}") do
  click_link '編集'
end

Railsで定期実行を実現したい

はじめに

rakeタスクとcronを使って定期実行をする手順をまとめたいと思います。

実装すること

記事を投稿するアプリにて、
記事の公開状態が「公開待ち」のもので、「公開日時」が過去の日時になっているものを、1時間ごとに確認し、公開する。

cronとは

cronとは、多くのUNIX系OSで標準的に利用される常駐プログラム(デーモン)の一種で、利用者の設定したスケジュールに従って指定されたプログラムを定期的に起動してくれるもの。
(引用:cronとは - IT用語辞典 e-Words

UNIX系のOSに標準に備わっているものがcronですが、今回は、rubyの文法でcronの設定を扱える、「whenever」というgemを使って実装していきます。

実装の手順

  • rakeタスク作成

  • schedule.rbを作成し、rakeタスクを1時間ごとに実行

  • crontabコマンドでcronをアップデート

rakeタスクの作成

$ rails g task article_status
namespace :article_status do
  desc '公開待ちの記事の中で、公開日時が過去のものを公開する'
  task update_article_status: :environment do
    Article.publish_wait.find_each(&:published!)
  end
end
  • (&:published!)はブロックの代わりに使える書き方。こちらの記事でも簡単に説明しています。

bon-voyage23.hatenablog.com

rakeタスクの実行を確認

 bundle exec rake article_status:update_article_status

Gem wheneverの導入

GitHub - javan/whenever: Cron jobs in Ruby

gem 'whenever', require: false

$ bundle install
$ bundle exec wheneverize . #config/schedule.rbファイルの生成

# Rails.rootを使用するために必要
require File.expand_path(File.dirname(FILE) + "/environment")
# cronのログの吐き出し場所
set :output, "#{Rails.root}/log/cron.log"   
# cronを実行する環境変数 rails_env = ENV['RAILS_ENV'] || :development # cronを実行する環境変数をセット set :environment, rails_env every 1.hours do rake 'article_status:update_article_status' end

cronをアップデート

$ bundle exec whenever --update-crontab

参考

Railsで定期的にバッチ回す「Whenever」 - Qiita

Rakeタスクとは

はじめに

Railsで定型作業を行う際に、rakeコマンドを使えばタスクを実行できる。

Rakeタスクとは

スクランナーであるRakeが実行する処理。 RakeはRubyで記述されたビルドツールのこと。

タスクの作成

desc "Print reminder about eating more fruit."

task :apple do
  puts "Eat more apples!"
end

desc "Print ...":タスクの一覧を表示(rake -T)した際に表示される。どのようなタスクかという説明文を設定できる。

task :apple ...:タスクの名称(apple)とタスクとして実行する処理を記述。上記の例では、appleタスクを実行することで、Eat more apples!が出力される。

rake apple
# Eat more apples!

(参考:What is Rake in Ruby & How to Use it - RubyGuides