- 投稿日:2020-01-15T23:48:06+09:00
Rails modelのjoin戦略についての理解を深める1歩
こちらの記事。よく参考にしてます。
https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58ここで言っているキャッシュとは何か?
同記事の最後の表についてなんとなくふわっとした理解でした。
特にキャッシュする・しないについて。
それについてまとめました。Rails modelって
そのmodelに定義されたリレーションに則ってデータを取得できます
class User < ApplicationRecord has_many :invoices ...というUserモデルがあったとして
user = User.first invoice = user.invoicesというふうに取得できますね
その時、invoicesはどのタイミングで取得する(SQLが発行される)かというとuser.invoicesとしたタイミングです
結合してみよう
先程のmodelを使って結合してみよう
joins
user = User.joins(:invoices).first user.invoices1行目のタイミングで
SELECT "users".* FROM "users" INNER JOIN "invoices" ON "invoices"."user_id" = "users"."id" ORDER BY "users"."id" ASC LIMIT $12行目で
SELECT "invoices".* FROM "invoices" WHERE "invoices"."user_id" = $1のようなSQLが発行されました。
invoicesを検索するタイミングは変わってませんね。
なるほど、これがキャッシュされたかどうかってことか。preload
user = User.preload(:invoices).first user.invoicesincludes
preload/eager_loadをいい感じに判断して使用してくれる
1行目の時点でinvoiceも検索しに行ってます
User Load (0.3ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]] Invoice Load (0.3ms) SELECT "invoices".* FROM "invoices" WHERE "invoices"."user_id" = $1 [["user_id", 1]]2行目では特にSQLは発行されていません。
キャッシュされてますね。
そして、SQLの結合は使わずに2回SELECTすることでuser.invoicesを取得してます。eager_load
user = User.eager_load(:invoices).first user.invoicesUsersに対してInvoicesをLEFT OUTER JOINで結合してました。
user.invoicesではキャッシュされてるので何も発行しません。キャッシュしないということ
joinsメソッドでは、結合先のデータを必要としないが、INNER JOINで絞り込みをしたいときに使用する。
なので、user = User.joins(:invoices).first user.invoicesのようなコードは無駄があるということ。
includesとwhereで絞り込みとキャッシュを行うべき。以上
という感じ
- 投稿日:2020-01-15T23:23:07+09:00
JavaScriptでinputのパスワードの黒丸のところを文字で表示させてみた!!
はじめに
某プログラミングスクールで、メ○○リのコピーサイトを作成しました。
チームメンバーが実装していた、ボタンを押すとパスワードの黒丸を文字として表示させる
実装を今日はやっていきたいと思います!と思ったのですが、
すでに下記の参考記事で簡単に作成できました。
パスワード表示時にマスキング有無を選択できるようにする方法なので、自分のメモ用で記載していきます。
ちなみにこんな感じのものを実装していきます。
解説
1.まずパスワードを文字として表示していきます。
(1)hamlを記載していきます。(一部のみ表示しています。)password.html.haml= form_for @resume do |f| = f.password_field :password,placeholder: '検索時に使用するパスワードを入力',class:"content_main9__form9--password",id:'password' %input#js-passcheck{type: "checkbox"} %label{for: "js-passcheck"} パスワードを表示するコードはこんな感じです。
(2)jsを記載していきます。
password.js$(function(){ var password = '#password'; #haml内のid:'password'を取得して代入。 var passcheck = '#js-passcheck'; #haml内のid:'js-passcheck'を取得して代入。 $(passcheck).change(function(){ #チェックボックスを押した際に発火するイベントを作成 if ($(this).prop('checked')){ #チェックボックスにチェックが入った場合発火 $(password).attr('type','text'); #パスワードのタイプをtypeからtextへ変更することで文字表示 } }); });チェックボックスを押した際に発火するイベントを作成しています。
そして、チェックボックスにチェックが入った時に、
input内のtypeをpasswordからtextへと変更する形となっています。
なので、input内の文字が表示されるようです。password.js$(function(){ var password = '#password'; #haml内のid:'password'を取得して代入。 var passcheck = '#js-passcheck'; #haml内のid:'js-passcheck'を取得して代入。 $(passcheck).change(function(){ #チェックボックスを押した際に発火するイベントを作成 if ($(this).prop('checked')){ #チェックボックスにチェックが入った場合発火 $(password).attr('type','text'); #パスワードのタイプをtypeからtextへ変更することで文字表示 } else { #チェックボックスからチェックがなくなった際に発火 $(password).attr('type','password'); #パスワードのタイプをpasswordへと変更 } }); });次に、elseの部分では、チェックボックスからチェックがなくなった時のイベントとなっています。
この文で、inputのtypeをtextからpasswordへと変更しているため、
文字が再び黒丸へと戻るようです。まとめ
もし間違えている理解が間違っている部分等が、ありましたらコメントをいただけると幸いです。
- 投稿日:2020-01-15T22:43:15+09:00
GemのソースをTracePointを使って効率的に読む
Rubyには様々な便利なGemがあるので有効活用しない手はないですよね。
ただ使っているGemが予期せぬ挙動をした時やドキュメントに載ってないような詳細仕様を知りたい時などにソースを読みたくなることがあります。Gemのソースを読みたい場合、GithubなどWeb上に公開されていることが多いのでブラウザでソースを見たり、ローカルにソースを落としてきて見たりすると思います。
ただ愚直にソースを読み始めるとソース量が膨大だったり、メタプロが多用されていたりなどで読解がかなり大変です。
そこでこの記事ではTracePointを使って効率的にソースを読む方法を紹介します。ソースを読んでみよう
具体例があった方が良いので、今回はrailsの
find_or_create_by
を使った場合に呼ばれるソースを探すことにしましょう。
https://github.com/rails/rails今回はローカルマシンにチェックアウトして読むことにします。
この記事では6-0-stableブランチ(2020/01/14時点)を使っています。愚直にやってみよう
該当箇所の探し方は人それぞれだと思いますが、私は最初はメソッド名でgrepすることが多いです。
今回はactiverecord配下にあることが明白なのでactiverecord/配下でgit grep find_or_create_by
しました。activerecord % git grep find_or_create_by CHANGELOG.md: `ActiveRecord::Base.find_or_create_by`/`!` by leaning on unique constraints in the database. lib/active_record/associations.rb: # def find_or_create_by_name(name) lib/active_record/associations.rb: # find_or_create_by(first_name: first_name, last_name: last_name) lib/active_record/associations.rb: # person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson") lib/active_record/associations.rb: # def find_or_create_by_name(name) lib/active_record/associations.rb: # find_or_create_by(first_name: first_name, last_name: last_name) lib/active_record/associations.rb: # def find_or_create_by_name(name) lib/active_record/associations.rb: # find_or_create_by(first_name: first_name, last_name: last_name) lib/active_record/associations.rb: # def find_or_create_by_name(name) lib/active_record/associations.rb: # find_or_create_by(first_name: first_name, last_name: last_name) lib/active_record/querying.rb: :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, lib/active_record/relation.rb: # User.find_or_create_by(first_name: 'Penélope') lib/active_record/relation.rb: # User.find_or_create_by(first_name: 'Penélope') lib/active_record/relation.rb: # User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett') lib/active_record/relation.rb: # User.find_or_create_by(first_name: 'Scarlett') do |user| lib/active_record/relation.rb: def find_or_create_by(attributes, &block) lib/active_record/relation.rb: # Like #find_or_create_by, but calls lib/active_record/relation.rb: def find_or_create_by!(attributes, &block) lib/active_record/relation.rb: # This is similar to #find_or_create_by, but avoids the problem of stale reads between the SELECT lib/active_record/relation.rb: # * While we avoid the race condition between SELECT -> INSERT from #find_or_create_by, lib/active_record/relation.rb: # Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new] lib/active_record/relation/finder_methods.rb: # Person.find_or_create_by(name: 'Spartacus', rating: 4) test/cases/finder_respond_to_test.rb: assert_not_respond_to Topic, :fail_to_find_or_create_by_title test/cases/finder_respond_to_test.rb: assert_not_respond_to Topic, :find_or_create_by_title? test/cases/finder_test.rb: assert_raise(NoMethodError) { Topic.fail_to_find_or_create_by_title("Nonexistent Title") } test/cases/finder_test.rb: assert_raise(NoMethodError) { Topic.find_or_create_by_title?("Nonexistent Title") } test/cases/nested_attributes_test.rb: ).find_or_create_by!( test/cases/relation/delegation_test.rb: :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, test/cases/relations_test.rb: def test_find_or_create_by test/cases/relations_test.rb: bird = Bird.find_or_create_by(name: "bob") test/cases/relations_test.rb: assert_equal bird, Bird.find_or_create_by(name: "bob") test/cases/relations_test.rb: def test_find_or_create_by_with_create_with test/cases/relations_test.rb: bird = Bird.create_with(color: "green").find_or_create_by(name: "bob") test/cases/relations_test.rb: assert_equal bird, Bird.create_with(color: "blue").find_or_create_by(name: "bob") test/cases/relations_test.rb: def test_find_or_create_by! test/cases/relations_test.rb: assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: "green") } test/cases/statement_cache_test.rb: def test_find_or_create_by test/cases/statement_cache_test.rb: a = Book.find_or_create_by(name: "my book") test/cases/statement_cache_test.rb: b = Book.find_or_create_by(name: "my other book")たくさんヒットしましたが、テストコードやコメントアウトされている行は無視すると下記の2行になります。
lib/active_record/querying.rb: :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, lib/active_record/relation.rb: def find_or_create_by(attributes, &block)どちらが呼ばれているのでしょうか?
これだけの情報ではわからないのでさらにソースを読む必要がありそうです。TracePointを使ってみよう
机上でソースを辿るのがツラくなってきたのでTracePointを使って呼び出されたメソッドをトレースしてみます。
動作環境は下記の通り
- Ruby: 2.6.5
- Rails: 6.0.2.1
Rails consleで下記を実行しました。
# TracePointのトレース開始 # :callを指定することでメソッド呼び出しをトレースします trace = TracePoint.trace(:call) do |tp| # 何も指定しないと全Gemがトレースされるのでactiverecordのみ出力するようにしました p tp.inspect if tp.path.include?('activerecord-6') end # トレースされるようになったのでfind_or_created_byを実行 User.find_or_create_by(name: 'hoge') # trace情報が大量に出力されます "#<TracePoint:call `find_or_create_by'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/querying.rb:21>" "#<TracePoint:call `all'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping/named.rb:26>" "#<TracePoint:call `current_scope'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping.rb:26>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping.rb:74>" "#<TracePoint:call `value_for'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping.rb:79>" "#<TracePoint:call `raise_invalid_scope_type!'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping.rb:99>" "#<TracePoint:call `base_class'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:99>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `relation'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:295>" "#<TracePoint:call `create'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/delegation.rb:114>" "#<TracePoint:call `relation_class_for'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/delegation.rb:120>" "#<TracePoint:call `relation_delegate_class'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/delegation.rb:8>" "#<TracePoint:call `arel_table'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:266>" "#<TracePoint:call `table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:195>" "#<TracePoint:call `reset_table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:226>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:195>" "#<TracePoint:call `reset_table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:226>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `table_name='@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:205>" "#<TracePoint:call `compute_table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:528>" "#<TracePoint:call `base_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:113>" "#<TracePoint:call `base_class'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:99>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `full_table_name_prefix'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:236>" "#<TracePoint:call `undecorated_table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:522>" "#<TracePoint:call `full_table_name_suffix'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:240>" "#<TracePoint:call `table_name='@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:205>" "#<TracePoint:call `type_caster'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:280>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/type_caster/map.rb:6>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/arel/table.rb:16>" "#<TracePoint:call `predicate_builder'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:276>" "#<TracePoint:call `table_metadata'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:306>" "#<TracePoint:call `arel_table'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:266>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/table_metadata.rb:7>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:7>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/basic_object_handler.rb:6>" "#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/base_handler.rb:6>" "#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/range_handler.rb:8>" "#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>" "#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/array_handler.rb:8>" "#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/array_handler.rb:8>" "#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation.rb:27>" "#<TracePoint:call `finder_needs_type_condition?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:86>" "#<TracePoint:call `descends_from_active_record?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:76>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `descends_from_active_record?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:76>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `default_scoped'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping/named.rb:57>" "#<TracePoint:call `build_default_scope'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping/default.rb:103>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `find_or_create_by'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation.rb:168>" ・・・(省略)これで最初に呼ばれているのは
lib/active_record/querying.rb#find_or_create_by
ということがサクッと分かりました。
ちなみに候補に上がっていたlib/active_record/relation.rb#find_or_create_by
も58個目に呼ばれていました。TracePointを使うとこのように速く、そして正確にメソッド呼び出しの順を知ることができます。
この情報をインプットとしてソースを読むとスムーズに処理の流れが読めます。ちなみに今回は
:call
を指定して呼び出されたメソッドだけを抽出しましたが、パラメーターや戻り値もトレースできるので、障害の原因調査などでも活躍すると思います。
詳細は"参照"に載せているリンクなどをご覧ください。参照
以下、TracePointの仕様を知る上でとても参考になったリンクです。
すごく参考になった記事
https://qiita.com/siman/items/9426ff6c113247088f7eRubyリファレンス
https://docs.ruby-lang.org/ja/latest/class/TracePoint.html
- 投稿日:2020-01-15T21:03:41+09:00
AWS EC2でCould not find pg-1.1.4 in any of the sources Run `bundle install` to install missing gems.と表示される
こんにちは@yukifreeworld12です。
EC2でエラーが出て解決したのでメモとして、そして後世の同じエラーにハマった人の為にも...1. エラー文
EC2でGitと連携しクローンしていた終盤
$ rake secret Could not find pg-1.1.4 in any of the sources Run `bundle install` to install missing gems. $ gem install pg Building native extensions. This could take a while... ERROR: Error installing pg: ERROR: Failed to build gem native extension. <略> To see why this extension failed to compile, please check the mkmf.log which can be found here: /home/name/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/extensions/x86_64-linux/2.4.0/pg-1.2.2/mkmf.log extconf failed, exit code 1と長いエラー文。
2. 解決策
解決したコマンド
sudo yum install -y postgresql-devel
このコマンドでpostgresql-develをインストールした後
bundle install
でちゃんといけました。3.しかしその後
やりたかった
rake secret
を実行したところ...$ rake secret rake aborted! Gem::LoadError: You have already activated rake 13.0.1, but your Gemfile requires rake 12.3.3. Prepending `bundle exec` to your command may solve this.
bundle exec
と書いてあるのでbundle execを付けて
bundle exec rake secret
としたところいけましたとさ。
- 投稿日:2020-01-15T20:00:42+09:00
Railsで作ったSNSでユーザの投稿へのコメント作成機能を実装する
・参考URL
https://sadah.github.io/rails-training/ja/004_comments.html今回紹介するコードは、プログラミングスクールで学んだTwitterクローンを基にしたRailsのアプリ(Micropost)への追加機能であることを前提にするとコードが読みやすくなると思います。
モデルの作成
rails g model Comment content:string user:references micropost:referencesコメントはUserとMicropostの多:多の関係性を表すため、中間テーブルが必要になります。
Userがコメントする投稿が複数あって、MicropostにはコメントしたUserが複数いるためです。マイグレーションファイル
年月日時_create_relationships.rbclass CreateComments < ActiveRecord::Migration[5.2] def change create_table :comments do |t| t.string :content t.references :user, foreign_key: true t.references :micropost, foreign_key: true t.timestamps end end endマイグレーションを忘れずに。
rails db:migrate自動作成されたコメントモデルにバリデーションを付けておきましょう。
コメントモデル
comment.rbclass Comment < ApplicationRecord belongs_to :user belongs_to :micropost validates :content, presence: true, length: { maximum: 255 } endコメントを文字なしで投稿できないように設定します。
関連モデル
Userモデル
user.rbclass User < ApplicationRecord has_many :microposts has_many :comments end一つのUserに対して多数のコメントがあるためこの関係になります。
Micropostモデル
micropost.rbclass Micropost < ApplicationRecord belongs_to :user has_many :comments, dependent: :destroy end一つのMicropostに対して多数のコメントがあるためこの関係になります。
Micropostモデルに
dependent: :destroy
を追記したことで、投稿が削除されるとそれに紐づいたコメントが削除されるようになります。
dependent: :destroy
がないと、コメントの付いた投稿を削除するときにエラーが起きます。micropost.rbhas_many :commenters, through: :comments, source: :userMicropost側から見ると、コメントしてくるユーザが多数いるので、正確にモデル作成するなら以下のコードの追記が必要かもしれません。(今回使うことはありませんでしたが、間違っていたらごめんなさい)
ルーティング
routes.rbRails.application.routes.draw do resources :microposts, only: [:create, :destroy, :show] do resources :comments, only: [:create, :destroy] end endどのMicropostに対してコメントなのかを明らかにするためにルーティングのネスト(入れ子構造)をします。
コントローラー作成
$ rails g controller comments create destroyコメントで必要な機能は作成機能(のちに削除機能)の2つだけなのでターミナルへの入力はこのようになります。
comments_controller.rbclass CommentsController < ApplicationController before_action :require_user_logged_in def create @micropost = Micropost.find(params[:micropost_id]) @comment = @micropost.comments.build(comment_params) @comment.user_id = current_user.id if @comment.save flash[:success] = '投稿にコメントしました。' redirect_back(fallback_location: root_path) else @micropost = Micropost.find(params[:micropost_id]) @comments = @micropost.comments.includes(:user) flash.now[:danger] = '投稿へのコメントに失敗しました。' render 'microposts/show' end end private def comment_params params.require(:comment).permit(:content) end endコメントはmicropostと紐づいているため
@micropost
を頭につけます。
コメントとuserを紐づけるために@comment.user_id = current_user.id
が必要です。
これがないとコメントからuserデータを得られません。else文が実行されるときrenderするファイルに
@micropost
と@comments
の
2つのインスタンス変数を渡すのは、micropostのshowファイルを表示するために必要だからです。
片方でも欠けるとエラーになります。関連コントローラー
コメント作成を投稿の詳細ページ(show)で実行できるようにします。
microposts_controller.rbclass MicropostsController < ApplicationController before_action :require_user_logged_in def show @micropost = Micropost.find(params[:id]) @comments = @micropost.comments.includes(:user) @comment = @micropost.comments.build(user_id: current_user.id) if current_user # form_with 用 end endshowページの
@micropost = Micropost.find(params[:id])
で特定の一つのmicropostを表示できるように設定します。
@comments = @micropost.comments.includes(:user)
により、対応する投稿へのコメントを一覧表示することができます。
includes(:user)
はコメントしたuserを表示するためにuserカラムを取得するメソッドです。これがないとgravatar_url
などを表示する時にuser
がnilになりエラーが起こります。
@comment
には、コメント入力フォームの表示エラーを避けるために空のコメントを入れておきます。comments_controller.rbclass CommentsController < ApplicationController before_action :require_user_logged_in def create @micropost = Micropost.find(params[:micropost_id]) @comment = @micropost.comments.build(comment_params) @comment.user_id = current_user.id if @comment.save flash[:success] = '投稿にコメントしました。' redirect_back(fallback_location: root_path) else @micropost = Micropost.find(params[:micropost_id]) @comments = @micropost.comments.includes(:user) flash.now[:danger] = '投稿へのコメントに失敗しました。' render 'microposts/show' end end private def comment_params params.require(:comment).permit(:content) end end基本的には
microposts_controller
のcreateと同じようにします。(microposts_controllerに関してはRails Tutorialなどを参考にしてください)
コメントはmicropost
に紐づくように作成したいので、対応するmicropost
の
インスタンスを記入しておきます。
@comment.user_id = current_user.id
がないとコメントにuser情報が入らないため必須です。
@micropost
と@comennts
両方のインスタンスを代入しないとrender先のshowファイルで
コメント失敗メッセージを表示できずにエラーが起こります。ルーティング
投稿詳細ページに飛ぶためのルーティングをターミナルを使って確認します。
$ rails routes
microposts#show
ページに飛ぶために必要なPrefixを確認しますmicropost GET /microposts/:id(.:format) microposts#show投稿一覧のパーシャルに以下の1行を追加して、投稿詳細ページに移動できるようにしましょう。
microposts一覧のパーシャルファイルの一部<% microposts.each do |micropost| %> 〜 <%= link_to 'Comments', micropost_path(micropost), class: 'btn btn-link btn-sm' %> 〜 <% end %>microposts#showのPrefixが
micropost
なのでmicropost_path
と記入します。
micropost一覧から特定の|micropost|
のページに飛びたいので、
micropost_path
の()内には||内のmicropost
を代入します。Viewファイル
投稿詳細ページ
コメントしたい特定のmicropostを表示するファイルです。
micropostを表示させるためには、controllerのdef show
で定義した
@micropost
のインスタンスをshowファイルに渡せるように注意しましょう。microposts/show.html.erb<ul class="list-unstyled"> <li class="media mb-3"> <img class="mr-2 rounded" src="<%= gravatar_url(@micropost.user, { size: 50 }) %>" alt=""> <div class="media-body"> <div> <%= link_to @micropost.user.name, user_path(@micropost.user) %> <span class="text-muted">posted at <%= @micropost.created_at %></span> </div> <div> <p><%= @micropost.content %></p> </div> <div class="btn-group"> <% if current_user == @micropost.user %> <%#=詳細ページで削除するとid見つからないエラー発生 link_to "Delete", @micropost, method: :delete, data: { confirm: "You sure?" }, class: 'btn btn-danger btn-sm' %> <% end %> <%= render 'favorites/favorite_button', micropost: @micropost %> </div> </div> </li> </ul> <%# コメント入力フォームのパーシャル %> <%= render 'comments/form', micropost: @micropost %> <%# コメント一覧のパーシャル %> <%= render 'comments/comments', micropost: @micropost %>投稿詳細ページでmicropostを削除すると、micropostの削除自体はできても
ルーティングエラーが起きてしまうため、現在は削除ボタンを使えないようにしています。コメント入力フォーム
form_with
のmodelに@micropost
と@comment
が入るようにしないとエラーが起こります。comments/_form.html.erb<div class="col-sm-8"> <%= form_with(model: [@micropost, @comment], local: true) do |f| %> <div class="form-group"> <%= f.text_area :content, class: 'form-control', rows: 3 %> </div> <%= f.submit 'Comment', class: 'btn btn-primary btn-block' %> <% end %> </div>コメント一覧
comments/_comments.html.erb<ul class="list-unstyled"> <% @comments.each do |comment| %> <li class="media mb-3"> <img class="mr-2 rounded" src="<%= gravatar_url(comment.user, { size: 50 }) %>" alt=""> <div class="media-body"> <div> <%= link_to comment.user.name, user_path(comment.user) %> <span class="text-muted">posted at <%= comment.created_at %></span> </div> <div> <p><%= comment.content %></p> </div> <div class="btn-group"> <% if current_user == comment.user %> <%#= link_to "Delete", micropost_comment_path(comment), method: :delete, data: { confirm: "You sure?" }, class: 'btn btn-danger btn-sm' %> <% end %> </div> </div> </li> <% end %> <%#= paginate comments %> </ul>コメント削除機能は正しいルーティングができていないので、まだ未実装です。
- 投稿日:2020-01-15T20:00:42+09:00
Railsで作ったSNSアプリでユーザの投稿へのコメント作成機能を実装する
・参考URL
https://sadah.github.io/rails-training/ja/004_comments.html今回紹介するコードは、プログラミングスクールで学んだTwitterクローンを基にしたRailsのアプリ(Micropost)への追加機能であることを前提にするとコードが読みやすくなると思います。
モデルの作成
rails g model Comment content:string user:references micropost:referencesコメントはUserとMicropostの多:多の関係性を表すため、中間テーブルが必要になります。
Userがコメントする投稿が複数あって、MicropostにはコメントしたUserが複数いるためです。マイグレーションファイル
年月日時_create_relationships.rbclass CreateComments < ActiveRecord::Migration[5.2] def change create_table :comments do |t| t.string :content t.references :user, foreign_key: true t.references :micropost, foreign_key: true t.timestamps end end endマイグレーションを忘れずに。
rails db:migrate自動作成されたコメントモデルにバリデーションを付けておきましょう。
コメントモデル
comment.rbclass Comment < ApplicationRecord belongs_to :user belongs_to :micropost validates :content, presence: true, length: { maximum: 255 } endコメントを文字なしで投稿できないように設定します。
関連モデル
Userモデル
user.rbclass User < ApplicationRecord has_many :microposts has_many :comments end一つのUserに対して多数のコメントがあるためこの関係になります。
Micropostモデル
micropost.rbclass Micropost < ApplicationRecord belongs_to :user has_many :comments, dependent: :destroy end一つのMicropostに対して多数のコメントがあるためこの関係になります。
Micropostモデルに
dependent: :destroy
を追記したことで、投稿が削除されるとそれに紐づいたコメントが削除されるようになります。
dependent: :destroy
がないと、コメントの付いた投稿を削除するときにエラーが起きます。micropost.rbhas_many :commenters, through: :comments, source: :userMicropost側から見ると、コメントしてくるユーザが多数いるので、正確にモデル作成するなら以下のコードの追記が必要かもしれません。(今回使うことはありませんでしたが、間違っていたらごめんなさい)
ルーティング
routes.rbRails.application.routes.draw do resources :microposts, only: [:create, :destroy, :show] do resources :comments, only: [:create, :destroy] end endどのMicropostに対してコメントなのかを明らかにするためにルーティングのネスト(入れ子構造)をします。
コントローラー作成
$ rails g controller comments create destroyコメントで必要な機能は作成機能(のちに削除機能)の2つだけなのでターミナルへの入力はこのようになります。
comments_controller.rbclass CommentsController < ApplicationController before_action :require_user_logged_in def create @micropost = Micropost.find(params[:micropost_id]) @comment = @micropost.comments.build(comment_params) @comment.user_id = current_user.id if @comment.save flash[:success] = '投稿にコメントしました。' redirect_back(fallback_location: root_path) else @micropost = Micropost.find(params[:micropost_id]) @comments = @micropost.comments.includes(:user) flash.now[:danger] = '投稿へのコメントに失敗しました。' render 'microposts/show' end end private def comment_params params.require(:comment).permit(:content) end endコメントはmicropostと紐づいているため
@micropost
を頭につけます。
コメントとuserを紐づけるために@comment.user_id = current_user.id
が必要です。
これがないとコメントからuserデータを得られません。else文が実行されるときrenderするファイルに
@micropost
と@comments
の
2つのインスタンス変数を渡すのは、micropostのshowファイルを表示するために必要だからです。
片方でも欠けるとエラーになります。関連コントローラー
コメント作成を投稿の詳細ページ(show)で実行できるようにします。
microposts_controller.rbclass MicropostsController < ApplicationController before_action :require_user_logged_in def show @micropost = Micropost.find(params[:id]) @comments = @micropost.comments.includes(:user) @comment = @micropost.comments.build(user_id: current_user.id) if current_user # form_with 用 end endshowページの
@micropost = Micropost.find(params[:id])
で特定の一つのmicropostを表示できるように設定します。
@comments = @micropost.comments.includes(:user)
により、対応する投稿へのコメントを一覧表示することができます。
includes(:user)
はコメントしたuserを表示するためにuserカラムを取得するメソッドです。これがないとgravatar_url
などを表示する時にuser
がnilになりエラーが起こります。
@comment
には、コメント入力フォームの表示エラーを避けるために空のコメントを入れておきます。comments_controller.rbclass CommentsController < ApplicationController before_action :require_user_logged_in def create @micropost = Micropost.find(params[:micropost_id]) @comment = @micropost.comments.build(comment_params) @comment.user_id = current_user.id if @comment.save flash[:success] = '投稿にコメントしました。' redirect_back(fallback_location: root_path) else @micropost = Micropost.find(params[:micropost_id]) @comments = @micropost.comments.includes(:user) flash.now[:danger] = '投稿へのコメントに失敗しました。' render 'microposts/show' end end private def comment_params params.require(:comment).permit(:content) end end基本的には
microposts_controller
のcreateと同じようにします。(microposts_controllerに関してはRails Tutorialなどを参考にしてください)
コメントはmicropost
に紐づくように作成したいので、対応するmicropost
の
インスタンスを記入しておきます。
@comment.user_id = current_user.id
がないとコメントにuser情報が入らないため必須です。
@micropost
と@comennts
両方のインスタンスを代入しないとrender先のshowファイルで
コメント失敗メッセージを表示できずにエラーが起こります。ルーティング
投稿詳細ページに飛ぶためのルーティングをターミナルを使って確認します。
$ rails routes
microposts#show
ページに飛ぶために必要なPrefixを確認しますmicropost GET /microposts/:id(.:format) microposts#show投稿一覧のパーシャルに以下の1行を追加して、投稿詳細ページに移動できるようにしましょう。
microposts一覧のパーシャルファイルの一部<% microposts.each do |micropost| %> 〜 <%= link_to 'Comments', micropost_path(micropost), class: 'btn btn-link btn-sm' %> 〜 <% end %>microposts#showのPrefixが
micropost
なのでmicropost_path
と記入します。
micropost一覧から特定の|micropost|
のページに飛びたいので、
micropost_path
の()内には||内のmicropost
を代入します。Viewファイル
投稿詳細ページ
コメントしたい特定のmicropostを表示するファイルです。
micropostを表示させるためには、controllerのdef show
で定義した
@micropost
のインスタンスをshowファイルに渡せるように注意しましょう。microposts/show.html.erb<ul class="list-unstyled"> <li class="media mb-3"> <img class="mr-2 rounded" src="<%= gravatar_url(@micropost.user, { size: 50 }) %>" alt=""> <div class="media-body"> <div> <%= link_to @micropost.user.name, user_path(@micropost.user) %> <span class="text-muted">posted at <%= @micropost.created_at %></span> </div> <div> <p><%= @micropost.content %></p> </div> <div class="btn-group"> <% if current_user == @micropost.user %> <%#=詳細ページで削除するとid見つからないエラー発生 link_to "Delete", @micropost, method: :delete, data: { confirm: "You sure?" }, class: 'btn btn-danger btn-sm' %> <% end %> <%= render 'favorites/favorite_button', micropost: @micropost %> </div> </div> </li> </ul> <%# コメント入力フォームのパーシャル %> <%= render 'comments/form', micropost: @micropost %> <%# コメント一覧のパーシャル %> <%= render 'comments/comments', micropost: @micropost %>投稿詳細ページでmicropostを削除すると、micropostの削除自体はできても
ルーティングエラーが起きてしまうため、現在は削除ボタンを使えないようにしています。コメント入力フォーム
form_with
のmodelに@micropost
と@comment
が入るようにしないとエラーが起こります。comments/_form.html.erb<div class="col-sm-8"> <%= form_with(model: [@micropost, @comment], local: true) do |f| %> <div class="form-group"> <%= f.text_area :content, class: 'form-control', rows: 3 %> </div> <%= f.submit 'Comment', class: 'btn btn-primary btn-block' %> <% end %> </div>コメント一覧
comments/_comments.html.erb<ul class="list-unstyled"> <% @comments.each do |comment| %> <li class="media mb-3"> <img class="mr-2 rounded" src="<%= gravatar_url(comment.user, { size: 50 }) %>" alt=""> <div class="media-body"> <div> <%= link_to comment.user.name, user_path(comment.user) %> <span class="text-muted">posted at <%= comment.created_at %></span> </div> <div> <p><%= comment.content %></p> </div> <div class="btn-group"> <% if current_user == comment.user %> <%#= link_to "Delete", micropost_comment_path(comment), method: :delete, data: { confirm: "You sure?" }, class: 'btn btn-danger btn-sm' %> <% end %> </div> </div> </li> <% end %> <%#= paginate comments %> </ul>コメント削除機能は正しいルーティングができていないので、まだ未実装です。
- 投稿日:2020-01-15T16:47:41+09:00
Web系開発周りの自分用まとめ:学習:Ruby
0. 概要
Progateを使ってRubyを学んでみての感想をまとめてみた。
他言語と比較しての感想とかも混ざってる。
今後Rubyを書いてみて、あれ?これどうするんだっけ?といったときに見返す用。1. 感想
- 演算子
- インクリメント演算子++が使えない
- 条件式
- elseifじゃなくてelsifなのにびっくり
- 括弧
- くくらない言語なのでちょっと違和感(VBAとかもだけど)
- 加えて、多言語での中括弧終わりに相当するのが
end
- 文字列内""くくりでの変数展開が#{}なのが他でみたことない感じ
- 文字列と数値の+での結合ができない言語なので上をつかわないとダメ
- each doに違和感
- 慣習(と思われるもの「)
- 変数名はスネークケース
- 配列は複数形、each doでのループ回す時のループ変数は単数形
- タブ(インデント)は半角スペース2つ
- bool値を返すメソッド名の末尾は?をつける
- クラス名は大文字開始(というか言語仕様的に大文字以外できない)
- メソッドはキーワード引数で定義して、呼び出し側の可読性を向上させるっぽい
- ハッシュ(連想配列)
- 文字列と同じような(とはいえ異なる)シンボル(:<任意の文字列>)というものをキーにすることもできるがよくわからん・・・
- シンボルはハッシュのキーとしてよく使い、通常は{"key1" => value1, "key2" => value2}と書かないといけないのを、省略して{key1: value1, key2: value2}と書くことができて楽。これは便利!
- nil
- オブジェクトが存在しない、はRubyではnullではなくnil
- 条件式でnilはfalseと扱われ、それ以外の値はtrueと扱われる仕様。へぇー
- メソッド
- 定義するときには、
def
を頭につける。なんかCのマクロと混同しちゃう。- 定義時に引数の名前を
ref:
のように:
で終わらせると、呼び出し側でその引数をつけて呼び出させることができる。キーワード引数と呼ぶ。呼び出し側のコードの可読性があがりそう!- ちなみにメソッド内でのキーワード引数へのアクセスは:が取れてるバージョン。へぇ・・・
- 引数なしの場合は呼び出し時に()は不要。つけてもうごくけど
- クラス
- クラス名
- 大文字縛り。コーディング規約的に一般的なのでいいね。
- メンバ
- インスタンス変数と呼ぶ.変な感じ
attr_accessor :member1
のようにシンボル的な書き方をする.変な感じ。attr_accessorもなんか長ったらしー- インスタンス変数へのアクセスは
.member1
のようにする。えぇ・・・:member1
じゃないんかーい- ※attr_accessor :member1はメンバ変数の宣言ではないらしい。こう書くと、
@member1
というインスタンス変数への以下のpublicなgetter, setterが定義される特殊な書き方らしい。だから、インスタンス化した後に.member1
で外部からアクセスすると、C#やJavaのような言語でpublicなmember1というインスタンス変数へ直接アクセスするかのような使い方ができる、というものらしい。
def member1
def member1=(引数)
- メソッド
- メンバで
attr_accessor
だったからattr_method
とか必要なのかと思いきや、ふつーにdef <メソッド名>
ええ・・一貫性がなーい- クラスに定義したメソッドはインスタンスメソッドという。なんでなんや・・・クラスメソッドでいいじゃん。メンバの方もインスタンス変数だったけどクラス変数でいいじゃん・・・
- と思いきや!クラスにはインスタンス化した後に呼び出せるメソッドとインスタンス化前に呼び出せるメソッドがあり、まだインスタンス化していないときに使えるメソッドのことをクラスメソッドと呼ぶ!なるほど!
- なお、インスタンスメソッドの主な使いみちはインスタンス化する時に特別な処理をさせたいとき、みたい。
- 自身のインスタンス変数を使うときには
self.<インスタンス変数名>
。なんでthis.<インスタンス変数名>
じゃないんや・・・
- 厳密
- コンストラクタはinitialize()。クラス自身の名前じゃないんですねぇ・・・・
- 継承
子クラス < 親クラス
で定義する.:
じゃなくて<
なんですね。へぇー- 子クラスのメソッドの中で
super()
とすると親クラスの同名のメソッドを呼び出すことができる。子クラス側で親クラスと同じ処理になる場合は、こうしておくと処理の修正をしなければならないときに親クラス側を修正するだけで済みそう- コンソールからの入力
- gets.chompでやる。へぇー
- gets.chompでうけとった値は絶対に文字列。なので、文字列以外の値として扱いたい場合は、メソッドを実行する。たとえば
get.chomp.to_i
みたいな。なるほどねー- 日付を使いたい
- Rutyで用意されているDateクラスを使う
require "date"
が必要。require "./date"
ではダメ。へぇー- コンストラクタは(年, 月, 日)になっている。
- Date.new()でなく、Date.todayとすると今日の日付でインスタンス化される。へぇー
- 投稿日:2020-01-15T16:14:17+09:00
Rails(Ruby)アプリからスプレッドシートに書き込み
GCP側のでやること
GCPのコンソール(
https://cloud.google.com
)から、
・「Google Drive API」と「Google Sheets API」を有効化
注意:WEBとかiOSとかではなく、「Other」を選ぶ
Rails(Ruby)側
gemを追加
Gemfilegem 'google_drive'認証情報を記載するconfig.jsonを作成
touch config.json
config.json{ "client_id": "xxxxxxx-xxxxxxxxxxxxxxxxx.apps.googleusercontent.com", "client_secret": "xxxxxxxXXxxxxxxxxxxxxxxxxx" }キーからスプレッドシートを取得。スプレッドシートのURLのうち、
https://docs.google.com/spreadsheets/d/xxx
このxxxの部分がスプレッドシートのキーになる。require "google_drive" session = GoogleDrive::Session.from_config("config.json") sheet = session.spreadsheet_by_key("xxxxxxxxxxxxxxxxxxxxxxxxxxx").worksheets[0] # 書き込み sheet[1,1] = "From API" sheet.save実行すると、認証が始まるので表示されるURLに対象アカウントでログインして認証。
書き込まれるかどうか確認
- 投稿日:2020-01-15T15:53:30+09:00
【初心者向け】Rubyで超簡単ドリンク注文アプリケーション(リファクタリングver)
@scivola さんよりアドバイスいただき、DRYな部分を整理しました。
こちらの記事は
https://qiita.com/pontarou194/items/9e91b40dcc2da608dd24
の続きです。もともとは、メソッドが4つありました。。。。
otyaメソッド
coffeeメソッド
beerメソッド
saidaメソッドこれを一つにまとめられました。
もともとはこんな感じ。これがメソッドの多いアプリケーション(進化前)
def otya(otya) puts "あなたが選んだのはお茶" puts "お茶の値段は100円です" puts "いくつ注文しますか?" order_quantity = gets.to_i total_price = order_quantity * otya puts "合計#{total_price}円です" end def coffee(coffee) puts "あなたが選んだのはコーヒー" puts "コーヒーの値段は200円です" puts "いくつ注文しますか?" order_quantity = gets.to_i total_price = order_quantity * coffee puts "合計#{total_price}円です" end def beer(beer) puts "あなたが選んだのはビール" puts "ビールの値段は300円です" puts "いくつ注文しますか?" order_quantity = gets.to_i total_price = order_quantity * beer puts "合計#{total_price}円です" end def saida(saida) puts "あなたが選んだのは三ツ矢サイダー" puts "サイダーの値段は300円です" puts "いくつ注文しますか?" order_quantity = gets.to_i total_price = order_quantity * saida puts "合計#{total_price}円です" end otya = 100 coffee = 200 beer = 300 saida = 400 puts "何を飲みたいですか?" drink_menu = ["お茶","コーヒー","ビール","三ツ矢サイダー"] drink_menu.each.with_index(1) do |drink_name, number| puts "#{number}:#{drink_name}" end case gets.to_i when 1 otya(otya) when 2 coffee(coffee) when 3 beer(beer) when 4 saida(saida) else puts "無効な入力値です" end結論、メソッドを一つにまとめると、下記のようなコードとなりました(進化後)
def order_drink(drink_name, drink_price) puts "あなたが選んだのは#{drink_name}" puts "#{drink_name}の値段は#{drink_price}円です" puts "いくつ注文しますか?" order_quantity = gets.to_i total_price = order_quantity * drink_price puts "合計#{total_price}円です" end otya = 100 coffee = 200 beer = 300 saida = 400 puts "何を飲みたいですか?" drink_menu = ["お茶","コーヒー","ビール","三ツ矢サイダー"] drink_menu.each.with_index(1) do |drink_name, number| puts "#{number}:#{drink_name}" end case gets.to_i when 1 order_drink("お茶", otya) when 2 order_drink("コーヒー", coffee) when 3 order_drink("ビール", beer) when 4 order_drink("三ツ矢サイダー", saida) else puts "無効な入力値です" end@scivola さん完璧です。アドバイスいただきありがとうございました。
なんと、36行もコードを圧縮できました!@scivola さんがおっしゃるお通り、ポイントは以下の通りです。
・DRYの原則にあてこむ(繰り返しは避ける、似た表現が繰り返される場合もなるべく避ける)
・メソッド名をわかりやすく↓
(自分で見返したとき用に)もっとわかりやすくします。擬似コードで書いててわかるように、
def ①お茶が選ばれた場合、値段を表示 ②注文数を入力 ③合計額を表示 end def ①コーヒーが選ばれた場合、値段を表示 ②注文数を入力 ③合計額を表示 end def ①ビールが選ばれた場合、値段を表示 ②注文数を入力 ③合計額を表示 end def ①サイダーが選ばれた場合、値段を表示 ②注文数を入力 ③合計額を表示 endこの①〜③とメソッド名がかなり似ている。
これ、まとめたほうがすっきりしてわかりやすい。特に
①のあなたが選んだのは○○
→共通化できる②○○の値段は○○円です
→共通化できる③合計○○円です
→共通化できるあとは、いくつ注文しますかとか、gets.to_iとかを一緒に入れて完成。
def order_drink(drink_name, drink_price) puts "あなたが選んだのは#{drink_name}" puts "#{drink_name}の値段は#{drink_price}円です" puts "いくつ注文しますか?" order_quantity = gets.to_i total_price = order_quantity * drink_price puts "合計#{total_price}円です" end otya = 100 coffee = 200 beer = 300 saida = 400引数の使い方について
今回勉強になったポイントは、引数の使い方
複数の引数を使用するケースをあまり経験したことがなかった・・・・・・なので、いざ自分でコードを組み立てていくとなると、
何がどうなってるのやら??
っという状況でした。下記のコードでは、drink_nameが第一引数、drink_priceが第二引数ですね。
コードにポイントをコメントで記述します。def order_drink(drink_name, drink_price) #このdrink_nameとdrink_priceが仮引数 puts "あなたが選んだのは#{drink_name}" puts "#{drink_name}の値段は#{drink_price}円です" puts "いくつ注文しますか?" order_quantity = gets.to_i total_price = order_quantity * drink_price puts "合計#{total_price}円です" endcase gets.to_i when 1 order_drink("お茶", otya) #お茶とotyaが実引数 when 2 order_drink("コーヒー", coffee) #上記に習って同じ when 3 order_drink("ビール", beer) #上記に習って同じ when 4 order_drink("三ツ矢サイダー", saida) #上記に習って同じ else puts "無効な入力値です" end仮引数と実引数は一致しなくても良いというのがありましたね。なので
これでも下記のようにcase文の実引数をいじって、otya = 100 等の変数宣言を省略することもできます。case gets.to_i when 1 order_drink("お茶", 100) when 2 order_drink("コーヒー", 200) when 3 order_drink("ビール", 300) when 4 order_drink("三ツ矢サイダー", 400) else puts "無効な入力値です" endメソッド名をわかりやすく。
order_drinkというメソッド名はドリンクをオーダーするためだけなのか、計算もしてくれるのかがわかりずらいですよね。
なので、注文管理メソッドという名前をつけてみました。
グーグル翻訳すると、order_managementとなってましたので、そのまま名付けました。最後に、コードをまとめました、こんな感じです。
def order_management(drink_name, drink_price) puts "あなたが選んだのは#{drink_name}" puts "#{drink_name}の値段は#{drink_price}円です" puts "いくつ注文しますか?" order_quantity = gets.to_i total_price = order_quantity * drink_price puts "合計#{total_price}円です" end puts "何を飲みたいですか?" drink_menu = ["お茶","コーヒー","ビール","三ツ矢サイダー"] drink_menu.each.with_index(1) do |drink_name, number| puts "#{number}:#{drink_name}" end case gets.to_i when 1 order_management("お茶", 100) when 2 order_management("コーヒー", 200) when 3 order_management("ビール", 300) when 4 order_management("三ツ矢サイダー", 400) else puts "無効な入力値です" end引数や変数は便利だなぁと改めて感じました。
また、DRYを原則とすることで、「保守しやすっ!」っと思いました。ヒアドキュメントはもう少し勉強必要でした、、、
@scivola さんありがとうございました。
@scivola から、さらにアドバイスいただきました。
二重配列を使用して、case文を省略し、コードを短くできるよう見直しました。
def order_management(drink_name, drink_price) puts "あなたが選んだのは#{drink_name}" puts "#{drink_name}の値段は#{drink_price}円です" puts "いくつ注文しますか?" order_quantity = gets.to_i total_price = order_quantity * drink_price puts "合計#{total_price}円です" end puts "何を飲みたいですか?" drink_menu = ["お茶","コーヒー","ビール","三ツ矢サイダー"] drink_menu.each.with_index(1) do |drink_name, number| puts "#{number}:#{drink_name}" end drinks = [ ["お茶", 100], ["コーヒー", 200], ["ビール", 300], ["三ツ矢サイダー", 400] ] input = gets.to_i drink_name = drinks[input - 1][0] drink_price = drinks[input -1][1] order_management(drink_name, drink_price)(おそらく合っている?
動作は問題なく動きました)2次元配列について参考とさせていただきました。
https://pikawaka.com/ruby/arraycase文使わない方法があるとは・・・・
実際かいてみると、単純構造でした。@scivola さん、アドバイスいただきありがとうございます。
- 投稿日:2020-01-15T15:44:30+09:00
railsでレコードに保存している「連続した改行」を改行を保持したままviewに表示する
環境
Rails 5.2.3
Ruby 2.6.3実現したいこと
例えばArticleモデルのcontentカラムに保存している、
「あいうえおかきこけこ」
をviewに表示したい。解決策
safe_join関数を利用し、改行文字をbrに変換する。
simple_format 関数もあるのだが連続改行が認識されないため、
safe_join関数を利用する。show.html.haml= safe_join(@article.content.split("\n"), tag(:br))参照
https://api.rubyonrails.org/classes/ERB/Util.html#method-c-html_escape
- 投稿日:2020-01-15T15:39:14+09:00
【Ruby初心者向け】打順の組み方を変えて得点の入り方をシミュレーションして比較してみた
概要
野球が好きなので、打順の最適な組み方をついつい考えてしまいます。
ある検証で「強打者と弱い打者を交互に並べる」よりも「強打者を下位打線に固める」方が得点が入りやすいという結果が出たというのを知り、単純化したモデルで検証してみようと思い試してみました。
本格的な検証ではないので、Rubyを使って何か簡単な検証や計算を行ってみたいという人に大雑把な流れだけわかってもらえれば嬉しいです。
前提条件
- 3割打者5人と2割打者4人の2タイプを用意
- 打者は凡打か単打のみを打つ
- 凡打においてランナーは進塁せず、3連打で1点が入る
- 3連打以上では安打のたびに1得点を追加
- エラーや四死球は簡略化のため考慮しない
- 盗塁や送りバントはセイバーメトリクスで推奨されないので行わない
- 1万試合回して合計得点を競う
この超簡略化した野球で2タイプの打者の打順を入れ替えて、どれだけ得点が入るかを検証します。ただ四死球と長打を全て排除するのはあまりにも酷なので、あとでそれらを考慮したコードも紹介していきます。
打順の組み方は
- 上位5人に3割打者を固める
- 3割打者と2割打者を交互におく
- 3割打者を5-9番の下位に固める
の3種類とします。
変数を宣言
baseball.rb#1-9番の打者の打順を記録した配列 average=[0.3,0.3,0.3,0.3,0.3,0.2,0.2,0.2,0.2] #合計得点 sumScore=0 #打席に立っている打者の打順 batter=1 #アウトカウント outCount=0 #塁上のランナーの数 runner=0 #アウトカウント iningCount=01イニングを終わらせてみる
baseball.rbloop { hit = rand(1..100) hit = hit/100.0 #乱数を使って単打か凡打を記録 if hit <= average[batter-1] #走者を一人増やす runner+=1 if runner>=3 #走者が2人以上たまっていたらランナーを本塁に返す runner=2 sumScore+=1 end else #凡打なのでアウトカウントを増やすだけ outCount+=1 if outCount == 3 #3アウトチェンジしてループを抜ける runner=0 outCount=0 break end end #次のバッターに交代 batter+=1 #9番打者を打ち終わったら一番に戻る batter = (batter-1)%9 + 1 }1万試合を回してみる
1万試合なので90000イニング繰り返して、何点入るかを検証します。ただ9万回回すのではなく、9回が終わったら打順を1番に戻す処理を忘れないようにします。
baseball.rb90000.times { iningCount+=1 loop { hit = rand(1..100) hit = hit/100.0 #乱数を使って単打か凡打を記録 if hit <= average[batter-1] #走者を一人増やす runner+=1 if runner>=3 #走者が2人以上たまっていたらランナーを本塁に返す runner=2 sumScore+=1 end else #凡打なのでアウトカウントを増やすだけ outCount+=1 if outCount == 3 #3アウトチェンジしてループを抜ける runner=0 outCount=0 break end end #次のバッターに交代 batter+=1 #9番打者を打ち終わったら一番に戻る batter = (batter-1)%9 + 1 } if iningCount == 9 #9回が終了したらイニングと打順をリセットする iningCount=0 batter=1 end } ##最後に合計得点を出力 puts sumScore結果
打線の組み方 上位打線に強打者 下位打線に強打者 交互に並べる 1試合平均得点 1.57 1.52 1.50 という結果になりました。ものすごく簡単な検証でしたが、強打者は分散して配置するよりも、下位打線でも良いから連続させて打順を組む方が効果的ということがわかります。
今回のルールでは連打が出ないと点が入らないので、チャンスでいかに打てるかが大事です。強打者が作ったチャンスを平凡な打者が潰してしまうのを減らすためには、強打者は集中させるのがいいという直感と合致しますね。
おまけ 得点が少なすぎるので調整
上のルールにおける検証結果では強打者を上位に並べても1試合あたり1,2点しか入らないということになり、あまりにも少なすぎるので、もう少し実際の野球に近づけてみました。
近年の野球において特に重視されるのは、出塁率と長打率です。野球は27個のアウトを取られるまでは永遠に攻撃を続けられるスゴロクゲームと捉えることができます。
スゴロクに置いて重要なのは
- いかにアウトにならず塁に出られるか
- 長打によって一度にたくさん駒を進められるか
の2点です。実際の野球ではヒットでなくても四球で塁に出ることができますし、本塁打が出ればその時点で得点が入ります。
プロ野球で打率3割を残せるのはごく一部ですが、出塁率3割はレギュラークラスなら超えておかなければならない数字です。
コードで本塁打と四球を反映させてみた
ここからは打線の組み方というよりも本物の野球の点の入り方に近づけることを目標とします。四球は単打と同じ扱いとし、全員の出塁率を一律に3割2分とします。3回に1回塁に出る計算になります。
また、出塁のうちの10本の1本はホームランが出て、ランナーを全員返すと仮定しましょう。(この辺りの仮定は実際のプロ野球のパリーグのデータを用いています)
baseball.rb#チーム全員の出塁率を宣言する average=[0.32,0.32,0.32,0.32,0.32,0.32,0.32,0.32,0.32] sumScore=0 batter=1 outCount=0 runner=0 iningCount=0 90000.times { iningCount+=1 loop { hit = rand(1..100) hit = hit/100.0 if hit <= average[batter-1] runner+=1 if rand(1..10) == 1 #10回に一回はホームランが出て全員が帰りランナーを0に戻す sumScore+=runner runner=0 elsif runner>=3 runner=2 sumScore+=1 end else outCount+=1 if outCount == 3 runner=0 outCount=0 break end end batter+=1 batter = (batter-1)%9 + 1 } if iningCount == 9 iningCount=0 batter=1 end } puts sumScore四球とホームランを考慮した結果、1試合あたりの平均得点は4.4点となりました。DH制を利用しているパリーグの昨年の平均得点が4.31点だったので、大雑把なモデルの割にはかなり近い感じになりました。
まとめと展望
出塁率と長打率さえ調整すれば、実際の野球と近い平均得点を出せることがわかりました。ここからさらに実際の野球に近づけるには、以下のことをする必要があるでしょう。
- ホームランバッターを上位におく
- 3連続出塁で1点も入らない場合も想定する
- 打者の出塁率の差を考慮する
- 2塁打を考慮する(3塁打は少なすぎて誤差の範囲)
バント、盗塁はセイバーメトリクスでは推奨されていませんし、エラーも出塁と同値と考えて良いのかもしれません。コードが簡潔に保てる範囲内で色々試してみたいと思います。
- 投稿日:2020-01-15T14:25:54+09:00
アプリにLINE Pay決済を導入する簡易手引き(LINE Pay API - v3)
株式会社OKANで「おかんPay」の開発(フロント/バックエンド)に携わっているsuzu_Dと申します。
このアプリを簡単に説明すると、「オフィスおかん」という置型の社食サービスで、小銭を使わずキャッシュレス決済ができるアプリとなっております。
今まで当アプリではクレジットカードを登録し支払う、という方法でのみの決済でしたが、
2019年12月の目玉アップデートとして、当アプリ経由でLINE Pay決済ができる機能をリリースしました。今回は、技術的なお話というよりLINE Payを組み込む際に、今現在ネットの情報だけだと自分的には不足している部分があり
「そもそもLINE Pay APIに通信できない・・・」といった事が多発し、LINE Payの技術スタッフの方と何回かやり取りさせていただいたので、
その中で改めて知った、LINE Payの知見を、自分なりに纏めてアウトプットし、
もし皆さんが関わっているプロダクトで「LINE Pay決済」を導入する機会がありましたら
少しでも参考になればなと思い、本記事を寄稿しました。本文献の購読対象者と、目的・出来るようになること
購読対象者
・開発中のネイティブアプリやWEBアプリでLINE Pay決済を組み込もうとしている方
・実際にいまLINE Payを組込中で「そもそもLINE Pay APIと通信がうまくいかない!」と嘆いている方目的・出来るようになること
・自プロダクトでLINE Payで決済ができる仕組みが、最低限なんとなく理解できて、LINE Pay APIとREST通信出来るようになる。
LINE Pay APIのバージョンと参考ガイドについて
当記事で対象としているLINE Pay APIのバージョン
当記事で対象としているLINE Pay APIのバージョンは
v3(2019/12 時点)
の情報になります、v3以前のバージョンや、これ以降APIのバージョンが上がった際
参考にならない記述が出る可能性があることを、ご了承願います。当記事で参考にしているLINE Pay API ガイド
https://pay.line.me/jp/developers/apis/onlineApis?locale=ja_JP
LINE Pay APIの環境について
API通信に使う環境として、主に使うのは以下2つになると思います。
- 本番環境
- テスト加盟店環境
API通信に使う環境について
本番環境
加盟店申込後に審査が完了してから利用可能になる環境です。
実際のお金のやりとりをする環境になります。
加盟店申込後にLINE Pay側から連絡があり、申請したアカウント(メールアドレス)にて
LINE PayのMy Pageからログインを行うと、取引の履歴を見たり、決済のキャンセル・返金を行うことが出来ます。
また、本番のAPI通信に必要なキーの情報を見ることが出来ます。テスト加盟店環境
Sandbox生成後利用可能になる環境です。
このページにて登録したメールアドレス宛に、テスト加盟店環境にログインできるアカウントが通知されます。
通知されたアカウントでLINE PayのMy Pageからログインを行うと、テスト加盟店環境で行った取引の履歴を見たり、決済のキャンセル・返金を行うことが出来ます。また、テスト加盟店環境にてAPI通信をするために必要なキーの情報を見ることが出来ます。
こちらも、本番環境同様、決済処理の通信を行った場合、実際のお金のやり取りが発生する(LINE Pay残高が減る)のですが、毎日23:55と2:55に支払いを行った決済に対して自動的に決済のキャンセル・返金処理が行われます。
アプリに決済機能を実装し、LINE Pay決済をテスト的につかう場合はこちらの環境を使うことをおすすめします。各環境にAPI通信するために必要なエンドポイントとキーについて
API通信する際のエンドポイント
エンドポイント(本番、テスト加盟店環境 共通) https://api-pay.line.me 例 - Request APIをコールする場合のエンドポイント https://api-pay.line.me/v3/payments/request各環境にAPI通信するためのキー、Channel IDとChannel SecretKeyについて
LINE Pay API通信する際、必要な認証情報としてChannel IDとChannel SecretKeyを使用することになります。
このキーが本番環境、もしくはテスト加盟店環境の違いになるので、本番環境用とテスト用で接続先の設定ファイルを作る際は間違えないようにしてください(テスト用の設定ファイルに本番用のChannel IDとChannel SecretKeyを登録してしまった等の間違いをしないようにしてください。)
確認方法は以下になります。①本番環境、もしくはテスト加盟店環境のアカウントにてLINE PayのMy Pageからログインを行う
↓
②ログイン後画面にて「決済連動管理」>「連動キー管理」と進み、ログイン用のパスワードを入力する
↓
③Channel ID とChannel Secret Keyを確認することが出来る(このChannel ID とChannel Secret Keyは、絶対外部に漏らさないようにしてください。)
このChannel IDとChannel SecretKeyによって、どの加盟店での決済かを判別しているようです、APIのリクエストを投げる際、共通ヘッダーにキーを設定することによって、通信が可能となります、このキーの設定が間違っている場合に
https://api-pay.line.me
に通信を行うと以下のようなレスポンスが返ってきます、{ "returnCode": "1106", "returnMessage": "Header information error. request verification Failed" }このキーを共通ヘッダーに設定する際、少々特別な処理が必要になります、詳しい説明は後述する共通ヘッダーの設定方法を参照してください。
※SandBoxについて
本番環境、テスト加盟店環境 の双方どちらも「SandBox」で通信できる環境があります。
こちらの環境は以下のエンドポイントで通信が行えますhttps://sandbox-api-pay.line.meテスト的に使うのであれば、この環境を使いたいところですが、
SandBox環境だと決済の流れが、実際アプリではなくエミュレータのようなもので動き、
本物の決済の流れが体感的に分かりにくいのと、お試しでREST通信をしたい場合(Postman等を使用して疎通確認したい場合)
何らかの原因でエラーがでてしまい、通信が成功しないなどあるので、テスト的に使う場合はhttps://sandbox-api-pay.line.me
のエンドポイントは使わず、少額の決済であれば、実際のお金を幾らかLINE Payに入金しておき、テスト加盟店環境
のhttps://api-pay.line.me
をコールするようにした方がよいと考えます。
(PostmanがSandbox環境にて使えない原因をLINE Payの技術スタッフの方に問い合わせしたところ、通信時に特殊なパラメータが付与されている為のことだそうです。)環境まとめ
本番環境 テスト加盟店環境 Sandbox(本番・テスト加盟店環境) 利用制限 加盟店に申込後、審査完了後利用可能 なし、無料利用可能 なし、無料利用可能(テスト加盟店環境の場合) 利用料 加盟店契約により変動 無料 無料 (テスト加盟店環境の場合) 利用方法 加盟店申込をする Sandbox環境を作成する(ややこしいが、この環境がテスト加盟店環境という) 接続先 https://api-pay.line.me https://api-pay.line.me https://sandbox-api-pay.line.me 決済処理時の動き LINEアカウントの残高を利用 LINEアカウントの残高を利用 Sandboxの架空残高を利用(この利用に関しては実際に試していないのでどんな感じで決済されるかについてはよく分かっていない) 返金方法 MyPageから手動 MyPageから手動 or 毎日23:55と2:55に自動的に返金 架空残高を使うので特に無し LINE Pay APIにPOST通信する為の共通ヘッダーについて
共通ヘッダーについて
共通ヘッダーに設定する値
リクエストを送る時の、最低限の必要な共通ヘッダーが下記になります。
キー データの型 設定する値 Content-Type String 'application/json' X-LINE-ChannelId String LINE PayのMy Pageで確認したChannel ID X-LINE-Authorization-Nonce String 1回限りのランダムな値を設定する、UUIDを生成したり、現在日時のミリ秒等を生成して設定する。 X-LINE-Authorization String ※後述 X-LINE-Authorization-Nonceに設定する値としては、一度切りに使う値を生成し使ってください、(例えばJavaScriptでヘッダーを実装する場合
var nonce = (new Date()).getTime();
と現在のミリ秒を生成しnonceの値に設定する、といった実装です。)
X-LINE-Authorizationに関しては少々ややこしいので、次項に詳細を書きます。X-LINE-Authorization に設定する値について
X-LINE-Authorizationに設定する値はガイドによるとこう記述されています。
HTTP Method : GET Signature = Base64(HMAC-SHA256(Your ChannelSecret, (Your ChannelSecret + URL Path + Query String + nonce))) Query String : ?を除いたクエリ文字列(例 : Name1=Value1&Name2=Value2...) HTTP Method : POST Signature = Base64(HMAC-SHA256(Your ChannelSecret, (Your ChannelSecret + URL Path + RequestBody + nonce)))今回は「POST」の時のX-LINE-Authorizationについて説明します。
POST時にX-LINE-Authorizationを設定するために、以下の4つの値が必要です。
①Your ChannelSecret - LINE PayのMy Pageで確認したChannel SecretKey
②URL Path - POSTするURLのパス(例えばhttps://api-pay.line.me/v3/payments/request
等)
③RequestBody - POSTする時のRequest Body
④nonce - X-LINE-Authorization-Nonceに設定した値
そして上記の値を文字列連結し、ハッシュ メッセージ認証コード (HMAC) で計算します。
(HMACとは?)
この計算に使う、秘密鍵とメッセージ(データ)とハッシュ関数は以下になります
秘密鍵: LINE PayのMy Pageで確認したChannel SecretKey
メッセージ(データ):上記①〜④を文字列連結した値
ハッシュ関数:SHA256そして、ハッシュ化した値をBase64 でエンコードし、
エンコードした値をX-LINE-Authorizationに設定します。この処理をJAVAで書いた例はガイドに記載されてあります。
JavaScriptで書く場合は以下のようになるかと思います。var crypto = require('crypto-js');// crypto-jsという暗号化、復号化のライブラリを使う var channelSecret = '上記①の値'; var path = '上記②の値'; var body = '上記③の値'; var nonce = (new Date()).getTime(); // nonceに使うランダムな値の生成をする var message = channelSecret + path + body + nonce ; var hash = crypto.HmacSHA256(message,channelSecret); // channelSecretの値をキーにSHA256ハッシュ関数を使用してハッシュ (HMAC) を計算 var hashHeader = CryptoJS.enc.Base64.stringify(hash); // ここの値がX-LINE-Authorizationに設定する値になるまた、今回自分が実際実装した言語は
ruby
を使ったので
ruby
で実装する場合は下記のように①〜④の値を受けとって処理するようなメソッドを実装すればよいと思います。def authorization(channel_secret, params, url, nonce) message = "#{channel_secret}#{url}#{params.to_json}#{nonce}" hash = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), channel_secret, message) Base64.strict_encode64(hash) endヘッダーの実装例(ruby)
rubyでヘッダー部分を実装するとしたら、以下のようになります。
def headers(params, url) # ヘッダーを作る処理をする時にPOSTするURLのパスとPOSTする時のRequest Bodyを渡す nonce = authorization_nonce { 'Content-Type' => 'application/json', 'X-LINE-ChannelId' => channel_id, 'X-LINE-Authorization-Nonce' => nonce, 'X-LINE-Authorization' => authorization(params, url, nonce), } end def channel_id Rails.application.credentials.dig(# ここはcredentials.yml.encに設定した値を取り出す).to_s end def channel_secret Rails.application.credentials.dig(# ここはcredentials.yml.encに設定した値を取り出す).to_s end def authorization_nonce Time.zone.now.to_s(:db_jst) end def authorization(params, url, nonce) message = "#{channel_secret}#{url}#{params.to_json}#{nonce}" hash = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), channel_secret, message) Base64.strict_encode64(hash) end決済するためのAPIについて
「決済」を行うためのAPI
商品の決済をする為に必要最低限のAPIは以下の2つのみです。
・Request API (POST /v3/payments/request)
https://pay.line.me/documents/online_v3_ja.html#request-api・Confirm API (POST /v3/payments/{transactionId}/confirm)
https://pay.line.me/documents/online_v3_ja.html#confirm-apiその他決済取り消し等のAPIありますが、本記事では取り上げません。
また、上記のAPIを毎回コールし決済するパターンを「一般決済」と言い、
上記APIを初回のみコールし、以降上記APIをコールしなくても決済が出来るパターンを「自動決済」と言うのですが
今回は一般決済のみの事を記述します。決済の処理をアプリに組み込む際のざっくりな流れ(一例)
①Request API (POST /v3/payments/request)をコールする
↓
②成功時のレスポンスにLINE Pay取引番号(transactionId)と
決済用のURL(info.paymentUrl.web 又は info.paymentUrl.web.app) が返ってくる
↓
③アプリまたはWEB上で ②のレスポンス内にある決済用のURL(info.paymentUrl.web 又は info.paymentUrl.app)
に遷移する処理をする。
↓
④(LINEが端末にインストールされている状態でinfo.paymentUrl.appのURLに遷移した場合)
LINEアプリが立ち上がり LINE Payの支払画面に遷移するので、LINEアプリ内で決済の操作を行う
↓
⑤決済が完了すると ①のRequest APIをコール時に設定した画面に遷移する(設定の仕方は後述)
↓
⑥ ⑤にて遷移した瞬間にConfirm API (POST /v3/payments/{transactionId}/confirm)をコールする処理をする、
{transactionId} には②のレスポンスに含まれていたLINE Pay取引番号(transactionId)を設定する。
設定例: /v3/payments/2019010100000000000/confirm
↓
⑦決済が完了する。Request APIについて
Request API のざっくりとした説明
「こんな商品を買うから、購入するために整理券ちょうだい」的なAPIです。
「商品の値段は○○で、個数が○○で〜」といった情報をRequest Bodyに詰め込んで送ると
「整理券あげるよ、整理番号(LINE Pay取引番号)はこれだよ、この場所(決済用のURL)で買ってね」
というようなレスポンスが返ってきます。
返ってきた、整理番号(LINE Pay取引番号)は次にコールするConfirm APIで使う事になるので
どこかに保持しておく必要があります。Request API のRequest Bodyの例とレスポンス
実際通信する時のRequest Bodyの例は以下のようになっています。
必要最低限のことだけ記述してあるので、「商品の送料、配送先」等、詳細にせってしたい場合は公式ドキュメントを参照してください。
Request Bodyの例-コピペ用{ "amount": 300, "currency": "JPY", "orderId": "20190101", "packages": [ { "id": "1", "amount": 300, "name": "LINE Pay商品", "products": [ { "name": "豚の角煮(赤みそ)", "imageUrl": "https://exmple.jpg", "quantity": 1, "price": 100 }, { "name": "れんこんサラダ", "imageUrl": "https://exmple.jpg", "quantity": 2, "price": 100 } ] } ], "redirectUrls": { "confirmUrl": "myapp://confirm", "cancelUrl": "myapp://cancel" } }Request Bodyの例-各パラメータの簡単な説明
{ "amount": 300, // 合計金額を設定、この金額はpackages[].amountの合計金額と一致していないとエラーになる。 "currency": "JPY", // 決済通貨の種類 日本であれば"JPY"で問題ないかと "orderId": "20190101", // 実装者側で発行するユニークなID、特に決まっていなければ、ランダムなUUIDを生成したり、現在日付のTIME_STAMP等設定すれば良い "packages": [ { "id": "1", // 特に決まっていなくても設定する必要あり "amount": 300, // products[].priceの合計金額、一致してないとエラーになる "name": "LINE Pay商品", // 購入する商品の全体的な名前、特に決まっていなければ、商品と同じ名前でよいと思う。 "products": [ { "name": "豚の角煮(赤みそ)", // 商品の名前 ここの値がLINE Payの決済画面で表示される。 "imageUrl": "https://linepay.exmple.image.jpg", // 商品の画像のサムネイル LINE Payの決済画面で表示される。 "quantity": 1, // 商品の個数 "price": 100 // 商品の値段 }, { "name": "れんこんサラダ", "imageUrl": "https://linepay.exmple.image.jpg", "quantity": 2, "price": 100 } ] } ], "redirectUrls": { "confirmUrl": "myapp://confirm", // LINE Payで商品決済したときに遷移するURL、遷移させたいのがWEBページならhttp://~ ネイティブアプリならアプリのDeepLinkを設定する "cancelUrl": "myapp://cancel" // LINE Payで商品決済をキャンセルしたときに遷移するURL } }※"imageUrl" に設定している値は一例なので、実際にサムネイルとして何か画像を表示してみたい場合は、実在するURLを入れてください、若しくは設定しないでください。
上記のリクエストを飛ばし、成功すると以下のようなレスポンスが返ってきます(~の部分は可変します)
{ "returnCode": "0000", "returnMessage": "Success.", "info": { "paymentUrl": { "web": "https://web-pay.line.me/web/payment/wait?~~~~~", "app": "line://pay/payment/~~~~~" }, "transactionId": ~~~~~~~~, "paymentAccessToken": "~~~~~~~~" } }端末にLINEがインストールされており、LINE Payが使える設定にしている場合に、上記のレスポンスの「info.paymentUrl.app」に遷移すると以下のような画面に遷移します。
※上記の画像にサムネイルが表示されていますがRequest BodyのimageUrlで設定した「linepay.exmple.image.jpg」とは異なる画像を使っています。「linepay.exmple.image.jpg」というURLはあくまで一例であって実際には存在しませんのでご了承ください。
※上記の画像で決済やキャンセルを行うとRequest Bodyで設定したconfirmUrlやcancelUrlに遷移します。Confirm APIのRequest Bodyの例とレスポンス
Confirm API のざっくりとした説明
「さっき教えてもらった場所で決済したよ、承認してね。」的なAPIです。
LINE Payのアプリで決済処理をせずにコールするとエラーになるので注意が必要です。Request Bodyの例-コピペ用
{ "amount": 300, "currency": "JPY" }Request Bodyの例-各パラメータの簡単な説明
{ "amount": 300, //商品の合計金額 Request APIの時と違う金額だとエラーになるので注意 "currency": "JPY" // 決済通貨の種類 }上記のリクエストを飛ばし、成功すると以下のようなレスポンスが返ってきます(~の部分は可変します)
{ "returnCode": "0000", "returnMessage": "Success.", "info": { "transactionId": ~~~~~~~~, "orderId": "20190101", "payInfo": [ { "method": "BALANCE", "amount": 300 } ], "packages": [ { "id": "1", "amount": 300, "name": "LINE Pay商品", "products": [ { "name": "豚の角煮(赤みそ)", "imageUrl": "https://exmple.jpg", "quantity": 1, "price": 100 }, { "name": "れんこんサラダ", "imageUrl": "https://exmple.jpg", "quantity": 2, "price": 100 } ] } ], } }上記のようなレスポンスが返ってきた場合、実際にLINE Payの残高が減り、LINE PayのMy Pageにて取引の履歴が残っていることを確認できるかと思います。
LINE PayのMy Page
[取引管理]>[取引内訳]
以上で、LINE Pay APIにて一般決済する時の処理の流れになります。
(おまけ)Postmanで疎通確認を行う
まだ実際にはアプリには組み込まないけど、LINE Pay APIを叩いたらどんなレスポンスが返ってくるか見たい場合は
「Postman」を使うのが便利です、ここではPostmanを使ってLINE PayのAPIをコールするときの設定を明記します。(Postmanの導入方法についてはここでは説明しませんので、各自で調べていただくよう、宜しくお願いします。)各種設定
環境ファイルの設定(Channel ID とChannel SecretKeyの設定)
①赤枠部分の歯車のアイコンをクリックします。
↓
②赤枠部分の[Add]をクリックします。
↓
③以下のように設定し[Add]をクリックします。
VARIABLE INITIAL VALUE CURRENT VALUE channelId LINEのMyPageで確認したChannel ID ←INITIAL VALUEで設定した値と同じ channelSecret LINEのMyPageで確認したChannel SecretKey ←INITIAL VALUEで設定した値と同じ
↓
④赤枠部分のように環境ファイルを先程追加したものに設定しておきます。
Headersの設定
以下のように設定します。
KEY VALUE Content-Type application/json X-LINE-ChannelId {{channelId}} X-LINE-Authorization-Nonce {{nonce}} X-LINE-Authorization {{authorization}} Content-Typeは文字列で'application/json'と設定しておきます。
その他の設定はPostmanの環境ファイルから取得するようにします。
{{channelId}}
等、中括弧で囲むと、先程設定した設定ファイルのkey参照し、その値を参照するようになります。
Bodyの設定
rawを選択し、以下のように設定します。
{ "amount": 300, "currency": "JPY", "orderId": "20190101", "packages": [ { "id": "1", "amount": 300, "name": "LINE Pay商品", "products": [ { "name": "豚の角煮(赤みそ)", "imageUrl": "", "quantity": 1, "price": 100 }, { "name": "れんこんサラダ", "imageUrl": "", "quantity": 2, "price": 100 } ] } ], "redirectUrls": { "confirmUrl": "myapp://confirm", "cancelUrl": "myapp://cancel" } }Pre-request Scriptの設定
ヘッダーの「X-LINE-Authorization」に設定するための処理を記述します。
var crypto = require('crypto-js'); var time = (new Date()).getTime(); pm.environment.set("nonce", time); var path = pm.request.url.getPath(); var body = pm.request.body.toString(); var message = pm.environment.get("channelSecret") + path + body + pm.environment.get("nonce") ; var hash = crypto.HmacSHA256(message,pm.environment.get("channelSecret")); var hashHeader = CryptoJS.enc.Base64.stringify(hash); pm.environment.set("authorization",hashHeader);
pm.〜
で始まる記述はPostman独自の処理で、リクエストBodyを取得したり、環境ファイルの設定を読み込んだり書き込んだりするための記述です。レスポンス
Sendした結果、以下のようなレスポンスが返ってきたら成功です。
以下の結果はRequest API (POST /v3/payments/request)をコールした際のレスポンスです。
returnCodeが'0000'以外の場合はLINE APIへの疎通は成功しているが、処理が成功していません
各APIのReturn Codesの欄を参考にどこがミスをしているか探してみてください。
- 投稿日:2020-01-15T13:44:37+09:00
あっさり読むrails③(クラス)
はじめに
前回の記事にて、
もちろんこのままだと味気ないので、実際にはclassを設定したり、他のメソッドを使用することになります
と書きました。
本記事では、そのclassを設定する
の部分を実行しようと思います。実行
前回使用したコードを改良しようと思います。今の所、コードは下記の通りです。
- @products.each do |product| = product.name = product.price = image_tag(product.image)まずは、
name
,price
,image
をひとまとめにするクラスを作ります。index.html.haml- @products.each do |product| .product = product.name = product.price = image_tag(product.image)これで、テーブルに登録してあるだけの、
name,price,image
の3要素を持ったproduct
classのオブジェクトが作られます。
この状態では、name
とprice
でフォントを変えたり、image
の大きさを変更したり、といったことが難しいので、要素毎にclassを設定します。index.html.haml- @products.each do |product| .product = product.name, class: "product__name" = product.price, class: "product__price" = image_tag(product.image, class: "product__image")
- 投稿日:2020-01-15T12:31:25+09:00
Rails6 のちょい足しな新機能を試す 116(MySQL データベース存在チェック編)
はじめに
Rails 6 に追加された新機能を試す第116段。 今回は、
MySQL データベース存在チェック
編です。
Rails 6 では、MySQLのデータベースを存在するかどうかをチェックする方法が少し変わりました。
データベースが存在しないときに、bin/rails db:migrate
を実行した場合、 MySQLのエラーメッセージが英語以外でも、ActiveRecord::NoDatabaseError
が発生するようになりました。Ruby 2.6.5, Rails 6.0.2.1, Rails 5.2.4.1 MySQL 8.0.16 で確認しました。 (Rails 6.0.0 でこの修正が入っています。)
$ rails --version Rails 6.0.2.1今回は、MySQL のエラーメッセージを ja_JP にして起動して、
bin/rails db:migrate
コマンドを使って Rails 6.0.2.1 と Rails 5.2.4.1 の違いを確認してみます。今回、MySQL のエラーメッセージを日本語に切り変えるために mysqld コマンドに
--lc_messages_dir=/usr/share/mysql-8.0 --lc_messages=ja_JP
オプションを追加しています。MySQL 側で直接、データベースが存在しないことを確認する
mysql コマンドでデータベースが存在しないことを確認します。
ここで、メッセージの先頭が 1049 となっていることに注意してください。$ mysql -u root app_development ERROR 1049 (42000): 'app_development' は不明なデータベースです。db:migrate を実行する
エラーを確認するために、
db:create
しないでdb:migrate
を実行するとActiveRecord::NoDatabaseError
が発生します。$ bin/rails db:migrate rails aborted! ActiveRecord::NoDatabaseError: 'app_development' は不明なデータベースです。 /usr/local/bundle/gems/activerecord-6.0.2.1/lib/active_record/connection_adapters/mysql2_adapter.rb:28:in `rescue in mysql2_connection'ちなみに MySQL のエラーメッセージが英語の場合も
ActiveRecord::NoDatabaseError
となります。$ bin/rails db:migrate rails aborted! ActiveRecord::NoDatabaseError: Unknown database 'app_development' /usr/local/bundle/gems/activerecord-6.0.2.1/lib/active_record/connection_adapters/mysql2_adapter.rb:28:in `rescue in mysql2_connection'Rails 5 では
MySQL のエラーメッセージが日本語の場合は、
Mysql2::Error
となります。 英語の場合は、ActiveRecord::NoDatabaseError
になります。日本語の場合:
$ bin/rails db:migrate rails aborted! Mysql2::Error: 'app_development' は不明なデータベースです。 /usr/local/bundle/gems/mysql2-0.5.2/lib/mysql2/client.rb:90:in `connect'英語の場合
$ bin/rails db:migrate rails aborted! ActiveRecord::NoDatabaseError: Unknown database 'app_development' /usr/local/bundle/gems/activerecord-5.2.4.1/lib/active_record/connection_adapters/mysql2_adapter.rb:26:in `rescue in mysql2_connection'何が変わったのか
Rails 5 までは、 エラーメッセージに
Unknown database
が含まれた場合にActiveRecord::NoDatabaseError
を発生させていました。
Rails 6 では、 エラーメッセージではなく、エラーの番号が 1049 (最初に mysqlコマンドでデータベースが存在しないことを確認したときの値)であるときに、ActiveRecord::NoDatabaseError
を発生させるようにしています。試したソース
https://github.com/suketa/rails_sandbox/tree/try116_mysql_locale
参考情報
- 投稿日:2020-01-15T12:02:17+09:00
Ruby 行と列を入れ替えて配列の配列を作成する方法
paizaのスキルチェックで使った内容を投稿しました
array = [[1 ,2, 3], [4,5,6], [7,8,9]] p array.transpose >> [[1,4,7],[2,5,8],[3,6,9]]
- 投稿日:2020-01-15T12:01:02+09:00
変数展開
- 投稿日:2020-01-15T11:57:16+09:00
配列から複数の要素を取り出す方法
paizaのスキルチェックに使った内容を投稿しました。
array.each_cons(引数) {|arr| block }引数で指定した数の要素を繰り返し取り出して、ブロックを実行
ブロック引数には取り出した要素が配列で入る
- 投稿日:2020-01-15T10:27:00+09:00
【Ruby on Rails】タイムゾーンの設定を日本時間に変更する方法
- 投稿日:2020-01-15T07:57:01+09:00
Rails MySQL データ型をdecimalにしているのに小数点を扱ってくれなくて困った話
目的
- DBに格納されている数値に少数の値を足そうとして詰まりまくったところをまとめる
困った箇所
- viewファイルで表している入力formの少数の値が入力できない。
- こちらで解決した→HTML 少数入力時に「有効な値を入力してください」と出たときの話
- 入力してDBのカラムに保存しようとしてもカラムに反映されない。
save
でのエラーは出ない。rails console
で当該カラムに少数を入力しsave
するとDBのカラムに反映されるが、formから少数を入力しても足し算されない、整数の足し算はできる。「viewファイルで表している入力formの少数の値が入力できない」の解決方法
- 下記の方法で解決した。
「入力してDBのカラムに保存しようとしてもカラムに反映されない。
save
でのエラーは出ない。」の原因と解決方法調査と原因
- 数値を保存するカラム
stady_time
のデータ型は少数を扱うためにdecimal
にしている。- 下記にカラム
stady_time
を作成したときのマイグレーションファイルを下記に記載する。class AddStudyTimeHashTagToPosts < ActiveRecord::Migration[6.0] def change add_column :posts, :study_time, "decimal" end end
- 原因はカラム作成時にデータ型"decimal"を設定するときにオプションで格納する数値の全桁数と少数部分の桁数のを指定する必要があったようである。
解決方法
- カラム
study_time
を削除して再度オプション付きで作成し直すか、現在存在するstudy_time
カラムのデー型を設定し直すかの二通りがある。- 今回は個人プロダクトであるが、デプロイして使用されているサービスで重要なカラムを一旦削除することはありえないため、データ型を設定し直す方法をとる。
下記の修正を加える
修正前 修正後 修正可否 カラム名 study_time study_time 修正しない データ型 decimal decimal 修正しない データ型のオプション 指定なし 全桁数12桁 少数点以下桁数2桁 修正する 下記にカラムのデータ型設定を修正する手順を記載する。
下記コマンドを実行してマイグレーションファイル(DBの仕様変更を命令するファイル)を作成する。
$ rails g migration change_study_time
db/migrate/
に存在するマイグレーションファイル20200112011439_change_study_time.rb
に下記の記載を行う(#
の行はコメントなので実際はなくても良い)class AddPasswordToUsers < ActiveRecord::Migration[6.0] def change #カラムの変更なのでchenge_column :テーブル名, :カラム名, :データ型, :オプション(数値の全桁数), :オプション(数値の小数点以下の桁数、第二位まで欲しいので2とした) change_column :posts, :study_time, :decimal, precision: 12, scale: 2 end end下記コマンドを実行してマイグレート(DBにマイグレーションファイル通りの仕様変更を行う命令)を行う。
$ rails db:migrateもしマイグレーションファイルの記載を間違えてマイグレートしてしまったら下記の方法でマイグレート前の状態に戻す。
下記方法でカラムの方が正常に設定されているか確認する。
- Rails6 rails consoleからカラムのデータ型を確認する
今回の場合Postモデルのstudy_timeカラムなので下記のコマンドになる。
- rails consoleを起動し下記コマンドを実行する。
>Post.columns_hash['study_time'].type => :decimal「
rails console
で当該カラムに少数を入力しsave
するとDBのカラムに反映されるが、formから少数を入力しても足し算されない、整数の足し算はできる。」の原因と解決方法調査と原因
- rails consoleでの確認と切り分けにより足し算を行なっているコントローラファイルに問題があると感じた。
- 少数は表示できるのに足されないということはformから送られてきた値を数値に変換している箇所に間違いがあると予想した。
下記にpostコントローラの当該処理のメソッドを抜粋して記載する。
def update @post = Post.find_by(id: params[:id]) @post.study_time += params[:study_time].to_i @post.save redirect_to("/posts/#{@post.id}") end
params[:study_time]
にはフォームで入力した少数値が入っている。.to_i
で入力した文字列を数値に変更している。整数を表現する場合は
.to_i
でも良いが、少数を扱い際は別の表現にしないといけない。解決方法
- コントローラでフォームで入力した値を数値をして扱う処理で整数に変換してしまっているので少数も含む数値に変換してあげる記載をする。
.to_i
を.to_f
とする。下記に正しいコードを記載する。
def update @post = Post.find_by(id: params[:id]) @post.study_time += params[:study_time].to_f @post.save redirect_to("/posts/#{@post.id}") end
- 投稿日:2020-01-15T02:57:29+09:00
画像の複数枚投稿と編集とプレビューと私
以前の記事にて
初投稿につき緊張ながら投稿したのをよく覚えています。
たくさん見てくださってありがとうございました。
画像の複数投稿??プレビュー表示??え??しかし......
「投稿のみやん。editは?はよ。」と多数ご指摘をいただきました。
しかし.......
何の成果もあげられませんでした。
編集することができませんでした。
藁にもすがる思いでメンターさんにコードを見てもらい、アドバイスを仰ぎましたが、
「僕こんな実装したことないし、こんなやり方見たことない。
メンテナンス性も悪いし可読性もよろしくない。
他に効率のいい方法あるから.....やり直そうか」と、ありがたくも残酷なご指摘をいただきました。
参考にしてくださった方々ごめんなさい。
メンテナンス性が悪く可読性もよくないお粗末なコードを書いてしまいました......なので今回改良版をやります。意地で。
仕様
- 画像は5枚投稿できる
- 投稿した画像は1枚ずつプレビューされる
- 5枚目を投稿すると投稿欄が消える
- ドラッグ&ドロップは非実装
- 削除を押すとプレビューが消える
前回の反省
前回は新規投稿に注力しすぎて編集する時のことを考えられていなかったことが敗因でした。
新規投稿が完成した時点でやりきった感がありました。
スプリントレビューで「編集は?」と言われて「Oh.......」ってなったものの対応できず、
「1枚画像が投稿できる状態にする」というレビューOKの最低ラインに合わせるべくJSファイルを消去しました。悲しい。「後のことを考えて実装する」というもっとも重要な設計思想が抜けていました。
大いに反省するきっかけとなったのでよしとしましょう。しかし僕は諦めが悪いので、
今回は編集機能の実装を前提として考えていきたいと思います。
ではLET'S GO.編集するには....
まず、編集ページへアクセスすると、
登録済みの写真についてはプレビューが表示されている状態にしなければいけません。
前回は、画像登録の際にdataTransferというデータの箱を使って、1つのfile_fieldにデータを追加していく形で複数投稿を実装していました。
しかし、いざeditを実装するとなった際に、dataTransferにデータを追加することができず断念する結果となりました......
1つのfile_fieldを酷使する実装にはやはり無理があったようです。無理させてごめんねfile_field。そこで今回は別々のidを持ったfile_fieldを5つ作成し、そのそれぞれにデータを入れていく形で実装することにしました。
これなら画像の編集も削除もできそうです。
では画像投稿機能から実装していきましょう。プレビュー表示と削除
前回も利用したsample_appを使用します。
items/new.haml.main %section.main__block = form_with model:@item, local:true do |f| %h2.sell__block__head 商品の情報を入力 .sell__block__form .sell__block__form__upload %h3.sell__block__form__upload__head 出品画像 %span.require 必須 %p 最大5枚までアップロードできます .post__drop__box__container .prev-content .label-content %label{for: "item_images_attributes_0_image", class: "abel-box", id: "label-box--0"} %pre.label-box__text-visible クリックしてファイルをアップロード .hidden-content = f.fields_for :images do |i| = i.file_field :image, class: "hidden-field" %input{class:"hidden-field", type: "file", name: "item[images_attributes][1][image]", id: "item_images_attributes_1_image" } %input{class:"hidden-field", type: "file", name: "item[images_attributes][2][image]", id: "item_images_attributes_2_image" } %input{class:"hidden-field", type: "file", name: "item[images_attributes][3][image]", id: "item_images_attributes_3_image" } %input{class:"hidden-field", type: "file", name: "item[images_attributes][4][image]", id: "item_images_attributes_4_image" } .sell__block__form__name .form-group__name %label 商品名 %span.require 必須 %div = f.text_field :name, placeholder:"商品名(必須 40文字まで)",class: "form__group__name" .sell__block__form__btn %div = f.submit "出品する",class: "btn-default__btn-red"items.scss//投稿欄以外のCSSは省略します //image投稿欄のCSS .post__drop__box__container{ display: block; margin: 16px auto 0; display: flex; flex-wrap: wrap; //プレビュー表示欄のCSS .prev-content { display: flex; .preview-box { height: 162px; width: 112px; margin: 0 15px 10px 0; .upper-box { height: 112px; width: 100%; img{ width: 112px; height: 112px; } } .lower-box { display: flex; text-align: center; .update-box { color: #00b0ff; width: 50%; height: 50px; line-height: 50px; border: 1px solid #eee; background: #f5f5f5; cursor: pointer; } .delete-box { color: #00b0ff; width: 50%; height: 50px; line-height: 50px; border: 1px solid #eee; background: #f5f5f5; cursor: pointer; } } } } //投稿クリックエリアのCSS .label-content{ margin-bottom: 10px; width: 620px; .label-box { display: block; border: 1px dashed #ccc; position: relative; background: #f5f5f5; width: 100%; height: 162px; cursor: pointer; &__text-visible { position: absolute; top: 50%; left: 16px; right: 16px; text-align: center; font-size: 14px; line-height: 1.5; font-weight: bold; -webkit-transform: translate(0, -50%); transform: translate(0, -50%); pointer-events: none; white-space: pre-wrap; word-wrap: break-word; } } } //file_fieldのcss .hidden-content { .hidden-field { display: none; } .hidden-checkbox { display: none; } } }今回は画像を5枚投稿するので、file_fieldを5つ作りました。
全てdisplay: none;で隠しています。
こんな感じ。
5つのfile_fieldには別のidが振ってあります。
したがって、file_fieldに画像が入るたびにlabel側のforを変更していけば1枚ずつ複数の画像を投稿することができますね。では、JSで操作してきましょう。
item_new.js$(document).on('turbolinks:load', function(){ $(function(){ //プレビューのhtmlを定義 function buildHTML(count) { var html = `<div class="preview-box" id="preview-box__${count}"> <div class="upper-box"> <img src="" alt="preview"> </div> <div class="lower-box"> <div class="update-box"> <label class="edit_btn">編集</label> </div> <div class="delete-box" id="delete_btn_${count}"> <span>削除</span> </div> </div> </div>` return html; } // ラベルのwidth操作 function setLabel() { //プレビューボックスのwidthを取得し、maxから引くことでラベルのwidthを決定 var prevContent = $('.label-content').prev(); labelWidth = (620 - $(prevContent).css('width').replace(/[^0-9]/g, '')); $('.label-content').css('width', labelWidth); } // プレビューの追加 $(document).on('change', '.hidden-field', function() { setLabel(); //hidden-fieldのidの数値のみ取得 var id = $(this).attr('id').replace(/[^0-9]/g, ''); //labelボックスのidとforを更新 $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`}); //選択したfileのオブジェクトを取得 var file = this.files[0]; var reader = new FileReader(); //readAsDataURLで指定したFileオブジェクトを読み込む reader.readAsDataURL(file); //読み込み時に発火するイベント reader.onload = function() { var image = this.result; //プレビューが元々なかった場合はhtmlを追加 if ($(`#preview-box__${id}`).length == 0) { var count = $('.preview-box').length; var html = buildHTML(id); //ラベルの直前のプレビュー群にプレビューを追加 var prevContent = $('.label-content').prev(); $(prevContent).append(html); } //イメージを追加 $(`#preview-box__${id} img`).attr('src', `${image}`); var count = $('.preview-box').length; //プレビューが5個あったらラベルを隠す if (count == 5) { $('.label-content').hide(); } //ラベルのwidth操作 setLabel(); //ラベルのidとforの値を変更 if(count < 5){ //プレビューの数でラベルのオプションを更新する $('.label-box').attr({id: `label-box--${count}`,for: `item_images_attributes_${count}_image`}); } } }); // 画像の削除 $(document).on('click', '.delete-box', function() { var count = $('.preview-box').length; setLabel(count); //item_images_attributes_${id}_image から${id}に入った数字のみを抽出 var id = $(this).attr('id').replace(/[^0-9]/g, ''); //取得したidに該当するプレビューを削除 $(`#preview-box__${id}`).remove(); console.log("new") //フォームの中身を削除 $(`#item_images_attributes_${id}_image`).val(""); //削除時のラベル操作 var count = $('.preview-box').length; //5個めが消されたらラベルを表示 if (count == 4) { $('.label-content').show(); } setLabel(count); if(id < 5){ //削除された際に、空っぽになったfile_fieldをもう一度入力可能にする $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`}); } }); }); })プレビュー表示の流れとしては、
(1) id = 0 〜 5のfile_fieldと、for = 0のlabelがある
(2) id = 0のfile_fieldに画像を放り込む
(3) labelのforがJS側で for = 1に変更される
(4) label部をクリックするとid = 1のfile_fieldに画像が入るようになる
(5) 繰り返し 5枚投稿するとlabelが消える
って感じです。
GIFで見るとわかりやすいかも。
......7秒は短い。
しかし、1枚目を投稿したあとでfile_fieldの判定が2つ目に移動しているのがわかりますね。また、プレビュー削除の流れとしては
(1) プレビューが2つある。それぞれが格納されているfile_fieldのidは0, 1。
(2) id = 0の方の削除ボタンを押す
(3) プレビューが消え、id = 0のfile_fieldの中身が消える
(4) labelのforオプションを0として更新
(5) id = 0のfile_fieldが再度入力可能になる
って感じで実装しています。
GIFで見ると......
削除したfile_fieldが再度入力可能になっているのがわかりますね。
プレビュー表示側ではプレビュー数をもとにlabelのforオプションを更新するよう設定しているので、この後ファイルを追加する際にはちゃんと次の空のfile_fieldに判定が移動する実装になっています。controller側はというと......
items_controller.rbclass ItemsController < ApplicationController def new @item = Item.new @images = @item.images.build end def create @item = Item.new(item_params) @item.save end private def item_params params.require(:item).permit( :name, [images_attributes: [:image]]) end endこんな感じです。サンプルなのでめちゃくちゃシンプルに書いてます。お許しを。
また、DBのカラム名ですが
imagesテーブルに関しては「image」カラムにファイル名が保存される形で実装しています。これで画像の複数投稿ができるようになりました。良かったね。
編集機能
さて今回の本題です。
編集ページへのアクセス時にどうやってプレビューを出すか......ですが、
わかりやすくeach文で表示することにしました。edit.haml.main %section.main__block = form_with model:@item, local:true do |f| %h2.sell__block__head 商品の情報を入力 .sell__block__form .sell__block__form__upload %h3.sell__block__form__upload__head 出品画像 %span.require 必須 %p 最大5枚までアップロードできます .post__drop__box__container .prev-content //JSで挿入したhtmlと同じ形 each文でのプレビュー表示 - @item.images.each do |image| .preview-box .upper-box = image_tag image.image.url, width: "112", height: "112", alt: "preview" .lower-box .update-box %label.edit-btn 編集 .delete-box .delete-btn %span 削除 .label-content //プレビューの数に合わせてforオプションを指定 = f.label :"images_attributes_#{@item.images.length}_image", class: "label-box", id: "label-box--#{@item.images.length}" do %pre.label-box__text-visible クリックしてファイルをアップロード .hidden-content = f.fields_for :images do |i| //プレビューが出ている分のfile_fieldとdelete用のチェックボックスを設置 = i.file_field :image, class: "hidden-field" = i.check_box:_destroy, class: "hidden-checkbox" //5つのfile_fieldを準備するに当たって、足りない分を表示 - @item.images.length.upto(4) do |i| %input{name: "item[images_attributes][#{i}][image]", id: "item_images_attributes_#{i}_image", class:"hidden-field", type:"file"} .sell__block__form__name .form-group__name %label 商品名 %span.require 必須 %div = f.text_field :name, placeholder:"商品名(必須 40文字まで)",class: "form__group__name" .sell__block__form__btn %div = f.submit "出品する",class: "btn-default__btn-red"file_field関連のdisplay: none;を外すとこんな感じになります。
ちょっとわかりにくいですが、現在投稿済みのプレビューに対応するfile_fieldの横に削除用のチェックボックスが追加されています。編集の流れとしては、
(1) id = 0, 1のfile_fieldが投稿済みのものと対応しているものとする
(2) id = 0の削除ボタンを押す
(3) プレビューが削除されるとともに、id = 0のfile_fieldに対応する削除用チェックボックスにチェックが入る
(4) 投稿時と同様、labelをクリックすると削除済みのid = 0のfile_fieldがアクティブになる
(5) 再度id = 0に新しい画像が入ったら、削除用チェックボックスのチェックを外すこんな感じで実装していきたいと思います。
(4)を見るに、「item_new.js」に追記、場合分けする形でeditも一緒に実装できそうですね。完成コードはこちら。
item_new.js$(document).on('turbolinks:load', function(){ $(function(){ //プレビューのhtmlを定義 function buildHTML(count) { var html = `<div class="preview-box" id="preview-box__${count}"> <div class="upper-box"> <img src="" alt="preview"> </div> <div class="lower-box"> <div class="update-box"> <label class="edit_btn">編集</label> </div> <div class="delete-box" id="delete_btn_${count}"> <span>削除</span> </div> </div> </div>` return html; } // 投稿編集時 //items/:i/editページへリンクした際のアクション======================================= if (window.location.href.match(/\/items\/\d+\/edit/)){ //登録済み画像のプレビュー表示欄の要素を取得する var prevContent = $('.label-content').prev(); labelWidth = (620 - $(prevContent).css('width').replace(/[^0-9]/g, '')); $('.label-content').css('width', labelWidth); //プレビューにidを追加 $('.preview-box').each(function(index, box){ $(box).attr('id', `preview-box__${index}`); }) //削除ボタンにidを追加 $('.delete-box').each(function(index, box){ $(box).attr('id', `delete_btn_${index}`); }) var count = $('.preview-box').length; //プレビューが5あるときは、投稿ボックスを消しておく if (count == 5) { $('.label-content').hide(); } } //============================================================================= // ラベルのwidth操作 function setLabel() { //プレビューボックスのwidthを取得し、maxから引くことでラベルのwidthを決定 var prevContent = $('.label-content').prev(); labelWidth = (620 - $(prevContent).css('width').replace(/[^0-9]/g, '')); $('.label-content').css('width', labelWidth); } // プレビューの追加 $(document).on('change', '.hidden-field', function() { setLabel(); //hidden-fieldのidの数値のみ取得 var id = $(this).attr('id').replace(/[^0-9]/g, ''); //labelボックスのidとforを更新 $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`}); //選択したfileのオブジェクトを取得 var file = this.files[0]; var reader = new FileReader(); //readAsDataURLで指定したFileオブジェクトを読み込む reader.readAsDataURL(file); //読み込み時に発火するイベント reader.onload = function() { var image = this.result; //プレビューが元々なかった場合はhtmlを追加 if ($(`#preview-box__${id}`).length == 0) { var count = $('.preview-box').length; var html = buildHTML(id); //ラベルの直前のプレビュー群にプレビューを追加 var prevContent = $('.label-content').prev(); $(prevContent).append(html); } //イメージを追加 $(`#preview-box__${id} img`).attr('src', `${image}`); var count = $('.preview-box').length; //プレビューが5個あったらラベルを隠す if (count == 5) { $('.label-content').hide(); } //プレビュー削除したフィールドにdestroy用のチェックボックスがあった場合、チェックを外す============= if ($(`#item_images_attributes_${id}__destroy`)){ $(`#item_images_attributes_${id}__destroy`).prop('checked',false); } //============================================================================= //ラベルのwidth操作 setLabel(); //ラベルのidとforの値を変更 if(count < 5){ $('.label-box').attr({id: `label-box--${count}`,for: `item_images_attributes_${count}_image`}); } } }); // 画像の削除 $(document).on('click', '.delete-box', function() { var count = $('.preview-box').length; setLabel(count); var id = $(this).attr('id').replace(/[^0-9]/g, ''); $(`#preview-box__${id}`).remove(); //新規登録時と編集時の場合分け========================================================== //新規投稿時 //削除用チェックボックスの有無で判定 if ($(`#item_images_attributes_${id}__destroy`).length == 0) { //フォームの中身を削除 $(`#item_images_attributes_${id}_image`).val(""); var count = $('.preview-box').length; //5個めが消されたらラベルを表示 if (count == 4) { $('.label-content').show(); } setLabel(count); if(id < 5){ $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`}); } } else { //投稿編集時 $(`#item_images_attributes_${id}__destroy`).prop('checked',true); //5個めが消されたらラベルを表示 if (count == 4) { $('.label-content').show(); } //ラベルのwidth操作 setLabel(); //ラベルのidとforの値を変更 //削除したプレビューのidによって、ラベルのidを変更する if(id < 5){ $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`}); } } //============================================================================= }); }); });items_controller.rbclass ItemsController < ApplicationController #省略 def edit @item = Item.find(params[:id]) end def update @item = Item.find(params[:id]) @item.update(item_update_params) end private def item_params params.require(:item).permit( :name, [images_attributes: [:image]]) end def item_update_params params.require(:item).permit( :name, [images_attributes: [:image, :_destroy, :id]]) end endJSに関しては、コメントアウトの「============================」で囲んであるところが追記箇所です。
わかりにくかったらすみません。動作はこのようになります。
削除を押すとチェックボックスにチェックが入ります。
このまま『編集する』ボタンを押すと、画像が削除されます。
画像を選択した場合、チェックボックスからチェックが外れます。
この状態で『編集する』ボタンを押すと、画像が差替わります。
編集後のリンク先を指定していないので微妙な感じですが、
each文で引っ張り出される画像が更新されているのがわかりますね。ね??一旦これで完成です。
はぁ、疲れた。
最後に
とりあえず形はなんとかできました。
まだエラーは残っていると思うので、完璧なものは保証できません。
自分で試した中ではエラー起こらなかったですが、
もし見落としがあったら教えていただけると嬉しいです。僕ができるのはここまでです。参考にして頂けましたら幸いです。
あとドラッグ&ドロップやら10枚投稿への対応やら2段目のドロップボックス出現やら削除やら色々あると思いますが、
皆さんの手でよしなに実装してください。また気が向いたら何か書きます。
おわり。
- 投稿日:2020-01-15T02:27:57+09:00
ruby で公開鍵の文字列から public key オブジェクトを作成する方法
概要
公開鍵の文字列から public key オブジェクトを生成する方法について少しはまったので、共有します。
コード
下記のような公開鍵があるときは
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0bXcnrheJ2snfq1wv6Qz 8+TEPDGKHCM0SsrQjxEFpXSEycL2/A+oW1ZGUzCuhz4HH4wkvc4CDJl25johSIUT Vyo4mrFrJ0ab0QAhrWE7gMyWFIfraj9cksPAGyVAiXLCN9Ly2xuoJxFjCAZXw1VO 8i7RTYK8ZP6dhcosiyzdhYt7C/65B5ikmCS4AymXIa83QQanCtjoGiwy4Cf2pLnn 9zXMZEnqQ+wwSoGn32YExmap7GAtjOwHNWU5zpW3dwNMq+zkcln3ICEBwxDpWJhE ZHZPBpPWgN+dQZDR2FiGHJgUFE3EM+CIcwxekrRBP+R3xEUeMFf5z1HeQNK8sjZe RwIDAQAB -----END PUBLIC KEY-----「-----BEGIN PUBLIC KEY-----」の後と「-----END PUBLIC KEY-----」の前に改行記号を入れた文字列を作ります。改行記号の前後にスペースがあるとエラーが出ます。
str = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0bXcnrheJ2snfq1wv6Qz 8+TEPDGKHCM0SsrQjxEFpXSEycL2/A+oW1ZGUzCuhz4HH4wkvc4CDJl25johSIUT Vyo4mrFrJ0ab0QAhrWE7gMyWFIfraj9cksPAGyVAiXLCN9Ly2xuoJxFjCAZXw1VO 8i7RTYK8ZP6dhcosiyzdhYt7C/65B5ikmCS4AymXIa83QQanCtjoGiwy4Cf2pLnn 9zXMZEnqQ+wwSoGn32YExmap7GAtjOwHNWU5zpW3dwNMq+zkcln3ICEBwxDpWJhE ZHZPBpPWgN+dQZDR2FiGHJgUFE3EM+CIcwxekrRBP+R3xEUeMFf5z1HeQNK8sjZe RwIDAQAB\n-----END PUBLIC KEY-----"その文字列を OpenSSL::PKey::RSA に渡します。
public_key_object = OpenSSL::PKey::RSA.new(str)
- 投稿日:2020-01-15T01:05:35+09:00
【Ruby】ハッシュとJSONの変換メモ
前提
- Ruby 2.7
- Hash -> JSON は JSONモジュールの
to_json
を使用- JSON -> Hash は
JSON.parse
を使用メモ
JSONはキーが文字列限定。Rubyのハッシュはキー、値ともに任意のオブジェクト。
ということは、RubyのハッシュからJSONに変換するときは、何らかのルールで、元のハッシュオブジェクトにJSONがサポートしていない型があった場合は変換されることとなる。以前のメモ
JSONの形式の仕様ざっくりまとめとサンプル集 - Qiita https://qiita.com/kure/items/df04ca760290fd51b904
Rubyのハッシュの小さなサンプルメモ - Qiita https://qiita.com/kure/items/4f104bf4af4a5e243e61
サンプル
JSON -> Hash
キーは文字列のまま、値もそのまま変換される。null は nil へ。
require "json" # JSON -> Hash json1 = '{ "name1": 0 }' json2 = '{ "name4": { "name5": 0 } }' json3 = '{ "name2": "value2" }' json4 = '{ "name4": null }' pp JSON.parse(json1) # => {"name1"=>0} pp JSON.parse(json2) # => {"name4"=>{"name5"=>0}} pp JSON.parse(json3) # => {"name2"=>"value2"} pp JSON.parse(json4) # => {"name4"=>nil}Hash -> JSON
- キー
- 強制的に文字列に変換される。nil は空文字列として変換される。- - 値
- シンボルやBoolean等はその文字のまま文字列として変換される。 Rubyのクラスやオブジェクト等、そのままではJSONに無い型は文字列として変換されるもよう。
# Hash -> JSON class Test; end hash1 = { 0 => 1 } pp hash1.to_json # => "{\"0\":1}" hash2 = { "name" => "value" } pp hash2.to_json # => "{\"name\":\"value\"}" hash3 = { true => "false_str" } pp hash3.to_json # => "{\"true\":\"false_str\"}" hash4 = { name: "value" } pp hash4.to_json # => "{\"name\":\"value\"}" hash5 = { nil => nil } pp hash5.to_json #=> "{\"\":null}" hash6 = { "class" => Class } pp hash6.to_json # => "{\"class\":\"Class\"}" hash7 = { "class" => Test } pp hash7.to_json # => "{\"class\":\"Test\"}" hash8 = { "bool" => true } pp hash8.to_json # => "{\"bool\":true}" hash9 = {Class => Class} pp hash9.to_json # => "{\"Class\":\"Class\"}"
- 投稿日:2020-01-15T00:11:27+09:00
Unknown action ,The action 'create' could not be found for MessagesControllerの一例
1.どんなエラー
MessagesControllerにcreateが定義されていませんよという内容です。
ちなみに筆者は下記の通りcreateは定義していました。<エラー文>
<エラーに該当するファイル>
messages_controller.rbclass MessagesController < ApplicationController before_action :set_group def index @message = Message.new @messages = @group.messages.includes(:user) end end def create @message = @group.messages.new(message_params) if @message.save redirect_to group_messages_path(@group), notice: 'メッセージが送信されました' else @messages = @group.messages.includes(:user) flash.now[:alert] = 'メッセージを入力してください。' render :index end end private def message_params params.require(:message).permit(:content, :image).merge(user_id: current_user.id) end def set_group @group = Group.find(params[:group_id]) end2.原因
今回の場合は'class MessagesController'の'end'の位置が'create'の定義の前で記入してしまっていたため、定義されていないという状態となっていました。
なので八行目の'end'を一番下にカットアンドペーストすることで解決します。3.類似ケース
ちなみに下記のurlでは変数名を間違っていたため、同様なエラーとなっていますが、筆者も変数'message'を'messages'としているというミスがあったため、そちらも確認していただければいいのかなと思います
<参考にした記事>
https://teratail.com/questions/83406<筆者が変数で間違えていた箇所>
_message.html.haml.messages ←この変数が複数形になっていました .chat-main__message-list .chat-main__message-list__name =message.user.name .chat-main__message-list__name__date =message.created_at.strftime("%y年%m月%d日%H時%M分") .chat-main__message-list__comment - if message.content.present? %p.chat-main__message-list__comment__content = message.content = image_tag message.image.url, class: 'chat-main__message-list__comment__content__image' if message.image.present?ご参考になればと思います