20200814のRubyに関する記事は17件です。

【Rails】ハッシュや配列に特定要素が存在するか判別 [ActiveRecord] [has_attribute?]

ハッシュや配列の中に特定の要素が存在するか確認する方法です。
emailが登録されていなかったら登録、されていたらメッセージ、みたいなイメージです。

任意のハッシュを判別

このようなハッシュを例にします。
nil empty に注目です。

@user = {
 "id" => 1,
 "name" => "Taro",
 "age" => 26,
 "from" => nil,
 "sex" => "",
 "created_at" => Sun, 21 Aug 2020 09:55:05,
 "updated_at" => Sun, 21 Aug 2020 09:55:05
}

has_attribute?で判別

特定の要素自体があるかどうか判別します。
has_attribute?でこのような結果が得られます。

@user.has_attribute?(:name) # => true
@user.has_attribute?(:from) # => true
@user.has_attribute?(:sex) # => true
@user.has_attribute?(:userid) # => false

少しややこしいのが、中身が nil empty だとしても結果はtrueというところです。
要素自体があるかないかの判別ということです。

attribute_present?で判別

特定の要素の中身が存在するか判別します。
attribute_present?でこのような結果が得られます。

@user.attribute_present?(:name) # => true
@user.attribute_present?(:from) # => false
@user.attribute_present?(:sex) # => false
@user.attribute_present?(:userid) # => エラー

要素は存在する前提で、中身が空かどうか判別します。

参考

ActiveRecordやハッシュ・配列などで使えるメソッドは他にもたくさんありますよー
https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods.html

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

assign_attributes で中間テーブルが保存されてしまう

  • 入力フォームから値を受け取り、バリデーションを行ってから保存したい、みたいな時に一旦インスタンスの変数を更新するために assign_attributes を利用したら思わぬ結果になった
  • rails の中間テーブルにおいては、assign_attributes 実行時にDBインサートが走り、コミットまでされてしまう
    • そのため、modelのバリデーションエラーで保存に失敗した際に、中間テーブルが保存されっぱなしの状態になってしまった
    • 以下のように理解していたが、これは正確ではなさそう
      • update_attributes はインスタンス変数の更新を行い、DBの更新まで行う
      • assign_attributes はインスタンス変数の更新を行うが、DBの更新は行わない
  • つまり、assign_attributes してからバリデーションにかかったり、何らかの理由で保存されたくない時は明示的にロールバックするか、先にインサートしないという工夫が必要になる

ユースケース

  • 以下のようなユースケースを想定
    • UserGroup が多対多で、中間テーブルのモデルとして GroupUsers が存在
    • User を保存する際に、同時に GroupUsers も保存したい
    • view から受け取ったパラメータをセットし保存を行うが、バリデーションエラーの時は保存せずエラーを返す
groups.rb
class Group < ApplicationRecord
  has_many :group_users
  has_many :users, through: :group_users
end
users.rb
class User < ApplicationRecord
  has_many :group_users
  has_many :groups, through: :group_users

  validates :name, presence: true
end
group_users.rb
class GroupUsers < ApplicationRecord
  belongs_to :group
  belongs_to :user
end
  • User に以下のフィールドが生えるが、そのフィールドに値をセットするとインサートが走ってしまう
    • groups
    • group_ids
    • group_users
    • group_user_ids

案1 transaction を張る

  • assign_attributes から save までを transaction で囲んで、保存できない時はロールバックするようにする
users_controller.rb
class UsersController < ApplicationController
  def update
    @user = Users.new

    begin
      ActiveRecord::Base.transaction do
        @user.assign_attributes(user_params)     #ここで一旦中間テーブルに保存されるが、save!でエラーならロールバックされる
        @user.save!
      end
      p "更新成功"
    rescue
      p "更新失敗"
    end
  end
end

案2 after_save を利用する

  • after_save は、データベースへの COMMIT の直前に実行される
    • つまり、model のバリデーションなどを実行後に、そのメソッドを呼び出してくれる
  • 自動で生えるリレーション用のフィールドとは別の attribute 名で view からパラメータを送り、model のバリデーション後にそのパラメータの値を保存したい中間テーブルの attribute にセットして保存することで、バリデーション通過後に中間テーブルが保存されるようにする
    • assign_attributes 実行時にはリレーションを意味するフィールドには何もセットせず、after_save で実行するメソッドの中で、値をセットするようにする
users.rb
class User < ApplicationRecord
  has_many :group_users
  has_many :groups, through: :group_users

  validates :name, presence: true

  after_save :save_group_user

  attr_accessor :g_ids

  # User のバリデーション実行後、コミットする直前に呼ばれる
  def save_group_user
    self.group_ids = @g_ids
  end
end
  • model には after_save を定義し、実行するメソッドの中で中間テーブル用の group_ids をセットするようにしておく
  • また view からきたパラメータをセットしておく用の attribute として attr_accessor :g_ids を定義しておく
  • view からは g_ids のように適当なフィールド名でパラメータを送り、after_save のメソッドで group_ids にセットすることで中間テーブルを保存する
users_controller.rb
class UsersController < ApplicationController
    def update
      @user = Users.new
      @user.assign_attributes(user_params)
      @user.save ? (redirect_to root_path notice: '更新成功') : (render :edit)
    end
end
  • コントローラーでは普通に save を実行して保存すれば良い

ちなみに

  • 新規作成時など、User が id フィールドが nil の状態であれば、中間テーブル用のフィールドに値をセットしても自動でインサートが走ることはないので、あくまでも更新時に気をつければいいっぽい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vision API(トレーニング済み機械学習モデル)を使ってRubyでテキスト抽出(OCR)をためしてみた

Vision APIとは?

Google Cloud の Vision API は REST API や RPC API を介して強力な事前トレーニング済み機械学習モデルを提供します。画像にラベルを割り当てることで、事前定義済みの数百万のカテゴリに画像を高速に分類できます。オブジェクトや顔を検出し、印刷テキストや手書き入力を読み取り、有用なメタデータを画像カタログに作成します。
(公式ドキュメントより引用)

分析にかけた画像

アサインナビ トップページのキャプチャ画像(PNG形式)

anavi.png

分析結果

$ bundle exec rake cloud_vision:text['app/assets/images/anavi.png']
assign navi
新しい働き方
案件/人材を探す
ご利用ガイド
サービスについて
ログイン
会員登録
IT案件、外部人材との出会いを
テクノロジーで効率化
00

かなり高精度で、テキスト抽出できていますね (^^)

今回作成したコード

lib/tasks/cloud_vision.rake
## 呼び出し方
## $ bundle exec rake cloud_vision:text[image_file] # image_fileに'ここに画像ファイルのパス'

# Google Cloud client libraryの読み込み
require "google/cloud/vision"

namespace :cloud_vision do
  desc 'Run OCR.'
  task :text, [:image_file] do |task, args|
    return unless args[:image_file]

    # クライアントをインスタンス化
    image_annotator = Google::Cloud::Vision.image_annotator

    # OCRの実行
    response = image_annotator.text_detection(
      image: args[:image_file],
      max_results: 1 # optional, defaults to 10
    )

    # OCR結果の表示 
    response.responses.each do |res|
      res.text_annotations.each do |text|
        puts text.description
      end
    end
  end
end

環境変数はdotenvに記載

#.envファイル
GOOGLE_CLOUD_PROJECT="ここにプロジェクトIDを記載"
GOOGLE_APPLICATION_CREDENTIALS="Vision APIにアクセスする認証キーjsonファイルへのパスを記載"

セットアップ手順

セットアップ手順は近日中に追記させて頂きます

参考文献

Using the Vision API with Ruby

フォローや記事へのLGTMは、日々の投稿の励みになります。
温かい1クリックのほど、よろしくお願いします。 m(_ _)m

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

Invalid route name, already in use: 'new_user_session'のエラー解決方法

環境

Mac OS Catalina
Ruby 2.7.1
Rails 6.0.3.2

現象

ログイン機能用にdeviseを導入しrouteを設定。
その後、Rails routesを実行すると下記のエラーが出た。

Invalid route name, already in use: 'new_user_session'  (ArgumentError)
You may have defined two routes with the same name using the `:as` option, or you may be overriding a route already defined by a resource with the same naming. For the latter, you can restrict the routes created with `resources` as explained here: 
https://guides.rubyonrails.org/routing.html#restricting-the-routes-created

どうやら、'new_user_session'という名前のルートが既に使われているぞ!と言ってる様子。

