バリデーションエラーメッセージをJavaScript使って表示させる

いつもはフォームのエラーメッセージは下記のようにJavaScriptを使わずに行っていた。

 

messages_controller.rb

class MessagesController < ApplicationController
〜略〜
def create
@message = Message.new(message_params)
if @message.save
redirect_to root_path
else
render :new
end
end
〜略〜
end

 

new.html.erb

<%= form_with(model: @message, local: true) do |f| %>
<% if @message.errors.any? %>
  <%= render 'errors', model: f.object %>
<% end %>
 〜略〜
<% end %>

 

_errors.html.erb

< if model.errors.any? >
  <div class="error-alert">
<ul>
<% model.errors.full_messages.each do |message| %>
<li class='error-message'><%= message %></li>
<% end %>
</ul>
</div>
<% end %>

 

 

今回はJavaScriptを使ってエラー表示してみる。

必要なことは

①turbolinks を有効にする

②respond_toメソッドを使ってリクエストのフォーマットごとに処理を分ける

③local: true を消してリクエスト形式をJS形式に変える

④create.js.erb ファイルを作成し、要素を指定し、エラー表示の部分を埋め込む

⑤JS形式のときの部分テンプレートの読み込みは "j render" を使う

 

messages_controller.rb

class MessagesController < ApplicationController
 〜略〜
  def create
@message = Message.new(message_params)

respond_to do |format|
if @message.save
format.html { redirect_to root_path }
else
format.html { render :new }
format.js
end
end
end
〜略〜
end

 

new.html.erb

 

<%= form_with(model: @message ) do |f| %>
<div id="errors"></div>
〜略〜
<% end %>

 

create.js.erb

document.getElementById("errors").innerHTML = "<%= j render "shared/error_messages" %>"

 

_errors.html.erb

<div class="error-alert">
<ul>
<% @item.errors.full_messages.each do |message| %>
<li class='error-message'>
<%= message %>
</li>
<% end %>
</ul>
</div>

 

以上で問題なくバリデーションのエラーが表示された。

ちなみに参考にさせていただいたのはこちらのページ

https://pikawaka.com/rails/remote-true

テンプレートエンジン

ERB

Rubyに標準で使えるテンプレートエンジン。

ERBの見た目はHTMLとほぼ同じのため、既存のHTMLもそのまま使える。

<!DOCTYPE html>
<html>
<head>
<title>Title</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<script type="text/javascript" src="https://js.pay.jp/v1/"></script>
<%= stylesheet_link_tag 'application', media: 'all'%>
<%= javascript_pack_tag 'application' %>
</head>
<body>
<%= yield %>
</body>
</html>

おなじみのコード。

 

Haml

HTMLをシンプルなコードにしたテンプレートエンジン。

Rails標準搭載ではないのでgemパッケージをインストールして使う。

gemを使うときは gem 'haml-rails'

!!!
%html
%head
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
%title Title
= csrf_meta_tags
= csp_meta_tag
%script{:src => "https://js.pay.jp/v1/", :type => "text/javascript"}
= stylesheet_link_tag 'application', media: 'all'
= javascript_pack_tag 'application'
%body
= yield

end などの締めのコードを省けるので記述量は減る

既存のerb をhaml にしてくれるgem 'html2haml'もある。

 

逆にhaml からerb に翻訳してくれる参考にさせていただいているホームページはこちら。

https://haml2erb.org/

 

Slim

Hamlより更に簡潔なコードになるテンプレートエンジン。

こちらもRails標準搭載ではないのでgemパッケージをインストールして使う。

gem'slimrails'

doctype html
html
head
title
| Title
= csrf_meta_tags
= csp_meta_tag
script[type="text/javascript" src="https://js.pay.jp/v1/"]
= stylesheet_link_tag 'application', media: 'all'
= javascript_pack_tag 'application'
body
= yield

こんな感じ。

ちなみにこちらのページはHTMLをHamlまたはSlimに翻訳してくれるページ。

https://erb2slim.com/

 

 

 

 

 

 

タイムゾーンに関する話

1つ目はGitHubのOmniAuth の認証が進まなくて詰まった話

 

結論から言うと時間がずれていました。

PCのローカル環境の時間がずれていることで発生したようだ。

見てほしいのはこちら。

https://gyazo.com/fbcdfda4e4f76793242b60d8c604c9f2

 

 0.2秒…これだけでエラーになるのか…

 

https://qiita.com/hirokishirai/items/5a43977a38ecd922bfb9

の記事を参考にさせていただいたが、最初は全部問題ないと思いきや、

