railsがんばる子

Ruby on Railsがんばる子です。胡蝶蘭のECサイトを運営しています。

EKS Ingress 俺的ハマりポイント

ローカル環境でminikubeを用いてKubernetesを構成して、パブリッククラウド使いたいなぁとおもってAmazon EKSを利用しました。 ekstool便利だし、慣れ親しみのあるAWSですし。

次のようにIngressを作り、kubectl apply -f ingress.yamlなどしてアクセスしてみると上手くいきました。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  namespace: sample2
  name: rails-ing
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
spec:
  rules:
  - host: 're----ruzu.com'
    http:
      paths:
      - path: /
        backend:
          serviceName: rails
          servicePort: 3000

しかし8b27e02b-sample2-railsing-837f-1682119185.ap-northeast-1.elb.amazonaws.com/hogeにアクセスすると404が返ってきます。🤔ハテ…。 ルート以外のpathはすべて404となってしまうようです。

いろいろ試行錯誤した結果 pathを/*としたらうまくいきました。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  namespace: sample2
  name: rails-ing
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
spec:
  rules:
  - host: 're----ruzu.com'
    http:
      paths:
      - path: '/*'
        backend:
          serviceName: rails
          servicePort: 3000

メンターさんに相談してみたところ、ALBの挙動だと思うから調べてみなはれとのこと。

ほーん、、、たしかに/以外は404になっていますね。。。 なるほどね。

ActionMailerを使ってる時にERBテンプレートでエラーしたときの原因特定方法

ActionMailerにて、erbテンプレートを利用することはよくあると思いますが、開発中にエラーが起きても下記の様に原因がよくわからないことがあります。

> Mailer.sth(@order)
=> #<ActionMailer::Base::NullMail:0x007f93bf3c8638>

ERBテンプレートだけを検証することで、原因を探ることが出来ます。

> ERB.new(File.new('app/views/mailer/sth.text.erb').read).result
ArgumentError: Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true
from ............................/lib/action_dispatch/http/url.rb:45:in `full_url_for'

なんか、URLの生成でしくじってるみたいですね☆

rubyでGoogle Sheets APIとGoogle Drive APIを使ってみた。

みんな大好きExcelファイルをシステムで自動生成してメールで送付してほしいという案件にあたりました。 そこで、みんな大好きGoogle様のAPIを使ってExcelファイルを生成しました。(※ なぜRubyXLという便利なgemを利用しなかったのかはこの記事の最後をご参照ください。) Google APIは以前も使いましたが、バージョンがあがっているみたいなので調べ直しました。

条件

  • テンプレートファイルがあるので、そのファイルを元に値を変更したい
  • VLOOKUPなどの式が入っているので、ファイルを開いた時に式が再計算されている状態にしたい

準備

Googleのアカウントを作成する

  • みんな持ってると思うので省略します

Google Drive APIを有効にする

Google Sheets APIを有効にする

  • Google Sheets APIが無効な状態でAPIを操作しよとすると、こんな感じのエラーがでるので、予め有効にしておくといいと思います。
Google::Apis::ClientError: forbidden: Google Sheets API has not been used in project before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/sheets.googleapis.com/overview?project=xxxxxx then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.
from .../vendor/bundle/ruby/2.2.0/gems/google-api-client-0.9.9/lib/google/apis/core/http_command.rb:211:in `check_status'
  • API Managerの概要でsheetと検索すると、Google Sheets APIが出てきます。あとはウィザードに従いAPIを有効にしてください。 gyazo.com

client_secretを得る

  • Quickstartに書いてありますが、APIアクセスするためにはclient_secretが必要です。
  • 今回はOAuth client IDを使うので、quickstartに書いてあるとおりの手順で、client_secretを作成してください。

コード

さて、お待ちかねのコードの時間です。 テンプレートファイルをコピーして、セルの値を更新して、メールに添付するという実装を行います。 ついでですが、不要になったファイルは消します。

gemをインストールします。

  • Gemfile
gem 'google-api-client'
gem 'google_drive'

認証用のメソッドを作成する

  • quickstartを参考にしたコードです。
    • quickstartではcredentialsをファイルにstoreしていますが、Redisに変更しています。
    • quickstartでは必要最低限のSCOPEのみとなっていたため、Drive APIとSheets APIの両方を使えるSCOPEに変更しています。
  • client_secretはclient_secret.jsonという名前で、同じディレクトリにおいてください。
  • 実行したらURLが表示されるので、ブラウザで認可したあとに表示される文字列をCLIに貼り付けてください。
require 'google/apis/sheets_v4'
require 'googleauth'
require 'googleauth/stores/redis_token_store'


OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'
CLIENT_SECRETS_PATH = 'client_secret.json'
SCOPE = [Google::Apis::SheetsV4::AUTH_SPREADSHEETS,
 Google::Apis::DriveV3::AUTH_DRIVE]

def authorize
  client_id = Google::Auth::ClientId.from_file(CLIENT_SECRETS_PATH)
  token_store = Google::Auth::Stores::RedisTokenStore.new(redis: Redis.new)
  authorizer = Google::Auth::UserAuthorizer.new(
    client_id, SCOPE, token_store)
  user_id = 'default'
  credentials = authorizer.get_credentials(user_id)
  if credentials.nil?
    url = authorizer.get_authorization_url(
      base_url: OOB_URI)
    puts "Open the following URL in the browser and enter the " +
         "resulting code after authorization"
    puts url
    code = gets
    credentials = authorizer.get_and_store_credentials_from_code(
      user_id: user_id, code: code, base_url: OOB_URI)
  end
  credentials
end

テンプレートファイルをコピーする

  • ファイルをコピーするにはDrive APIを利用します。
# Initialize the API

drive_service = Google::Apis::DriveV3::DriveService.new
drive_service.client_options.application_name = 'nyaahara sama'
drive_service.authorization = authorize

# example: https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit
drive_service.copy_file('1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms')

かんたんですね!!

セルの値を変更する

  • セルの値を変更するにはSheets APIを利用します。
# Initialize the API
sheet_service = Google::Apis::SheetsV4::SheetsService.new
sheet_service.client_options.application_name = 'nyaahara sama'
sheet_service.authorization = authorize


value_range = Google::Apis::SheetsV4::ValueRange.new
value_range.range = 'A1:D1'
value_range.major_dimension = 'ROWS'
value_range.values = [['にゃ','','','']]

# example: https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit
sheet_service.update_spreadsheet_value(
  '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
  value_range.range,
  value_range,
  value_input_option: 'USER_ENTERED',
)

これで、A1〜D1にそれぞれ「にゃ」「あ」「は」「ら」と入力されているはずです。 縦に行きたかったら、下記のようにしてみてください。

value_range.range = 'A1:A4'
value_range.values = [['にゃ'],[''],[''],['']]

value_range.major_dimension = 'ROWS'を変更してもいいのかもしれません。調べられていないです。

ダウンロードしてメールに添付する

  • 今回はGoogle APIの使い方の紹介なので、メールに添付する部分は割愛しています。
  • ファイルのダウンロードはDrive APIのほうを使います。
# example: https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit
drive_service.export_file('1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
                          'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                          download_dest: StringIO.new)
  • 簡単ですね!!
  • download_destFile.open('file_name', 'w')などにしてみても、動くと思いますよ!!
  • ちなみに第二引数はMIME typeですが、見つけるのにちょっと手間取りました。
    • ここにあります。リンク集にも出しておきますね。

ファイルを削除する

  • ファイルの削除はDrive APIを使います。なんとなく予測できましたよね?
drive_service.delete_file('1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms')
  • 説明は不要ですね?

まとめ

  • Google Spread Sheetのファイルをコピーして、セルの値を編集したあとダウンロードして削除するまでを実装しました。
  • フィアル、シート、セルなどの新規追加や、ファイル移動など他の事もたくさんできるみたいなので、また書きたいと思います。
  • 神ソースを発見したことで、早く実装できました。ありがとうございます。

参考リンク

なぜ、GoogleSpreadSheetを利用したのか

RubyXLを利用して作成したExcelファイルをWindowsで開くとVLOOKUPなどの式に値が反映されていなかったのですよ。。。トホホ。

/xxx.txtなどのアクセスを受けた時、MissingTemplateエラーが出力される

何が起きたのか

/xxx.txtなどのアクセスを受けた時、MissingTemplateエラーが出力される

概要

404.txtをrenderしようとするが、404.html.erbなどHTMLファイルしか用意していないことが原因である。 なるほどね。 まあ、当たり前ですよね

解決策

  • Railsで対応しているフォーマットの404と500を全部作る
  • MissingTemplateが発生したら404.htmlに無理やり変更する(もしくは500)

Railsで運営されてるっぽいサイトはどうやって対応しているのか?

github様

いつもお世話になっています。 https://github.com/rails/rails/issues/25593.txt

ホワイトアウト!

これ、何も対処してないね。

https://github.com/rails/rails.txtなどは404ページが表示される。 slugがrails.txtのリソースを探しにいってNOT FOUNDということだと思う。 まあ、何も対応してないね。

Oh My Glasses様

Spreeを利用されているメガネのECサイトさん。 同じフレームワークを使っているので、こういうとき、よく参考にしています。 https://www.ohmyglasses.jp/brands.txt

しぼん。

結論

  • ま、きにしなくていいんじゃねーか?
  • 他にもいくつかみたけど、適切にtxtを返すようなサイトは見つからなかった。
  • .txtなどをpermalinkに含めて検索してNOT FOUNDを返すケースと、サーバーエラーになるケースの2つに分かれる模様。
  • 「1行routes.rbに書けば解決!」みたいなのが無ければやらなくて良さそう。めんどくさいし。

Rubyの正規表現を複数行に分けて名前付きキャプチャをする

目的

  • 整頓されていないテキストデータをrubyで取り扱いたい
  • コンテンツデータを作る人は、プログラマーではないことが多いので、スペースや改行、フォーマットなどを気にしない

解決策

  • 正規表現でキャプチャする
  • 名前キャプチャを出来ると便利(可読性UP)
  • 長くなるので複数行で正規表現を定義したい(可読性UP)

早速

# コロンの後にスペースが有ったり無かったりしますよね
# 各項目について改行があったりなかったりしますよね
raw_text = "
お名前:にゃあはらさん
メールアドレス: akb48@nyaahara.comお電話番号:教えられないよ!"

# \s? を使ってスペースがあってもなくてもマッチング
# \n?を使って改行があってもなくてもマッチング
# (?<name>pattern) で指定した変数名(name)にキャプチャした結果を投入します。
/
  お名前:\s?(?<name>.+)\n?
  メールアドレス:\s?(?<email>.+)\n?
  お電話番号:\s?(?<phone_number>.+)\n?
/xi =~ raw_text
# xiオプションを指定すると、改行やスペースを無視してくれます
          

実行結果サンプル

[1] pry(main)> raw_text = "
[1] pry(main)* お名前:にゃあはらさん
[1] pry(main)* メールアドレス: akb48@nyaahara.comお電話番号:教えられないよ!"
=> "\nお名前:にゃあはらさん\nメールアドレス: akb48@nyaahara.comお電話番号:教えられないよ!"

[2] pry(main)> /
[2] pry(main)*   お名前:\s?(?<name>.+)\n?
[2] pry(main)*   メールアドレス:\s?(?<email>.+)\n?
[2] pry(main)*   お電話番号:\s?(?<phone_number>.+)\n?
[2] pry(main)* /xi =~ raw_text
=> 1

[3] pry(main)> name
=> "にゃあはらさん"

[4] pry(main)> email
=> "akb48@nyaahara.com"

[5] pry(main)> phone_number
=> "教えられないよ!"          

参考リンク

Ruby の正規表現を複数行で書く

railsでgoogleのclient idを拾う

Google Analyticsではgoogleのclient idを一意にしてユーザの特定を行っているようです。 こんなやつです。

gyazo.com

cookieに_gaというキーで登録されており、サーバーに送信していることについ最近きがつきました。 たとえば、ホットペッパーさんのサイトなんかのクッキーをみると、ありますね。こういうのです。

f:id:nyaahara:20160628224449p:plain

さて、cookieに登録してあるということは簡単に参照できます。

module GoogleAnalyticsSupport
  extend ActiveSupport::Concern
    def client_id
      cookies[:_ga]
    end
  end
end

このモジュールをコントローラーでincludeすれば大丈夫ですね。★

あとは、

Analyticsの画面で、自社のアプリケーションのユーザの情報を表示できるようにしたいですね。。。 まあ、それは追々。

RankedModelをSTIで使う時にハマった

Rails 4で作るドラッグアンドドロップで表示順を変更できるサンプルアプリ(スクリーンキャスト付き)などで有名なRankedModel。 RankedModelを知ってから、順序を指定するようなモデルではRankedModelを利用しています。

本日はRankedModelをSTIを利用したとき、順番が意図した通りにならずハマったので共有します。

何が起きたのか?

RankedModelでは作成した順番に並ぶようにデータが作成されていきますが、STIを利用したときに上手く行きませんでした。 下の例では、car1、car2、truck1、truck2と並んで欲しいところです。 どうやらモデルごとに値を決めているようです。

class Vehicle < ActiveRecord::Base
  ranks :row_order
end

class Car < Vehicle
end

class Truck < Vehicle
end

Car.create!(name: :car1)
Car.create!(name: :car2)
Truck.create!(name: :truck1)
Truck.create!(name: :truck2)


Vehicle.rank(:row_order).pluck(:name)
#=> ["car1", "truck1", "car2", "truck2"] ( ゚∋゚)

解決策

ranksメソッドの第二引数にオプションとしてclass_nameを渡せば解決します。

よく見たらGithubに書いてありました。

class Vehicle < ActiveRecord::Base
  ranks :row_order, class_name: 'Vehicle'
end

class Car < Vehicle
end

class Truck < Vehicle
end

Car.create!(name: :car1)
Car.create!(name: :car2)
Truck.create!(name: :truck1)
Truck.create!(name: :truck2)


Vehicle.rank(:row_order).pluck(:name)
#=> ["car1", "car2", "truck1", "truck2"]

解決策に至るまでに考えたこと

  • データを投入したあとにupdate_columnで値を更新する
Car.create!(name: :car1)
Car.create!(name: :car2)
Truck.create!(name: :truck1)
Truck.create!(name: :truck2)
Vehicle.rank(:row_order).each.with_index { |v, i| v.update_column(:row_order, i + 1) }
# => rankした時点でcar1、truck1、car2、truck2の順番に並ぶため、無意味。この時点ではモデルごとに値を決めていることに気づいていなかった