現象発生時のルートは下記の通り。

routes.rb
Rails.application.routes.draw do
  devise_for :users
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html

  root to: 'toppages#index'

  resources :users, only: [:index, :show, :edit, :update, :destroy] do
    member do
      get :followings
      get :followers
      get :likes
    end
  end

  devise_for :users,
    path: '',
    path_names: {
      sign_up: '',
      sign_in: 'login',
      sign_out: 'logout',
      registration: 'signup'
    },
    controllers: {
      registrations: 'users/registrations',
      sessions: 'users/sessions',
      passwords: 'users/passwords'
    }
  devise_scope :user do
    get 'signup', to: 'users/registrations#new'
    get 'login', to: 'users/sessions#new'
    get 'logout', to: 'users/sessions#destroy'
  end

  resources :posts, only: [:create, :destroy] do
    collection do
      get :search
    end
  end

  resources :relationships, only: [:create, :destroy]
  resources :favorites, only: [:create, :destroy]

end

解決方法

「2箇所定義とかしてないのにな〜」とか考えていましたが、ふと一番上に行が追加されていることに気づきましたw
devise_for :users の行を削除することで解決。

routes.rb
Rails.application.routes.draw do
  root to: 'toppages#index'

  resources :users, only: [:index, :show, :edit, :update, :destroy] do
    member do
      get :followings
      get :followers
      get :likes
    end
  end

  devise_for :users,
    path: '',
    path_names: {
      sign_up: '',
      sign_in: 'login',
      sign_out: 'logout',
      registration: 'signup'
    },
    controllers: {
      registrations: 'users/registrations',
      sessions: 'users/sessions',
      passwords: 'users/passwords'
    }
  devise_scope :user do
    get 'signup', to: 'users/registrations#new'
    get 'login', to: 'users/sessions#new'
    get 'logout', to: 'users/sessions#destroy'
  end

  resources :posts, only: [:create, :destroy] do
    collection do
      get :search
    end
  end

  resources :relationships, only: [:create, :destroy]
  resources :favorites, only: [:create, :destroy]

end

無事、起動することができました!

最後に

already in use系のエラーは、今回と同様に同じルーティングが既に設定されていると考えて良さそうですね。
(そのままですが)
今後同じエラーが出た際は、サクッと解決できそう。

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

Railsのテンプレート機能(ERB)をController(Action View)以外で使う

概要

Railsは、テンプレートをアクションで紐付けてくれるので便利ですよね。
Action ViewとERB (Embedded Ruby)ってやつです。
たまに、そのテンプレートを、他の箇所で使いたいことがあります。
調べましたがあまり情報が出てこなかったり、見つけたと思ったら動かなかったりしたので解決した方法を備忘として投稿します。

動作環境

ruby 2.6.5
rails 5.2.3

サンプル

例えばJobとかで利用。

app/views下(任意の場所可)にtemplateを作成。

sample.html.erb
<%= user_name %>さんへ

これはサンプルです。

次にJob。

send_template_job.rb
class SendTemplateJob < ApplicationJob
  queue_as :default

  def perform(*args)
    # テンプレートを使う
    content = ActionView::Base.new('app/views').render(file: 'sample',
                                                       locals: { user_name: current_user.name },
                                                       layout: false)
    # テンプレートを利用して生成した文字列をどこかに送る処理を記載
    # 例えばslackとか

  end
end

こんな感じで書くと、erb内のuser_nameて定義してある変数が文字列として展開され取得できます。

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

[ruby]ドリルアウトプット

内容

問題.1
名前を入力する機能を作成しましょう。
名前以外にピリオド(.)や空白があるとエラーを表示させます。

※わからない場合はAPIを利用して問題を解きましょう。
参考URL: https://docs.ruby-lang.org/ja/search/

ヒント
include?メソッドを使いましょう。

include?
include?メソッドは指定した要素が、配列や文字列内に含まれているかを判定するメソッドです。
image.png
スクリーンショット 2020-08-14 16.50.22.png

実装

image.png

解説

strに(.)か( )があったら登録できないように、include?で条件分岐させればOKです。

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

【AWS Lambda】S3に保存された画像をリサイズして別S3に保存(Ruby利用)

目標

S3に保存された画像データをLambda関数を利用してリサイズし、別S3に保存する。
プログラミング言語はRubyを利用しています。

はじめに

あまりLambdaに関して詳しくないので、基本的にはLambdaの基本動作を手順として残すのが目的です。
AWSコンソールからS3にプットした画像をリサイズ(固定サイズ)して別S3に保存するという単純な処理を試してみました。

前提

事前に2つのS3バケットを作成していること。

構成図

Untitled Diagram.png

作業の流れ

項番 タイトル
1 Lambda関数の作成とアップロード
2 Lambda環境変数の設定
3 ハンドラーと実行ロールの設定
4 イベントトリガーの定義
5 実行ロールの設定
6 動作検証

手順

1.Lambda関数の作成とアップロード

任意の関数名とLambda関数で利用するプログラミング言語を選択します。
今回はRuby2.5で記述します。
image.png

配備するLambda関数本体と外部ライブラリ(mini_magick)をzip化したものを作成します。
作業ディレクトリ上で以下操作を行います。

まずはGemfileの作成

Gemfile
source 'https://rubygems.org'
gem "mini_magick"

bundle install --path vendor/bundleを利用して作業ディレクトリ配下にgemをインストールします。

bundle install --path vendor/bundle

更にLambda関数本体となるhandler.rbを作成します。

handler.rb
require 'aws-sdk-s3'
require 'base64'
require 'mini_magick'

def resize_image(event:, context:)
  s3_client = Aws::S3::Client.new(
                           :region => ENV['REGION'],
                           :access_key_id => ENV['ACCESS_KEY'],
                           :secret_access_key => ENV['SECRET_ACCESS_KEY']
                          )

  # イベントソースとして指定したS3から保存された画像を取り込む
  key = event['Records'][0]['s3']['object']['key']
  image_file = s3_client.get_object(:bucket => ENV['BUCKET_BEFORE'], :key => key).body.read
  image = MiniMagick::Image.read(image_file)

  # リサイズした画像をLambda環境の/tmpに一時書き込み
  resized_tmp_file = "/tmp/#{key.delete("images/")}"
  image.resize("300x300").write(resized_tmp_file)

  # アップロード実行
  s3_resource = Aws::S3::Resource.new()
  object = s3_resource.bucket(ENV['BUCKET_AFTER']).object(key).upload_file(resized_tmp_file)
end

handler.rbvenderをzip化します。

image.png

作成したzipファイルのアップロードを行います。
Lambdaコンソール上の関数コード欄アクションから.zipファイルをアップロードをクリック
tempsnip.png

handler.zipを選択し、保存

tempsnip.png

2.Lambda環境変数の設定

Lambdaコンソール上からLambda関数内で利用する環境変数を設定します。

tempsnip.png

3.ハンドラーと実行ロールの設定

どのファイルのどの関数を呼び出すか…という設定は、ハンドラーというパラメータで指示しているとのことなので、
今回作成したLambda関数に合わせて内容を編集します。

Lambdaコンソール上の基本設定の編集をクリック
tempsnip.png

ハンドラ欄に実行ファイル名(拡張子除く).関数名の形式で値を入力し保存します。
tempsnip.png

4.イベントトリガーの定義

Lambdaコンソール上からトリガーを追加をクリック
tempsnip.png

今回イベントトリガーとするAWSサービスはS3
tempsnip.png

トリガーの詳細設定を行います。
今回はimagesディレクトリ内にオブジェクトが作成されたタイミングでのイベント起動とします。
tempsnip.png

トリガーの有効化をいれることでS3がこのLambda関数に処理をキックすることが可能となります(Lambda関数ポリシーの設定)。
最後に追加をクリック

tempsnip.png

5.実行ロールの設定

更に今回はリサイズした画像を保存する際にLambdaからS3にアクセスをかける必要があるため、
S3へのアクセス権限付きの実行ロールを付与します。

Lambdaコンソール上部にあるアクセス権限をクリック後、実行ロール名をクリック

tempsnip.png

ポリシーをアタッチしますをクリック
tempsnip.png

AmazonS3FullAccessを選択後、ポリシーのアタッチ
tempsnip.png

6.動作検証

イベントソースとして指定したS3のimagesディレクトリ内に任意の画像をアップロードします。
image.png

ちなみにアップロードした画像はこちら
image.png