情報通信研究機構さんのページ(https://www.nict.go.jp/JST/JST5.html)で調べると時間がずれていることが判明。

 

色々調べた結果、

.bash_profileにexport TZ="Asia/Tokyo" を記述すると良いとのこと。

ターミナルで vim ~/.bash_profile、 i(インサート)で編集。

export TZ="Asia/Tokyo" を入れて esc、:wq で保存して解決。

 

 

 

 

2つめは別のタイムゾーンの変更方法。

 

Rails でアプリケーションを作るとデフォルトはUTCで日本とは9時間のズレでデータベースに保存される。

では日本時間で保存したい場合はどうするかというと、 config/application.rb内に config.time_zone = "Tokyo" とconfig.active_record.default_timezone = :local を追加する。

class Application < Rails::Application
config.load_defaults 6.0
config.time_zone = "Tokyo"
config.active_record.default_timezone = :local
end

 

 

ちなみにconfig.time_zone = "Tokyo" だけを記述するとRails内の時間だけを変えることが出来る。

 

 

ちなみに上記の2つは違うシチュエーションで行った処理のため、どちらも同じように作用するのかは検証していない。

pythonで模写してみた

Pythonならyoutubeアーカイブのチャット欄からコメントを取得できると思い、手を付けてみた。(実際はLIVEチャットの取得のみでアーカイブからではなかった)

 

どこをどう持っていけばよいのか参考にした動画を見ながら、Youtube DATA API のリファレンスで試していった。

正直いきなりやるものではないと思った笑

ただ、需要があるおかげでコードはyoutubeにあったので2日かけて少しずつ理解しながらなんとか模写。

(ちなみにここにコピーしたコードのインデントはスペースを開けてもブログのブラウザに反映されてないかも…) 

 

こちらが模写したコード。

import json
import time
import re
import datetime as dt
import matplotlib.pyplot as plt

from googleapiclient.discovery import build


class YouTubeLiveChat:
   # コンストラクタ…インスタンスに実体を持たせるための特殊メソッド(データの初期化も行っている?)
   def __init__(self, _video_id):
   _json_open = open('config.json', 'r')
   _api_key = json.load(_json_open)['api_key']
   _youtube = build('youtube', 'v3', developerKey=_api_key)
   self.video_id = _video_id
   # execute is used for connecting database
   _res = _youtube.videos().list(
   part='liveStreamingDetails', id=_video_id).execute()
   self.liveChat_id = _res['items'][0]['liveStreamingDetails']['activeLiveChatId']
  _publishedAt = _res['items'][0]['liveStreamingDetails']['actualStartTime']
   # youtubeライブラリをこれで使えるようになる
   self.youtube = _youtube
   self.pattern = '[T0-9-:]+'
   self.publishedAt = self.set_publishedAt_format(_publishedAt)
 
   def get_liveChat(self, _next_page_token):
     #get_liveChatにアクセスするためだけにこの関数があるのでとりあえずreturnで返している
     return self.youtube.liveChatMessages().list(
       liveChatId=self.liveChat_id,
       part='snippet',
       maxResults=2000,
       pageToken=_next_page_token  
     ).execute()

   def get_liveChat_all(self, _access_counts=2):
     _next_page_token = None
     _liveChats =
     _access_count = 0
     while 1:
       _res = self.get_liveChat(_next_page_token)
       # scope秒にコメントが0件の場合はリストを飛ばしている
      if len(_res['items']) > 0:
         _liveChats.append(_res['items'])
      _access_count += 1
     print("コメントを" + str(len(_res['items'])) + "件取得")
     _next_page_token = _res['nextPageToken']

     if _access_count == _access_counts:
       break

     print(str(
       self.convert_milisec_to_seconds(
       _res['pollingIntervalMillis'])
       ) + "秒後にアクセス")
     # timeのsleep関数で次のコメント取得まで待機させている
     time.sleep(self.convert_milisec_to_seconds(_res['pollingIntervalMillis']))
     return _liveChats
 
   def assemble_page(self, _liveChats, _scope): # _scope=何秒のスパンでデータ取得するか
     _datas =

     for _items in _liveChats:
       for _item in _items:
         # 正規表現で時間を計算できる値にする(情報を型落ちさせる)
         _dt = self.set_publishedAt_format(_item['snippet']['publishedAt'])
         # 今の時間 - 開始した時間 = 動画のコメント取得している時間(秒)を割り出している
         _dt = int*1
          # _scope秒で合計時間(秒)を割ることで時間をまとめている
         _datas.append(_dt - (_dt % _scope))
     return _datas
 
   # 同じ時間のコメントをまとめるための辞書
   def createDict(self, _datas):
     _dct = {}  
     for _data in _datas:
       _dct.setdefault(_data, 0)  
       _dct[_data] += 1
     return _dct

  def pyplot_show_liveChat(self, _liveChats, _scope):
    _datas = self.assemble_page(_liveChats, _scope=_scope)  
    _dct = self.createDict(_datas)
    _ls = []
    # はdefで関数を作らなくても引数を使うことができる
    _ls = sorted(_dct.items(), key=lambda x: x[0])
 
    #オブジェクトはintではなくstrでないと使えない
    _x = [str(dt.timedelta(hours=e[0]/3600)) for e in _ls]
    _y = [int(e[1]) for e in _ls]

    plt.plot(_x, _y)
     plt.show()
 
    _d = {}
    _d.update(zip(_x, _y))

  def set_publishedAt_format(self, _publishedAt):
    # 正規表現で必要な情報だけ取得
    return dt.datetime.strptime(re.findall(self.pattern, _publishedAt)[0],
        '%Y-%m-%dT%H:%M:%S')

  def convert_milisec_to_seconds(self, _milisec):
      return (_milisec/1000)
 
   # これを適用することで定義された戻り値をオブジェクトに付与できる。
   def __str__(self):
     return f'{self.video_id}, {self.liveChat_id}, {self.publishedAt}'
 
# モジュールを直接実行したときにだけ、実行したいコード
if __name__ == '__main__':
   _video_id = '動画のid'
   ylc = YouTubeLiveChat(_video_id)
   datas = ylc.get_liveChat_all(60) # 引数は_access_counts
   ylc.pyplot_show_liveChat(datas, _scope=60)
 
 

 

APIのKeyはconfig.json の中。

 

なんとかyoutubeの生放送のチャット欄のコメントは拾えるようになったのではなかろうか。さすがにまだここまで複雑なのはむりだけど、Youtube DATA API のリファレンスも使い方がわかってきたし学んでよかった。

 

参考にさせていただいたのはこちらの動画。

https://www.youtube.com/watch?v=_hxJlL6uVvg

 

本当にありがとうございました。

次はチャット欄からコメント拾って感情分析したり、アーカイブからコメント取得なんかもしてみたいができればRailsでやってみたいなぁ。



*1:_dt - self.publishedAt).total_seconds(

存在するかを確認できるexists?、present?、presenceメソッド

exists?メソッド

 

レコードの存在チェックだけを行う場合に使用する。

 

例えば

モデル名.exists?

でデータベースのテーブルの中にデータが存在するかを確認し、真偽をtrue,false で返してくれる。

また、

モデル名.exists?(条件)

にすると指定した条件でマッチするか確認し真偽を返してくれる。

条件にはテーブルのidや値を入れることもできる。

Purchase.exists?(3)

Purchase.exists?(item_id: @item.id)

 

 

 

present?メソッド

レコードの存在チェックを行った後にインスタンスを使って何か処理をする場合に使用する。

 

例えば

if @item.purchase.present?

 のようにインスタンスを使う場合。

 

 

exists?とpresent? は似ていてインスタンスを使うかどうか以外は同じような挙動をしていると思う。

 

presenceメソッド

present?メソッドがtrueの時、レシーバ自身を返し、 false のときは nil を返してくれる。

例えば

pry(main)> Item.find(1).present?
Item Load (0.6ms) SELECT `items`.* FROM `items` WHERE `items`.`id` = 1 LIMIT 1
=> true

[18] pry(main)> Item.find(1).presence
Item Load (0.5ms) SELECT `items`.* FROM `items` WHERE `items`.`id` = 1 LIMIT 1
=> #<Item:0x00007fed38fe7540
id: 1,
item_name: "A",
item_text: "A",
category_id: 4,
condition_id: 3,
shipping_id: 2,
sender_id: 3,
delivery_date_id: 3,
price: 1000,
user_id: 1,
created_at: Thu, 10 Dec 2020 16:38:34 UTC +00:00,
updated_at: Thu, 10 Dec 2020 16:38:34 UTC +00:00>

 というようにそのまま中身を返してくれるもの

 

コンソールやpry-railsの有用性

エラーが起きたときは"binding.pry"もしくは"rails c"で調べるとわかりやすい

 

current_userのidやparamsのitem_id、Itemモデルの値段を調べていた時のコンソールやbinding.pryの一部のコピー

rails

1] pry(main)> p current_user.id
NameError: undefined local variable or method `current_user' for main:Object
from (pry):1:in `__pry__'