リサイズ後の画像保存先として指定したS3を確認したところ、画像が保存されていたのでOKです。
image.png

中身もリサイズされていました。
image.png

LambdaのログはCloudwatch Logsから確認可能です。
image.png

参考にさせて頂いた記事

AWS Lambda Ruby を利用して 1時間で作る画像のリサイズ API

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

【AWS Lambda】S3に保存された画像をリサイズして別S3に保存

目標

S3に保存された画像データをLambda関数を利用してリサイズし、別S3に保存する。

はじめに

あまりLambdaに関して詳しくないので、基本的にはLambdaの基本動作を手順として残すのが目的です。
AWSコンソールからS3にプットした画像をリサイズ(固定サイズ)して別S3に保存するという単純な処理を試してみました。

前提

事前に2つのS3バケットを作成していること。

構成図

Untitled Diagram.png

作業の流れ

項番 タイトル
1 Lambda関数の作成とアップロード
2 Lambda環境変数の設定
3 ハンドラーと実行ロールの設定
4 イベントトリガーの定義
5 実行ロールの設定
6 動作検証

手順

1.Lambda関数の作成とアップロード

任意の関数名とLambda関数で利用するプログラミング言語を選択します。
今回はRuby2.5で記述します。
image.png

配備するLambda関数本体と外部ライブラリ(mini_magick)をzip化したものを作成します。
作業ディレクトリ上で以下操作を行います。

まずはGemfileの作成

Gemfile
source 'https://rubygems.org'
gem "mini_magick"

bundle install --path vendor/bundleを利用して作業ディレクトリ配下にgemをインストールします。

bundle install --path vendor/bundle

更にLambda関数本体となるhandler.rbを作成します。

handler.rb
require 'aws-sdk-s3'
require 'base64'
require 'mini_magick'

def resize_image(event:, context:)
  s3_client = Aws::S3::Client.new(
                           :region => ENV['REGION'],
                           :access_key_id => ENV['ACCESS_KEY'],
                           :secret_access_key => ENV['SECRET_ACCESS_KEY']
                          )

  # イベントソースとして指定したS3から保存された画像を取り込む
  key = event['Records'][0]['s3']['object']['key']
  image_file = s3_client.get_object(:bucket => ENV['BUCKET_BEFORE'], :key => key).body.read
  image = MiniMagick::Image.read(image_file)

  # リサイズした画像をLambda環境の/tmpに一時書き込み
  resized_tmp_file = "/tmp/#{key.delete("images/")}"
  image.resize("300x300").write(resized_tmp_file)

  # アップロード実行
  s3_resource = Aws::S3::Resource.new()
  object = s3_resource.bucket(ENV['BUCKET_AFTER']).object(key).upload_file(resized_tmp_file)
end

handler.rbvenderをzip化します。

image.png

作成したzipファイルのアップロードを行います。
Lambdaコンソール上の関数コード欄アクションから.zipファイルをアップロードをクリック
tempsnip.png

handler.zipを選択し、保存

tempsnip.png

2.Lambda環境変数の設定

Lambdaコンソール上からLambda関数内で利用する環境変数を設定します。

tempsnip.png

3.ハンドラーと実行ロールの設定

どのファイルのどの関数を呼び出すか…という設定は、ハンドラーというパラメータで指示しているとのことなので、
今回作成したLambda関数に合わせて内容を編集します。

Lambdaコンソール上の基本設定の編集をクリック
tempsnip.png

ハンドラ欄に実行ファイル名(拡張子除く).関数名の形式で値を入力し保存します。
tempsnip.png

4.イベントトリガーの定義

Lambdaコンソール上からトリガーを追加をクリック
tempsnip.png

今回イベントトリガーとするAWSサービスはS3
tempsnip.png

トリガーの詳細設定を行います。
今回はimagesディレクトリ内にオブジェクトが作成されたタイミングでのイベント起動とします。
tempsnip.png

トリガーの有効化をいれることでS3がこのLambda関数に処理をキックすることが可能となります(Lambda関数ポリシーの設定)。
最後に追加をクリック

tempsnip.png

5.実行ロールの設定

更に今回はリサイズした画像を保存する際にLambdaからS3にアクセスをかける必要があるため、
S3へのアクセス権限付きの実行ロールを付与します。

Lambdaコンソール上部にあるアクセス権限をクリック後、実行ロール名をクリック

tempsnip.png

ポリシーをアタッチしますをクリック
tempsnip.png

AmazonS3FullAccessを選択後、ポリシーのアタッチ
tempsnip.png

6.動作検証

イベントソースとして指定したS3のimagesディレクトリ内に任意の画像をアップロードします。
image.png

ちなみにアップロードした画像はこちら
image.png

リサイズ後の画像保存先として指定したS3を確認したところ、画像が保存されていたのでOKです。
image.png

中身もリサイズされていました。
image.png

LambdaのログはCloudwatch Logsから確認可能です。
image.png

参考にさせて頂いた記事

AWS Lambda Ruby を利用して 1時間で作る画像のリサイズ API

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

Ruby 二次元配列とは?

二次元配列とは?

二次元配列とは、配列を要素に持つ配列のこと。

# 例:
number_arrays = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
color_arrays = [["red", "yellow", "blue"], ["white", "black", "gray"]]

二次元配列を使用することで、二次元(平面)のデータを表すことができる。

例:マス目の状態(◯ or ✕)を二次元配列で表現する

001.jpeg

# ◯:1, ✕:0 とする
arrays = [
  [1, 1, 0, 0],
  [1, 1, 1, 1],
  [0, 1, 1, 1],
  [1, 1, 0, 1],
  [1, 0, 1, 1]
]

二次元配列の使い方

二次元配列の作成

二次元配列は通常の配列と同じように作成できる。

# 直接定義して作成
arrays = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# 空の配列arraysを定義し、その中に配列を追加して作成
arrays = []
arrays << [1, 2, 3]
arrays << [4, 5, 6]
arrays << [7, 8, 9]

arrays #=> [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

値の取得

二次元配列内の配列を取得するには

変数名[取得する配列のインデックス番号]

のように記述する。

arrays = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

arrays[0] #=> [1, 2, 3]
arrays[1] #=> [4, 5, 6]
arrays[2] #=> [7, 8, 9]

また、二次元配列内の配列の各要素を取得するには

変数名[取得する配列のインデックス番号][取得する要素のインデックス番号]

のように記述する。

arrays = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

arrays[0][0] #=> 1
arrays[0][1] #=> 2
arrays[0][2] #=> 3
arrays[1][0] #=> 4
arrays[1][1] #=> 5
arrays[1][2] #=> 6
arrays[2][0] #=> 7
arrays[2][1] #=> 8
arrays[2][2] #=> 9

二次元配列用のメソッド

二次元配列用のメソッドもある。

transpose

transposeメソッドを使用すると、二次元配列の行と列を入れ替えた二次元配列を作成できる。

# 例:

arrays = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# arraysを平面で表すと以下のようになる
# arrays = [
#   [1, 2, 3],
#   [4, 5, 6],
#   [7, 8, 9]
# ]

transposed_arrays = arrays.transpose
transposed_arrays #=> [[1, 4, 7], [2, 5, 8], [3, 6, 9]]

# transposed_arraysを平面で表すと以下のようになる
# transposed_arrays = [
#   [1, 4, 7],
#   [2, 5, 8],
#   [3, 6, 9]
# ]

flatten

flattenメソッドを使うと、二次元配列を平坦化(中の配列をバラす)した配列を作成できる。

# 例:

arrays = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened_arrays = arrays.flatten
flattened_arrays #=> [1, 2, 3, 4, 5, 6, 7, 8, 9]

まとめ

  • 二次元配列とは、配列を要素に持つ配列のこと
  • 二次元配列を使うと平面のデータを表現できる
  • 二次元配列内の配列や値はインデックス番号を指定することで取得できる
  • transpose, flattenなどの二次元配列に使用できるメソッドもある
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby 文字列の中から文字を抽出 sliceメソッド

sliceメソッドとは?

sliceメソッドとは、文字列の中から引数で指定した条件の文字を抜き出すメソッド。

# 例:文字列 "Hello world" の頭から2文字目の "e" を抜き出す
string = "Hello world"
string.slice(1) #=> "e"

# 引数に該当する文字が見つからなければnilを返す
string.slice(11) #=> nil

sliceメソッドの使い方

sliceメソッドは引数に、抜き出す文字の条件指定する。
引数の指定は以下の様な方法がある。

文字列の先頭 or 後ろから何文字目かを指定

文字列の先頭から何文字目かを指定する。番号は0から始まるのに注意(先頭の文字が0、先頭から2番目が1)

string = "Hello world"

# 先頭から何文字目かを指定、先頭の番号は0
string.slice(0) #=> "H"
string.slice(1) #=> "e"
string.slice(2) #=> "l"
string.slice(3) #=> "l"
string.slice(4) #=> "o"

マイナスの値を指定することで、後ろから何文字目かを指定することもできる。その場合、一番後ろの文字が-1、後ろから2番目が-2のように数えられる。

string = "Hello world"

# 後ろから何文字目かを指定、一番後ろの番号は-1
string.slice(-5) #=> "w"
string.slice(-4) #=> "o"
string.slice(-3) #=> "r"
string.slice(-2) #=> "l"
string.slice(-1) #=> "d"

文字列の先頭 or 後ろから何番目かと、そこから何文字かを指定

文字列の中の範囲を指定して文字を抽出する。始まりの位置と、そこから何文字抽出するかを指定する。この場合も、番号は0から始まる。

string = "Hello world"

# 先頭(0番目)から5文字抽出
string.slice(0,5) #=> "Hello"

# 6番目から5文字抽出
string.slice(6,5) #=> "world"

始まりの位置にマイナスの値を指定することで、後ろから何文字目かを指定することもできる。この場合も一番後ろの文字が-1、後ろから2番目が-2のように数えられる。

string = "Hello world"

# -11番目から5文字抽出
string.slice(-11,5) #=> "Hello"

# -5番目から5文字抽出
string.slice(-5,5) #=> "world"

抽出したい文字を直接指定

特定の文字を指定して抽出する。

string = "Hello world"

# "H"を指定
string.slice("H") #=> "H"

# "Hello"を指定
string.slice("Hello") #=> "Hello"

# 含まれていない文字を指定するとnilを返す
string.slice("hello") #=> nil

正規表現で指定

抽出したい文字を正規表現で指定する。

string = "Hello world"

# 正規表現で「wから始まってdで終わる文字」を指定
string.slice(/w.*d/) #=> "world"

範囲演算子で範囲を指定

範囲演算子( .. , ... )を使って文字列内の範囲を指定し、文字を抽出する。この場合も、番号は0から始まる。

string = "Hello world"

# 先頭(0番目)から4番目まで抽出
string.slice(0..4) #=> "Hello"

# ...にすると範囲の終わりの文字を含まない
string.slice(0...4) #=> "Hell"

sliceメソッドとslice!メソッドの違い

slice!というメソッドもあるが、こちらもsliceメソッドと同じように文字列の中から文字を抽出して返すメソッドである。引数の指定方法もsliceメソッドと同じ。ただし、slice!メソッドを使うと、抽出した文字は元の文字列から取り除かれる。

string = "Hello world"

# sliceメソッドを使用しても元のstringは変わらない
string.slice(0..4) #=> "Hello"
string #=> "Hello world"

# slice!メソッドを使用すると元のstringから文字列が取り除かれる
string.slice!(0..4) #=> "Hello"
string #=> " world"

まとめ

  • sliceメソッドは文字列から文字を抽出するメソッド
  • 引数に指定した条件で文字を抽出する
  • 引数は文字の順番や正規表現、範囲で指定できる
  • slice!メソッドは元の文字列から文字を取り除く
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Active Recordのjoinsメソッドで連結したテーブルから一意なレコードを取得する(Rails Tutorial第14章)

はじめに

Railsチュートリアルに取り込む中で教材で触れられていないバグに遭遇した。
バグの原因追及と解決方法をまとめる。

この記事で分かること

・Active Recordの『joins』メソッドによる連結されたテーブルのカラム名の確認方法
・重複したレコードを一意にして取得する『distinct』メソッドの使用方法

参考資料

・Railsチュートリアル
 https://railstutorial.jp/
・Railsガイド Active Record の関連付け
 https://railsguides.jp/association_basics.html
・【Rails】データベースの中身を確認する方法【Cloud9】
 https://shuheitakada.com/rails-database-check
・SELECT文の結果を表示する時にカラム名をヘッダーとして表示(.headersコマンド)
 https://www.dbonline.jp/sqlite/sqlite_command/index5.html
・Rails distinctメソッドについて
 https://qiita.com/toda-axiaworks/items/ad5a0e2322ac6a2ea0f4

環境について

Railsチュートリアル第6版に従う
・cloud9使用
・ruby 2.6.3p62
・Rails 6.0.3

学習動機

Rails Tutorial第14章 14.3.3サブセレクト 演習3にてバグが生じる。
演習の目的はjoinsメソッドを使用してフォローしたユーザーと自分自身に紐づくマイクロポストをフィードとして表示させることである。
一見いい感じに見えるが、
image.png

マイクロポストを投稿すると、
image.png

どう考えてもenjoyしすぎである。
明らかにenjoyしてない気持ちを抑えつつ原因の究明をする。

モデルの関連付

各モデルの関連付は以下の通り。

class User < ApplicationRecord
 has_many :microposts
 has_many :passive_relationships, class_name: "Relationship", foreign_key: "followed_id"      
 has_many :followers, through: :passive_relationships, source: :follower
end

class Relationship < ApplicationRecord
 belongs_to :follower, class_name: "User"
end

class Micropost < ApplicationRecord
 belongs_to :user
end

# Copyright (c) 2016 Michael Hartl

また、各テーブルの構成は以下の通り。
Rails Tutorial14章joinsの関連図.png

原因となったコード

models/user.rb
def feed
 part_of_feed = "relationships.follower_id = :id or microposts.user_id = :id"
 Micropost.joins(user: :followers).where(part_of_feed, { id: id })
end

# Copyright (c) 2016 Michael Hartl

まずは、コンソールに入り、Railsにより作成されるSQL 文を確認する。

rails_console
>>user.feed

SELECT * FROM "microposts" INNER JOIN "users" ON "users"."id" = "microposts"."user_id" 
   INNER JOIN "relationships" ON "relationships"."followed_id" = "users"."id" 
   INNER JOIN "users" "followers_users" ON "followers_users"."id" = "relationships"."follower_id" 
   WHERE (relationships.follower_id = 1 or microposts.user_id = 1) 
   ORDER BY "microposts"."created_at" DESC;

次に、データベースのコンソールにてjoinsメソッドにより作成されたテーブルの構造とデータを確認する。

rails_dbconsole
#SELECTの結果でカラム名を表示させる
>>.header on

>>SELECT * FROM "microposts" INNER 
  JOIN "users" ON "users"."id" = "microposts"."user_id" 
  INNER JOIN "relationships" ON "relationships"."followed_id" = "users"."id" 
  INNER JOIN "users" "followers_users" ON "followers_users"."id" = "relationships"."follower_id" 
  WHERE (relationships.follower_id = 1 or microposts.user_id = 1) 
  ORDER BY "microposts"."created_at" DESC LIMIT 5;

#検索結果 |~|はカラムの省略の意
id|content|user_id|~|id|name|~|id|follower_id|followed_id|~|id|name|~|
308|Enjoy Coding !!|1|~|1|Example User|~|50|4|1|~|4|Mr. Rey Lemke|~|
308|Enjoy Coding !!|1|~|1|Example User|~|51|5|1|~|5|Dr. Louisa Price|~|
308|Enjoy Coding !!|1|~|1|Example User|~|52|6|1|~|6|Charisse Stamm|~|
308|Enjoy Coding !!|1|~|1|Example User|~|53|7|1|~|7|Sang Metz IV|~|
308|Enjoy Coding !!|1|~|1|Example User|~|54|8|1|~|8|Robt Hamill|~|

どうやら、follower_idによりMicropostsにダブりが生じていることが分かる。
ちなみにMicropostのダブりの数と自身のFollowersの数が一致していることからも、
Followersがダブりの原因になっていると推測できる。

つまり、下の様なフォロー関係の場合、
  id:1 田中 → id:2 鈴木
  id:2 鈴木 → id:1 田中
  id:3 佐藤 → id:1 田中
  id:3 佐藤 → id:2 鈴木
下記図の様なテーブルが作成される。
Micopost.whereにより、みどり枠とあか枠の部分が抽出され、あか枠部分がダブりとなっている。
RailsTuorial14章Joinsで作成されるテーブルの模式図.png