[2] pry(main)> p @item.user.id
NoMethodError: undefined method `user' for nil:NilClass
Did you mean?  super
from (pry):2:in `__pry__'

[3] pry(main)> Item.find(current_user.id)
NameError: undefined local variable or method `current_user' for main:Object
from (pry):3:in `__pry__'

 

→結局current_userは定義されてないのでparamsの中の値が使えるbinding.pryを試す

 

binding.pry

11: def create
=> 12: binding.pry
  13: @user_info = UserInfo.new(user_info_params)
  14: if @user_info.valid?
   15: pay_item
   16: @user_info.save
   17: redirect_to item_path(@item)
   18: else
   19: render action: :index
   20: end
  21: end

[1] pry(#<PurchasesController>)> @item = Item.find(params[:item_id])
CACHE Item Load (0.0ms) SELECT `items`.* FROM `items` WHERE `items`.`id` = 4 LIMIT 1 [["id", 4], ["LIMIT", 1]]
↳ (pry):1:in `create'
=> #<Item:0x00007fd3e171fdb0
id: 4,
item_name: "あ",
item_text: "あ",
category_id: 3,
condition_id: 2,
shipping_id: 2,
sender_id: 3,
delivery_date_id: 2,
price: 2222222,
user_id: 1,
created_at: Sun, 13 Dec 2020 12:07:44 UTC +00:00,
updated_at: Sun, 13 Dec 2020 12:07:44 UTC +00:00>

[2] pry(#<PurchasesController>)> current_user
=> #<User id: 2, email: "s@s.s", nickname: "SSS", last_name: "ササ", first_name: "ササ", last_name_kana: "ササ", first_name_kana: "ササ", birth_date: "1932-03-03", created_at: "2020-12-10 16:39:19", updated_at: "2020-12-10 16:39:19">

[3] pry(#<PurchasesController>)> current_user.id
=> 2
[4] pry(#<PurchasesController>)> current_user[:id]
=> 2
[5] pry(#<PurchasesController>)> @item.id
=> 4
[6] pry(#<PurchasesController>)> @item[:user_id]
=> 1

[7] pry(#<PurchasesController>)> item_id
NameError: undefined local variable or method `item_id' for #<PurchasesController:0x00007fd3c322d190>
Did you mean? item_url
from (pry):7:in `create'

[8] pry(#<PurchasesController>)> :item_id
=> :item_id


[11] pry(#<PurchasesController>)> params
=> <ActionController::Parameters {"authenticity_token"=>"xc85a50Pk8j+cWdEUV28OzggqHNQFvEVCTcEYMTJYzK5UihjtmL1H5g5pfrtX+mNj9yDJNm36/cYbMMp858vRw==", "user_info"=>{"postal_code"=>"", "prefecture_id"=>"1", "city"=>"", "address_line"=>"", "building"=>"", "tel"=>""}, "controller"=>"purchases", "action"=>"create", "item_id"=>"4"} permitted: false>

[12] pry(#<PurchasesController>)> params[:item_id]
=> "4"

[13] pry(#<PurchasesController>)> Item.find(params[:item_id])[:price]
CACHE Item Load (0.1ms) SELECT `items`.* FROM `items` WHERE `items`.`id` = 4 LIMIT 1 [["id", 4], ["LIMIT", 1]]
↳ (pry):13:in `create'
=> 2222222


 

上記黄色と赤の部分の値が欲しいがために色々と試した(実際はもっといろいろ試しているが…)

 

今思うと当然当たり前のことがわかっていない。

@item = Item.find(params[:item_id])

を定義している以上、@item[:price] で値段は出てくる。

(というかそもそも@item.priceでいい)

 

VS上で試すとブラウザをリロードしなければならず、特にフォーム関連はいちいちフォームに入力したりするのが手間なので、今後はこっちで調べながら行こうと思う。

paramsについて(formオブジェクトのハッシュの外にあるパラメーターを使う)

[1] pry(#<PurchasesController>)> params

=> <ActionController::Parameters {"authenticity_token"=>"NqmRcmfoe9upA8OR3srT7HzO4o5RCGi/dyCS2jPuhUNiHNhiEF8Z/868Byp8VXlvMF4aScovG8aZmEMPqmgnoQ==", "user_info"=>{"postal_code"=>"999-9999", "prefecture_id"=>"3", "city"=>"まち", "address_line"=>"", "building"=>"", "tel"=>"0000000000"}, "token"=>"tok_2a06ee1f23eb5808850767712ec3", "controller"=>"purchases", "action"=>"create", "item_id"=>"1"} permitted: false>

[2] pry(#<PurchasesController>)>

 

 

user_infoのハッシュ外にある"item_id"と"token"を使うためにmergeメソッドで

.merge(item_id: params[:item_id], token: params[:token])

と記述。

 

これだけだとitem_idとtokenは属性値が定義されていないエラーが出るので

attr_accessor :item_id, :token

 

これで2つのオブジェクトを使えるようになった。

 

コンソールやbinding.pryを用いるようになってようやくいろんなエラーに対応できるようになってきた気がする。