models/user.rb
 #みどり枠部分
 relationships.follower_id = :id: 

 #あか枠部分
 microposts.user_id = :id

解決策

原因が分かるまで結構な時間を使ってしまったけれど、解決方法が分かるとたったの1語で解決できる。

distinctメソッドを使う

重複のない一意性のあるレコードを取得するためには、『distinct』メソッドを使用するとのこと。

早速組み込んでみる。

models/user.rb
def feed
 part_of_feed = "relationships.follower_id = :id or microposts.user_id = :id"
 Micropost.joins(user: :followers).where(part_of_feed, { id: id }).distinct
end

#Copyright (c) 2016 Michael Hartl

image.png
OK!!

テストを書く

今こそテストを書く時。
ということでFeedに自分が投稿したマイクロポストの重複がないことをテストする。

test/integrationtest/microposts_interface_test.rb
 def setup
   @user = users(:michael)
 end 

 test "should feed have microposts with uniqueness" do
   log_in_as(@user)
   get root_path

   # マイクロポストの投稿
   content = "This micropost is only one!"
   post microposts_path, params: { micropost: { content: content }}
   follow_redirect!

   # feedのダブりの確認
   assert_select 'span.content', { :count=>1, :text=> "#{content}" }
 end

#Copyright (c) 2016 Michael Hartl
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CarrierWaveを使い、複数の画像を一斉に投入する2つの方法/①seedの初期データで投入/②CSVでインポートする

他のtextカラムは簡単に複数投入できるのですが、image(画像)カラムだけはいずれもnilになってしまい苦労したので、ここに複数の画像カラムを一斉に投入する2つの方法を記しておきます。

①複数の画像カラムをseedの初期データで投入する方法

db/migrate/seed.rbに、

seed.rb
Post.create!(image: /public/uploads/post/image/1/yatoguti.jpg,
park: 谷戸口公園,
outline: 程久保の知る人ぞ知る,
location: 東京都日野市程久保1丁目20−14,
access: モノレール程久保駅から徒歩5)

のように記載し、rails db:resetしてから、rails db:seedするが、

seed.rb
=> [#<Post:0x00007fa6c66bc8e8
  id: 27,
  image: nil,
  park: "谷戸口公園",
  outline:
   "程久保の知る人ぞ知る。",
  location: "京王線高幡不動駅より百草園駅方面へ徒歩10分",
  access: "日野市高幡1024番地",
  created_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  updated_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  likes_count: nil,
  tag_list: ["#水遊び", "#アスレチック"]>,
 #<Post:0x00007fa6c7b48f28
  id: 28,
  image: nil,
  park: "雨乞公園",
  outline: "明るさと落ち着いた雰囲気の両面を持つ公園です。",
  location: "日野市百草881番地の8",
  access: "三沢台小学校バス停より東へ徒歩3分",
  created_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  updated_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  likes_count: nil,
:...skipping...

このように、画像カラムは直接URLだけをを記載するとnilになってしまう。

解決方法

imageカラムの画像URLの記載を下記のように書き換える。

seed.rb
Post.create!(image: File.open("#{Rails.root}/public/uploads/post/image/1/yatoguti.jpg"),
park: 谷戸口公園,
outline: 程久保の知る人ぞ知る,
location: 東京都日野市程久保1丁目20−14,
access: モノレール程久保駅から徒歩5)

imageカラムの記載を、

Post.create!(image: /public/uploads/post/image/1/yatoguti.jpg,

ではなく、

Post.create!(image: File.open("#{Rails.root}/public/uploads/post/image/1/yatoguti.jpg"),

に変更する。そして、rails db:resetしてから、rails db:seedする。

/image/1/yatoguti.jpg には、実際に画像が入っているかどうか、予めファイルを確認しておいて下さい。

これで、seed初期データの一斉投入が出来ました!

②複数の画像カラムをCSVでインポートする方法

seed.rbで初期データ投入では成功したので、今度はdb/csv_dara/csv_data.csvlib/tasks/import_csv.rakeで、同じように記載して、CSVインポートでも試してみる。

csv_data.csv
image,park,outline,location,access,tag_list
File.open("#{Rails.root}/public/uploads/post/image/6/ajisai.jpg"),芙蓉公園,日野市の「高幡不動」のすぐそばにあるこぢんまりとした公園です。団地の中の高台にあり、地域の子供達の憩いの場となっています。ブランコ、すべり台、鉄棒、砂場があり、自由に遊ぶことができるスペースも確保されています。,東京都日野市高幡714-21,高幡不動駅から徒歩5分,#滑り台 #鉄棒 #ブランコ #砂場
File.open("#{Rails.root}/public/uploads/post/image/8/hohoemi.jpg"),ほほえみ公園,コンクリートの小山が人気の公園です。小山の中には縦横に走るトンネル、外には留め金のついた登山ルート、そして幅広いすべり台と砂場とが一体化していて、ちょっとした冒険気分が楽しめます。,日野市南平2丁目31番地の6,北野街道口バス停より東へ5分,#滑り台 #鉄棒 #ブランコ
File.open("#{Rails.root}/public/uploads/post/image/7/hodokubo.jpg"),ほどくぼ地区広場,少し傾斜のある草原と雑木林。遊具はありませんが、木登り、草すべり、どんぐり拾いなどが楽しめます。原っぱなので、はだしでかけまわっても大丈夫。,日野市程久保3丁目22番地の2,京王線多摩動物公園駅より東へ徒歩5分,#芝生

lib/tasks/import_csv.rake
require 'csv'

namespace :import_csv do

  desc "postテーブルのCSVデータをインポートするタスク"
  task posts: :environment do
    path = File.join Rails.root, "db/csv_data/csv_data.csv"
    list = []
    CSV.foreach(path, headers: true) do |row|
      list << {
          image: row ["image"],
          park: row["park"],
          outline: row["outline"],
          location: row["location"],
          access: row["access"],
          tag_list: row["tag_list"]
      }
    end
    puts "インポート処理を開始"
     Post.create!(list)
      puts "インポート完了!!"
  end

end


seedの初期投入ができた記載だから、CSVインポートも出来ると思っていました。すると…

rake aborted!
CSV::MalformedCSVError: Illegal quoting in line 2.
/Users/sekishinya/Desktop/park_app/lib/tasks/import_csv.rake:11:in `block (2 levels) in <main>'
Tasks: TOP => import_csv:posts
(See full trace by running task with --trace)

エラーでインポートできません。
元々imageカラム抜きでインポートしていたので、imageカラムが問題なのは間違いないはずですが。

解決方法

CSVは、seedの時とは異なり、全て文字列扱いとなるので記載が異なる。

そこで、lib/tasks/import_csv.rakeと、db/csv_dara/csv_data.csvを、以下のように書き直す。

import_csv.rake
require 'csv'

namespace :import_csv do

  desc "postテーブルのCSVデータをインポートするタスク"
  task posts: :environment do
    path = File.join Rails.root, "db/csv_data/csv_data.csv"
    list = []
    CSV.foreach(path, headers: true) do |row|
      list << {
          image: File.open("#{Rails.root}/#{row["image"]}"),
          park: row["park"],
          outline: row["outline"],
          location: row["location"],
          access: row["access"],
          tag_list: row["tag_list"]
      }
    end
    puts "インポート処理を開始"
     Post.create!(list)
      puts "インポート完了!!"
  end

end

import_csv.rakeのimageカラムの記載を、

image: row ["image"],

ではなく、

image: File.open("#{Rails.root}/#{row["image"]}"),

に変更する。

そして、csv_data.csvのimageカラムを以下のように変更する。

csv_data.csv
image,park,outline,location,access,tag_list
"public/uploads/post/image/8/fuyou.jpg",芙蓉公園,日野市の「高幡不動」のすぐそばにあるこぢんまりとした公園です。団地の中の高台にあり、地域の子供達の憩いの場となっています。ブランコ、すべり台、鉄棒、砂場があり、自由に遊ぶことができるスペースも確保されています。,東京都日野市高幡714-21,高幡不動駅から徒歩5分,#滑り台 #鉄棒 #ブランコ #砂場
"public/uploads/post/image/8/hohoemi.jpg",ほほえみ公園,コンクリートの小山が人気の公園です。小山の中には縦横に走るトンネル、外には留め金のついた登山ルート、そして幅広いすべり台と砂場とが一体化していて、ちょっとした冒険気分が楽しめます。,日野市南平2丁目31番地の6,北野街道口バス停より東へ5分,#滑り台 #鉄棒 #ブランコ
"public/uploads/post/image/8/hodokubo.jpg",ほどくぼ地区広場,少し傾斜のある草原と雑木林。遊具はありませんが、木登り、草すべり、どんぐり拾いなどが楽しめます。原っぱなので、はだしでかけまわっても大丈夫。,日野市程久保3丁目22番地の2,京王線多摩動物公園駅より東へ徒歩5分,#芝生

seedの時は、URLのみの記載で失敗しましたが、逆にCSVデータはURLのみにした方が良いですね。

これで見事に、複数の画像の一斉インポートに成功しました!

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

【Rails6】CarrierWaveを使い、複数の画像を一斉に投入する2つの方法/①seedの初期データで投入/②CSVでインポートする

他のtextカラムは簡単に複数投入できるのですが、image(画像)カラムだけはいずれもnilになってしまい苦労したので、ここに複数の画像カラムを一斉に投入する2つの方法を記しておきます。

環境

  • Ruby 2.6.5
  • Rails 6.0.3.2

①複数の画像カラムをseedの初期データで投入する方法

db/migrate/seed.rbに、

seed.rb
Post.create!(image: /public/uploads/post/image/1/yatoguti.jpg,
park: 谷戸口公園,
outline: 程久保の知る人ぞ知る,
location: 東京都日野市程久保1丁目20−14,
access: モノレール程久保駅から徒歩5)

のように記載し、rails db:resetしてから、rails db:seedするが、

seed.rb
=> [#<Post:0x00007fa6c66bc8e8
  id: 27,
  image: nil,
  park: "谷戸口公園",
  outline:
   "程久保の知る人ぞ知る。",
  location: "京王線高幡不動駅より百草園駅方面へ徒歩10分",
  access: "日野市高幡1024番地",
  created_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  updated_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  likes_count: nil,
  tag_list: ["#水遊び", "#アスレチック"]>,
 #<Post:0x00007fa6c7b48f28
  id: 28,
  image: nil,
  park: "雨乞公園",
  outline: "明るさと落ち着いた雰囲気の両面を持つ公園です。",
  location: "日野市百草881番地の8",
  access: "三沢台小学校バス停より東へ徒歩3分",
  created_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  updated_at: Sun, 02 Aug 2020 14:38:30 JST +09:00,
  likes_count: nil,
:...skipping...

このように、画像カラムは直接URLだけをを記載するとnilになってしまう。

解決方法

imageカラムの画像URLの記載を下記のように書き換える。

seed.rb
Post.create!(image: File.open("#{Rails.root}/public/uploads/post/image/1/yatoguti.jpg"),
park: 谷戸口公園,
outline: 程久保の知る人ぞ知る,
location: 東京都日野市程久保1丁目20−14,
access: モノレール程久保駅から徒歩5)

imageカラムの記載を、

Post.create!(image: /public/uploads/post/image/1/yatoguti.jpg,

ではなく、

Post.create!(image: File.open("#{Rails.root}/public/uploads/post/image/1/yatoguti.jpg"),

に変更する。そして、rails db:resetしてから、rails db:seedする。

/image/1/yatoguti.jpg には、実際に画像が入っているかどうか、予めファイルを確認しておいて下さい。

これで、seed初期データの一斉投入が出来ました!

②複数の画像カラムをCSVでインポートする方法

seed.rbで初期データ投入では成功したので、今度はdb/csv_dara/csv_data.csvlib/tasks/import_csv.rakeで、同じように記載して、CSVインポートでも試してみる。

csv_data.csv
image,park,outline,location,access,tag_list
File.open("#{Rails.root}/public/uploads/post/image/6/ajisai.jpg"),芙蓉公園,日野市の「高幡不動」のすぐそばにあるこぢんまりとした公園です。団地の中の高台にあり、地域の子供達の憩いの場となっています。ブランコ、すべり台、鉄棒、砂場があり、自由に遊ぶことができるスペースも確保されています。,東京都日野市高幡714-21,高幡不動駅から徒歩5分,#滑り台 #鉄棒 #ブランコ #砂場
File.open("#{Rails.root}/public/uploads/post/image/8/hohoemi.jpg"),ほほえみ公園,コンクリートの小山が人気の公園です。小山の中には縦横に走るトンネル、外には留め金のついた登山ルート、そして幅広いすべり台と砂場とが一体化していて、ちょっとした冒険気分が楽しめます。,日野市南平2丁目31番地の6,北野街道口バス停より東へ5分,#滑り台 #鉄棒 #ブランコ
File.open("#{Rails.root}/public/uploads/post/image/7/hodokubo.jpg"),ほどくぼ地区広場,少し傾斜のある草原と雑木林。遊具はありませんが、木登り、草すべり、どんぐり拾いなどが楽しめます。原っぱなので、はだしでかけまわっても大丈夫。,日野市程久保3丁目22番地の2,京王線多摩動物公園駅より東へ徒歩5分,#芝生

lib/tasks/import_csv.rake
require 'csv'

namespace :import_csv do

  desc "postテーブルのCSVデータをインポートするタスク"
  task posts: :environment do
    path = File.join Rails.root, "db/csv_data/csv_data.csv"
    list = []
    CSV.foreach(path, headers: true) do |row|
      list << {
          image: row ["image"],
          park: row["park"],
          outline: row["outline"],
          location: row["location"],
          access: row["access"],
          tag_list: row["tag_list"]
      }
    end
    puts "インポート処理を開始"
     Post.create!(list)
      puts "インポート完了!!"
  end

end


seedの初期投入ができた記載だから、CSVインポートも出来ると思っていました。すると…

rake aborted!
CSV::MalformedCSVError: Illegal quoting in line 2.
/Users/sekishinya/Desktop/park_app/lib/tasks/import_csv.rake:11:in `block (2 levels) in <main>'
Tasks: TOP => import_csv:posts
(See full trace by running task with --trace)

エラーでインポートできません。
元々imageカラム抜きでインポートしていたので、imageカラムが問題なのは間違いないはずですが。

解決方法

CSVは、seedの時とは異なり、全て文字列扱いとなるので記載が異なる。

そこで、lib/tasks/import_csv.rakeと、db/csv_dara/csv_data.csvを、以下のように書き直す。

import_csv.rake
require 'csv'

namespace :import_csv do

  desc "postテーブルのCSVデータをインポートするタスク"
  task posts: :environment do
    path = File.join Rails.root, "db/csv_data/csv_data.csv"
    list = []
    CSV.foreach(path, headers: true) do |row|
      list << {
          image: File.open("#{Rails.root}/#{row["image"]}"),
          park: row["park"],
          outline: row["outline"],
          location: row["location"],
          access: row["access"],
          tag_list: row["tag_list"]
      }
    end
    puts "インポート処理を開始"
     Post.create!(list)
      puts "インポート完了!!"
  end

end

import_csv.rakeのimageカラムの記載を、

image: row ["image"],

ではなく、

image: File.open("#{Rails.root}/#{row["image"]}"),

に変更する。

そして、csv_data.csvのimageカラムを以下のように変更する。

csv_data.csv
image,park,outline,location,access,tag_list
"public/uploads/post/image/8/fuyou.jpg",芙蓉公園,日野市の「高幡不動」のすぐそばにあるこぢんまりとした公園です。団地の中の高台にあり、地域の子供達の憩いの場となっています。ブランコ、すべり台、鉄棒、砂場があり、自由に遊ぶことができるスペースも確保されています。,東京都日野市高幡714-21,高幡不動駅から徒歩5分,#滑り台 #鉄棒 #ブランコ #砂場
"public/uploads/post/image/8/hohoemi.jpg",ほほえみ公園,コンクリートの小山が人気の公園です。小山の中には縦横に走るトンネル、外には留め金のついた登山ルート、そして幅広いすべり台と砂場とが一体化していて、ちょっとした冒険気分が楽しめます。,日野市南平2丁目31番地の6,北野街道口バス停より東へ5分,#滑り台 #鉄棒 #ブランコ
"public/uploads/post/image/8/hodokubo.jpg",ほどくぼ地区広場,少し傾斜のある草原と雑木林。遊具はありませんが、木登り、草すべり、どんぐり拾いなどが楽しめます。原っぱなので、はだしでかけまわっても大丈夫。,日野市程久保3丁目22番地の2,京王線多摩動物公園駅より東へ徒歩5分,#芝生

seedの時は、URLのみの記載で失敗しましたが、逆にCSVデータはURLのみにした方が良いですね。

これで見事に、CSVでも複数の画像の一斉インポートに成功しました!

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

カリー化チートシート【記述例一覧版】

拙作記事『カリー化チートシート』について,対応言語や補足追記が増えるにつれ,チートシートというよりは解説記事と化してきたので,あらためて記述例一覧版を作成.しばらくしたら,元記事タイトルを『カリー化記述まとめ』に変えて,こちらを『カリー化チートシート』にするかも.

記述例一覧

複数の記法が可能な言語は,最新バージョン&カリー化メソッド未使用の場合の,最も短い書き方のみを掲載.その他の記法やバージョン等による違いは元記事を参照.

言語 (λxy.(真, if x>y; and 偽, if x≦y)) 10 20
Haskell (\x y -> x > y) 10 20
Scheme (((lambda (x) (lambda (y) (> x y))) 10) 20)
Python (lambda x: lambda y: x > y)(10)(20)
Ruby -> x { -> y { x > y } }[10][20]
JavaScript (x => y => x > y)(10)(20)
Scala ((x: Int) => (y: Int) => x > y)(10)(20)
Perl sub { my $x = shift; return sub { my $y = shift; return $x > $y }; }->(10)->(20)
Go言語 func(x int) func(int) bool { return func(y int) bool { return (x > y) } }(10)(20)
PHP (fn($x) => fn($y) => $x > $y)(10)(20)
Julia (x -> y -> x > y)(10)(20)
Emacs Lisp, Common Lisp (funcall (funcall (lambda (x) (lambda (y) (> x y))) 10) 20)
R言語 (function(x) { function(y) { x > y } })(10)(20)

更新履歴

  • 2020-08-14:大きい方の値ではなく真偽値を返す記述例に変更
  • 2020-08-14:初版公開(Haskell,Scheme,Python,Ruby,JavaScript,Scala,Perl,Go言語,PHP,Julia,Emacs Lisp/Common Lisp,R言語)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでAjaxでいいね機能を実装する方法

「Railsでいいね機能を実装する方法」でいいね機能の実装方法をご紹介しましたが、今回はそのいいね機能をAjax(非同期通信)実装する方法をご紹介いたします。
完成系は以下のような感じです。
いいね_Ajax.gif

環境

  • Ruby 2.5.7
  • Rails 5.2.4

前提

  • この記事によって、いいね機能が実装済みであること

index.html.erbを編集

2つのlink_to(method: :delete と method: post)に remote: trueを追記します。
remote: trueを記載することで、Ajaxでの処理を実行することができます。

index.html.erb
<div class="container">
  <h1>記事一覧</h1>
  <table class="table">
    <% @posts.each do |post| %>
      <tr>
        <td><%= post.title %></td>
        <td>
          <% if post.liked_by?(current_user) %>
            <% like = Like.find_by(user_id: current_user.id, post_id: post.id) %>
            <%= link_to like_path(like), method: :delete, remote: true do %>
              <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: red;">
              <span><%= post.likes.count %></span>
            <% end %>
          <% else %>
            <%= link_to post_likes_path(post), method: :post, remote: true do %>
              <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: gray;">
              <span><%= post.likes.count %></span>
            <% end %>
          <% end %>
        </td>
      </tr>
    <% end %>
  </table>
</div>

いいね機能の部分をテンプレート化する

index.html.erbと同じディレクトリに以下のファイルを作成し、いいね機能の部分をコピーし、貼り付けます。

_like.html.erb
<% if post.liked_by?(current_user) %>
  <% like = Like.find_by(user_id: current_user.id, post_id: post.id) %>
  <%= link_to like_path(like), method: :delete, remote: true do %>
    <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: red;">
    <span><%= post.likes.count %></span>
  <% end %>
<% else %>
  <%= link_to post_likes_path(post), method: :post, remote: true do %>
    <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: gray;">
    <span><%= post.likes.count %></span>
  <% end %>
<% end %>

部分テンプレート(_like.html.erb)を呼び出すため、いいね機能の部分があったところにrenderを記述します。
また、Ajaxの処理がされる部分を識別できるように id を記述します。

index.html.erb
<div class="container">
  <h1>記事一覧</h1>
  <table class="table">
    <% @posts.each do |post| %>
      <tr>
        <td><%= post.title %></td>
        <td id="like-<%= post.id %>">  <!--idで識別できるようにする-->

       <%= render "like", post: post %>  <!--renderで部分テンプレートを呼び出す-->

        </td>
      </tr>
    <% end %>
  </table>
</div>

controllerの編集

各アクションの最後にredirect_backをしていましたが、redirect_backをすると再読み込みをしていまい、Ajaxが機能しません。
そのため、redirect_backを削除します。

likes_controller.rb
  def create
    like = Like.new(user_id: current_user.id, post_id: params[:post_id])
    @post = like.post
    like.save
  end

  def destroy
    like = Like.find(params[:id])
    @post = like.post
    like.destroy
  end

jsファイルの作成

remote: trueによってjs形式のリクエストを送信しているため、実行するアクション名(createやdestroy)のjsファイルを最終的に探しに行きます。
そのため、app/views/配下にlikesフォルダを作成し、そのフォルダの中に create.js.erb と destory.js.erb を作成します。

create.js.erb
$("#like-<%= @post.id %>").html("<%= j(render 'posts/like', post: @post) %>");
destory.js.erb
$("#like-<%= @post.id %>").html("<%= j(render 'posts/like', post: @post) %>");

idで識別し部分的にhtmlを書き換えます。
これで完成です。

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

Railsでいいね機能を実装する方法

Railsでいいね機能を実装する方法をご紹介いたします。
完成系は以下のような感じです。
いいね1.gif

環境

  • Ruby 2.5.7
  • Rails 5.2.4

前提

  • deviseによるログイン機能が実装できていること
  • ユーザーのテーブルは"users"、記事のテーブルは"posts"、いいねの中間テーブルは"likes"とする。
  • postsのカラムはtitleを追加する

Likeモデルを追加

$ rails g model like user_id:integer post_id:integer

$ rails db:migrate

アソシエーションを設定

それぞれ以下を記述し、アソシエーションを設定します。

like.rb
  belongs_to :user
  belongs_to :post
user.rb
  has_many :likes, dependent: :destroy
post.rb
  has_many :likes, dependent: :destroy

ルーティングを設定する

routes.rb
  resources :posts, shallow: true do
    resources :likes, only: [:create, :destroy]
  end

liked_by?メソッドを作成

post.rb
  def liked_by?(user)
    likes.where(user_id: user.id).exists?
  end

controllerの記述

likes_controller.rb
  def create
    like = Like.new(user_id: current_user.id, post_id: params[:post_id])
    @post = like.post
    like.save
    redirect_back(fallback_location: posts_path)
  end

  def destroy
    like = Like.find(params[:id])
    @post = like.post
    like.destroy
    redirect_back(fallback_location: posts_path)
  end

Bootstrapを導入

Gemfile
gem 'bootstrap-sass', '~> 3.3.6'
gem 'jquery-rails'
$ bundle install

application.cssのファイル名をapplication.scssに変更する

$ mv app/assets/stylesheets/application.css app/assets/stylesheets/application.scss

Bootstrapをscssに読み込ませる

application.scssの一番下に以下を記述

application.scss
@import "bootstrap-sprockets";
@import "bootstrap";

application.jsを編集する

application.jsの以下の部分を

application.js
//= require rails-ujs
//= require turbolinks
//= require_tree .

から

application.js
//= require rails-ujs
//= require jquery
//= require bootstrap-sprockets
//= require_tree .

に書き換えます。

index.html.erbを編集

index.html.erb
<div class="container">
  <h1>記事一覧</h1>
  <table class="table">
    <% @posts.each do |post| %>
      <tr>
        <td><%= post.title %></td>
        <td>
          <% if post.liked_by?(current_user) %>
            <% like = Like.find_by(user_id: current_user.id, post_id: post.id) %>
            <%= link_to like_path(like), method: :delete do %>
              <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: red;">
              <span><%= post.likes.count %></span>
            <% end %>
          <% else %>
            <%= link_to post_likes_path(post), method: :post do %>
              <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: gray;">
              <span><%= post.likes.count %></span>
            <% end %>
          <% end %>
        </td>
      </tr>
    <% end %>
  </table>
</div>

これで完成です。

参考記事

https://qiita.com/soehina/items/a68ab66da3ea1d260301

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

初心者がDeviseのコードを見て仕組みをふんわり理解する【registrations new】

はじめに

Railsチュートリアルがやっと1周終わった者です。
gemのDeviseを入れてみたら、Railsチュートリアルでの5~6章分の実装が10分ほどで終わり、驚愕しています。

なんでできたのか分からない...
そして、テストも書きたいけどどうやって書いたらいいのか分からない。
そこで「公式やソースコードを読みましょう」ということで、読んでみたけどこれまたさっぱり分からない...ながらも少しずつ理解したいので、読み込んでいきます。

読みながら自分が調べたことをまとめていきます。

・初心者だけどDeviseの仕組みを知りたい

そんな方の役に立てたら幸いです。

DeviseのGitHub
https://github.com/heartcombo/devise

Deviseの概要

GitHubのREADMEの一番最初に、概要の説明があります。

  • Deviseは、Wardenに基づくRails向けの柔軟な認証ソリューションです。
  • ラックベースです。
  • Railsエンジンに基づく完全なMVCソリューションです。
  • 複数のモデルに同時にサインインさせることができます。
  • モジュール性の概念に基づいています。本当に必要なものだけを使用してください。

ここで出てくるWardenというのは認証のためのgemで、devise内でこれを引っ張ってきているようです。

また、10個のモジュールで構成されており、必要なものはコメントアウトを外したりしながら使ってね、ということのようです。
モジュールというかもう機能ですね。
Qiita内で表にしてくださっている方がいらっしゃったので、下記にて引用します。

機能 概要
database_authenticatable サインイン時にユーザーの正当性を検証するためにパスワードをハッシュ化してDBに登録します。認証方法としてはPOSTリクエストかHTTP Basic認証が使えます。
registerable 登録処理を通してユーザーをサインアップします。また、ユーザーに自身のアカウントを編集したり削除できるようにします。
recoverable パスワードをリセットし、それを通知します。
rememberable 保存されたcookieから、ユーザーを記憶するためのトークンを生成・削除します。
trackable サインイン回数や、サインイン時間、IPアドレスを記録します。
validatable Emailやパスワードのバリデーションを提供します。独自に定義したバリデーションを追加することもできます。
confirmable メールに記載されているURLをクリックして本登録を完了する、といったよくある登録方式を提供します。また、サインイン中にアカウントが認証済みかどうかを検証します。
lockable 一定回数サインインを失敗するとアカウントをロックします。ロック解除にはメールによる解除か、一定時間経つと解除するといった方法があります。
timeoutable 一定時間活動していないアカウントのセッションを破棄します。
omniauthable intridea/omniauthをサポートします。TwitterやFacebookなどの認証を追加したい場合はこれを使用します。

引用元:[Rails] deviseの使い方(rails6版)
https://qiita.com/cigalecigales/items/16ce0a9a7e79b9c3974e

また、Deviseは、コントローラーとビュー内で使用するヘルパーを作成します。
よく使用するコマンドを予め設定したものです。

デバイスモデルが 'User'であると想定してヘルパー名は例示していますが、
デバイスモデルがユーザー以外の場合は、「_ user」を「_yourmodel(任意のモデル名)」に置き換えると、同じロジックが適用されます。

こちらもQiita内で表にしてくださっている方がいらっしゃったので引用します。

メソッド 用途   
before_action :authenticate_user! コントローラーに設定して、ログイン済ユーザーのみにアクセスを許可する
user_signed_in? ユーザーがサインイン済かどうかを判定する
current_user サインインしているユーザーを取得する
user_session ユーザーのセッション情報にアクセスする

引用元:Rails deviseで使えるようになるヘルパーメソッド一覧
https://qiita.com/tobita0000/items/866de191635e6d74e392

registrations -- サインアップ・アカウント編集・削除

最も基本的なアカウントのCRUDはこのregistrationが担っているので、ここが分からないと応用の機能部分のコードリーディングは難しそうです。

コード全体を上から調べた範囲で記入してきますが、ちまちま区切っていくので、分かりくいかもしれません。
横にソースコードを置きながら見てもらえれば、ちょっとは分かりやすいかも...

frozen_string_literal

devise/app/controllers/devise/registrations_controller.rb
# frozen_string_literal: true

コメントアウトされていますが、Rubyのバージョンアップに備えた1文のようです。
参考:frozen_string_literalが入って気づいた、メソッド設計の原則
https://qiita.com/jkr_2255/items/300b5db8c1f04e1e2815

prepend_before_action

devise/app/controllers/devise/registrations_controller.rb
class Devise::RegistrationsController < DeviseController
  prepend_before_action :require_no_authentication, only: [:new, :create, :cancel]   
  prepend_before_action :authenticate_scope!, only: [:edit, :update, :destroy]
  prepend_before_action :set_minimum_password_length, only: [:new, :edit]
  • DeviseControllerを継承しています。 ソースコードのファイルをみると、devise_controller.rbはこのモジュールだけでなく、すべてのモジュールへ引き継いでいます。
  • prepend_before_actionbefore_actionより前に実行するメソッドです。アクセスできるアクションがユーザーのログイン状態で制限されるようにしています。

newアクション

devise/app/controllers/devise/registrations_controller.rb
 # GET /resource/sign_up
  def new
    build_resource
    yield resource if block_given?
    respond_with resource
  end
  • ログインするための最初の部分です。
  • resourceはすでにdevise_controllerで定義されています。
devise/app/controllers/devise_controller.rb
  def resource
    instance_variable_get(:"@#{resource_name}")
  end

  # Proxy to devise map name
  def resource_name
    devise_mapping.name 
  end

  alias :scope_name :resource_name
  • instance_variable_getメソッドはインスタンス変数の値を取得して返します。@user =からの定義と同じもののようです。
  • resource内に#{resource_name}という変数がありますが、その下部分で定義されています。
  • devise_mapping.nameこちらもすでにdevise_controllerで定義されてますが、引用記事をみると、別部分にヒントがありそうです。
devise/app/controllers/devise_controller.rb
  def devise_mapping 
    @devise_mapping ||= request.env["devise.mapping"]
  end

nameに注目してコードを繋げてみると、認証モデルがUserである場合は@singular = :users.to_s.tr('/', '').singularize.to_symとみれます。singularizeは複数形を単数形に変換するメソッドで、最終的に@singular = :userとなりsingularのエイリアスがnameとなっているのでmapping.nameで:userが取得できます。
するとdefine
methodsの引数に:userが渡されauthenticate_user!が出来上がるという流れになっています。
引用元:DeviseのコードリーティングでRailsを学ぶ
https://qiita.com/irisAsh/items/513b8b58f54421b9a1a0

端的に言うと、mapping.name:userが取得できるのでそれをresource_nameにしているということでしょうか。

  • registrations_controller下部にbuild_resourceがあります。セッションを新しく作るという意味のようです。
devise/app/controllers/devise/registrations_controller.rb
 def build_resource(hash = {}) #build_resource(hash = {})の定義
    self.resource = resource_class.new_with_session(hash, session)
  end
  • 元のregistrations_controller上部へ戻りましょう。buildはほぼnewと近い役割をしています。build_resourceという1文は、データベースから取り出したユーザーのインスタンス変数とセッションを作るということになります。
  • block_given?はメソッドを実行する時にブロックが渡されていればtrueを返し、渡されていない時はfalseを返します。
    • ブロックとは

do ... end または { ... } で囲まれたコードの断片 (ブロックと呼ばれる)を後ろに付けてメソッドを呼び出すと、そのメソッドの内部からブロックを評価できます。ブロック付きメソッドを自分で定義するには yield 式を使います。
引用元:Ruby 2.7.0 リファレンスマニュアル
https://docs.ruby-lang.org/ja/latest/doc/spec=2fcall.html#block

devise/app/controllers/devise_controller.rb
  def respond_with_navigational(*args, &block)
    respond_with(*args) do |format|
      format.any(*navigational_formats, &block)
    end
  end
devise/app/controllers/devise_controller.rb
  def navigational_formats
    @navigational_formats ||= Devise.navigational_formats.select { |format|Mime::EXTENSION_LOOKUP[format.to_s] }
  end

ピンときていないので勉強して加筆・修正したいと思います。

ここまで複雑なんだなGem...たった数行のコードにいろいろなものが凝縮されていてとても勉強になりました。
newアクションしか書けなかったけど、最終的にはregistrations_controllerの各アクションのだけでも読んだ記録を残したい...!

お付き合いいただきありがとうございました。

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