- 投稿日:2020-03-24T22:40:28+09:00
Railsフリマアプリ payjpを使ってクレジットカードを登録・削除
payjp とは・・・?
クレジットカードを登録、変更、購入を行ってくれる便利なAPIです。
みんさんもフリマアプリ、ECサイトでクレジットカードを使って商品の購入すると思います。その時登録したクレジットカード情報はどこに保存されているでしょうか?私は運営するサイトで暗号化されて管理されているのだと思っていましたが、違うようです。たしかにサイトのデータベースにクレジットカード情報を登録するのはセキュリティ上よろしくないですよね。
このとき登場するのがpayjp
になります。payjpの仕組み
まずpayjpとフリマアプリではどのような流れでクレジットカードの処理が行われているのかざっくりとイメージしましょう!
私はここを飛ばしてすぐコードを書き初めてため概念の理解に時間がかかりました。わかれば非常に単純でした。笑
以下の記事がとてもわかりやすく参考になります。
Pay.jpの仕組みについて!
セキュリティに加えて、payjpで発行されるトークンと顧客IDが紐つけられ永続化することで、ユーザーがスムーズに決済できることがポイントかと思います。実装内容
参考記事
実装においては以下の記事を参考としました。事情にわかりやすく説明されていています。
Payjpでクレジットカード登録と削除機能を実装する(Rails)基本的な実装な上記記事を見れば理解できると思います。今回は実装中につまずいたポイントや復習した内容についてまとめます。
JavaScript(jQuery)を使ってトークンを発行
ここまでの学習でJSについて理解が足りていなかったので、カリキュラムを復習、新しい知識をインプットしながらトークンの発行の仕方を理解しました。payjpに関して記事は結構あるのですが、コードがボーンって書いてあるだけだったので詳しく見ていきましょう!!
payjp.js
document.addEventListener( "DOMContentLoaded", e => { Payjp.setPublicKey("自分の公開鍵を記述する"); var btn = document.getElementById('token_submit'); btn.addEventListener("click", (e) => { e.preventDefault(); var card = { number: $("#number").val(), cvc: $("#cvc").val(), exp_month: $("#exp_month").val(), exp_year: $("#exp_year").val() }; Payjp.createToken(card, (status, response) => { if (status === 200) { $("#number").removeAttr("name"); $("#cvc").removeAttr("name"); $("#exp_month").removeAttr("name"); $("#exp_year").removeAttr("name"); $("#card_token").append( $('<input type="hidden" name="payjp-token">').val(response.id) ); document.inputForm.submit(); alert("登録が完了しました"); } else { alert("カード情報が正しくありません。"); } }); }); }, false );まずpayjpとに入力データを渡すために、公開鍵を記述します。
秘密鍵と公開鍵がありますので間違わないように注意しましょう。秘密鍵はcontrollerに記述します。Payjp.setPublicKey("自分の公開鍵を記述する");viewでカード情報を入力して、登録ボタン(#token_submit)をクリックした時、データを送信するデフォルトの機能を停止し、
var btn = document.getElementById('token_submit'); btn.addEventListener("click", (e) => { e.preventDefault();クレジットカード情報の、
number
,cvc
,exp_month
,exp_year
を.val()で属性の値を取得します。var card = { number: $("#number").val(), cvc: $("#cvc").val(), exp_month: $("#exp_month").val(), exp_year: $("#exp_year").val() };上記で取得した値は
Payjp.createToken()
の引数に渡し管理されます。
status === 200
はリクエストが成功している状況です。
ここでカード情報はサーバーには保存しないため、removeAttr
によって属性を削除します。そしてresponse.idを取得しトークンをデータベースに送るため隠しタグを生成します。Payjp.createToken(card, (status, response) => { if (status === 200) { $("#number").removeAttr("name"); $("#cvc").removeAttr("name"); $("#exp_month").removeAttr("name"); $("#exp_year").removeAttr("name"); $("#card_token").append( $('<input type="hidden" name="payjp-token">').val(response.id) ); document.inputForm.submit(); alert("登録が完了しました");最後に!
便利なAPIを使うと簡単に便利な機能を実装することができますが(結構時間かかったけど)、何でこの記述をしているのか考えるのもいい勉強になりました。特にJSがあまり理解できていなかったので、この機会復習もできました。
理解が間違っていましたらぜひ教えてください。
読んでくださりありがとうございました!!
- 投稿日:2020-03-24T22:33:29+09:00
Kinx 実現技術 - JIT
JIT Compile - Just In Time コンパイル
はじめに
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。作ったものの紹介だけではなく実現のために使った技術を紹介していくのも貢献。その道の人には当たり前でも、そうでない人にも興味をもって貰えるかもしれない。
前回のテーマは VM (Virtual Machine)。今回のテーマは JIT。
- 参考
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
- 個別記事へのリンクは全てここに集約してあります。
- リポジトリ ... https://github.com/Kray-G/kinx
- Pull Request 等お待ちしております。
正直、JIT は道半ばでコード晒すのもイマイチだが、参考にはなるかと思う。コントリビューター募集中というのもあるので興味があればよろしくお願いします。
JIT コンパイラとは
Just In Time、つまり まさにその場で コンパイルして実行する方式。ネイティブコード(機械語)にコンパイルして実行するため実行速度を追い求める人たちの間では欲しくてたまらない機能の一つ。
ただし、プロセッサーごとに個別に実装しないといけない。x64 だけ、とかならまだいいんだけど、どっかで ARM くらいはサポートすっかなー、とか、PowerPC は、SPARC?、とかになると、そもそもテストするのが大変だ。
Ruby 方式
Ruby も最近 JIT サポートをアピールしはじめたが、その辺の大変さからうまく?逃げている。つまり、C ソースを作成して、他のコンパイラでコンパイル したものを自分のプロセスに取り込む、といったことをしている。コンパイラがホストコンピュータ用のコードを出力してくれるし、もともと他人のモノなので Ruby 自体はプロセッサーを意識しなくて良い。
まあ逃げ方としてはそうなるとは思うが、しかしそれでは実行環境にコンパイラが必要になってしまう。しかもなにより Windows 環境で Visual Studio をインストールしてください、とか、Cygwin インストールしてください、とは 言えない というのが私の感覚です。開発者以外にコンパイラをインストールさせるのは ... かなり気が引ける。
sljit
そこで sljit を採用した。sljit はアセンブラを抽象化しており、その抽象化アセンブラで書くことでコード出力時に x64 や ARM などに合わせて出力ができるようになっている。その代わり、レジスタ数とかどのプロセッサーでも共通に使えるようにミニマムに合わせておかなければならない。
ただし、x64 以外ではテストしていないので悪しからず。
尚、ネイティブ・コード用 IR 出力ルーチンは src/ast_native.c にあり、sljit を使ったネイティブ・コード用コンパイラは src/nir_compile.c にある。そして、まだ実装されていないものが沢山あることがわかる。。。
個別の実装(特徴)
いくつか個別の実装の内容を記載してみる。まだまだやらなきゃいけないことが沢山あるなあ。
IR (Intermediate Representation)
ネイティブ・コードに変換する上で扱いやすいように、VM 用の IR と異なるコードを出力する。いやいや本当に違うものを出力する。なぜなら、例外の扱いとかコンパイル時点における型チェックの厳密さとかが全く異なるからで、それに合わせて違う体系になっている。
例えば;
native nfib(n) { if (n < 3) return n; return nfib(n-2) + nfib(n-1); } function fib(n) { if (n < 3) return n; return fib(n-2) + fib(n-1); }これは以下のようになる。
nfib(1, 10): .L0 1: jmp .L1 .L1 2: load r1, $(0,0) 3: lt r2 = r1, 3 4: jz .L4 .L2 5: load r3, $(0,0) 6: ret r3 .L3 8: jmp .L4 .L4 9: load r4, $(0,0) a: sub r5 = r4, 2 b: arg r5 c: call r6 = [rec-call]() d: excpt r7 = check e: jz .L6 .L5 f: excpt off 10: excpt on 11: ret 0 .L6 13: load r7, $(0,0) 14: sub r8 = r7, 1 15: arg r8 16: call r9 = [rec-call]() 17: excpt r10 = check 18: jz .L8 .L7 19: excpt off 1a: excpt on 1b: ret 0 .L8 1d: add r10 = r6, r9 1e: ret r10 .L9 20: ret 0 fib: .L460 d06: enter 23, vars(1), args(1) .L461 d07: lt_v0i $0(0), 3 d08: jz .L463(d0a) .L462 d09: retvl0 $0(0) .L463 d0a: pushvl0 $0(0) d0b: subi 2 d0c: callvl1 $1(34), 1 d0d: pushvl0 $0(0) d0e: subi 1 d0f: callvl1 $1(34), 1 d10: add d11: ret d12: halt長くなるので各命令の意味は省略します。ちょっと無駄なコードも見え隠れ(隠れてない)するが、こんな感じの出力を現時点ではする。しかし、こうやって書くと改善点も客観的に見えるというメリットもあるね。
例外処理
ネイティブコードでも
try-catch-finally
をサポートするにあたり、多少工夫が必要。setjmp/longjmp が使えれば簡単だが、JIT コード内で、しかも複数プロセッサー対応を目指した抽象化アセンブラの範囲内では使えないので実現するのはちょっと骨が折れた。具体的には以下の通りに実装されている。
- 例外が発生したかをチェックできる特別なフラグを用意する。
- 例外発生時、フラグを立て、キャッチ・ルーチンがあればそこにジャンプする。
finally
があればそれを実行する。- 同じ関数内に次の(外側の)キャッチ・ルーチンがあれば、そこにジャンプ。
- 無ければ return する。
- リターンは関数呼び出しの次のコードなので、そこには例外が発生したかのフラグを確認するコードを出力しておく。
- 例外発生を検知したら、上記と同じ形で伝播させていく。
スタックトレースも作れるはずなのだが、他のことを優先しているので今はできていない。どっかできちんとやる。
型チェック
ネイティブコードでは型チェックは重要。関数の入口で引数の型をチェック。レキシカル変数をアクセスする場合もチェック。関数から帰ってきたときにもチェック。型が期待するものと違ってたらすぐさま例外を上げるようになっている。
Switch-Case
sljit のライブラリを色々見たのだが、sljit の世界の中でどうしてもジャンプテーブルの実現方法が分からなかったので、今はサポートできていない(具体的にはレジスタ値にジャンプする方法がインターフェースとして用意されていない模様)。
最悪は
if-else
で実現するくらいだが、ジャンプテーブルの無い Switch-Case は軽自動車並みのエンジンで見た目だけ F1 カーといった感じなので、やる意味があるかどうか。sljit を改造するという手もあるが、x64 以外はすぐに対応できそうもない、といったところ。VM 関数呼び出し
これも現在は未サポート。子 VM を起動してジャンプすれば良いとは思うが、入れ子になった VM 内で例外が発生した場合の処理が大変そうなので、まだ手がつけられていない。おそらく以下のような感じでできるのでは、と思っているが、どうだろう。
- フレームに
native
からのコールであるフラグを立てる。return
時、native
からであれば復帰値をセットして VM からreturn
する。- 例外発生時、スタックを巻き戻している最中に
native
コールされたフラグを見つけた場合、そこでスタックトレース情報の作成を中断し、native
向けの例外情報を設定してnative
にリターン。native
側に帰ってきたら、native
での例外伝搬の仕組みで伝搬させていく。他に気にするところはあるかな。例外のスタックトレースを
native
関数内でも保存するようにしないといけないな。やってみる時間があれば挑戦してみる、という予定。オブジェクトへのアクセス
レキシカル変数へのアクセスはできるので、できるはずだがこれもまだ未サポート。演算で認められていない型同士の演算をきちんと拒絶、もしくはきちんとキャストするようにした上で、オブジェクトへのインデックス・アクセスをできるようにすれば良いだけだが、先にテストコードとドキュメントとライブラリを充実させたいもので。
そのうちやる。
レジスタ割り付け
複数のプロセッサーに対応させようとすると、レジスタ数の縛りが結構キツイ。なので割り切ってレジスタ割り付けは省略。全部ストア・ロードで対応。出力コードは汚いが、マイクロベンチマークである程度パフォーマンスが出ているので、良しとした。
尚、最初の例ではあえて載せていなかったが、x64 向けコードを出力させてみると以下のようになる。うむ、汚い。大半は型チェックと例外チェックなのだが、演算内容に対しても全くレジスタ割り付けしていないのも分かるだろう。毎回ストア・ロードしているし、無駄にストアしたのを再度ロードしたりしているのでその辺は簡単に改善できそうではある。今のところ重要度は高くないのでペンディング中。
return
くらいは一か所に集めるか。nfib: (native-base:0x7f1b2aa50010) 0: 53 push rbx 1: 41 57 push r15 3: 41 56 push r14 5: 41 55 push r13 7: 55 push rbp 8: 41 54 push r12 a: 48 8b df mov rbx, rdi d: 4c 8b fe mov r15, rsi 10: 4c 8b f2 mov r14, rdx 13: 48 81 ec 68 02 00 00 sub rsp, 0x268 1a: 49 8b 47 08 mov rax, qword [r15+0x00000008] 1e: 48 83 c0 01 add rax, 0x01 22: 49 89 47 08 mov qword [r15+0x00000008], rax 26: 48 3d 00 04 00 00 cmp rax, 0x400 2c: 72 29 jb 0x00000057 2e: 48 c7 43 20 01 00 00 00 mov qword [rbx+0x00000020], 0x01 36: 48 c7 43 28 06 00 00 00 mov qword [rbx+0x00000028], 0x06 3e: 48 c7 c0 00 00 00 00 mov rax, 0x00 45: 48 81 c4 68 02 00 00 add rsp, 0x268 4c: 41 5c pop r12 4e: 5d pop rbp 4f: 41 5d pop r13 51: 41 5e pop r14 53: 41 5f pop r15 55: 5b pop rbx 56: c3 ret 57: 49 83 bf 10 01 00 00 01 cmp qword [r15+0x00000110], 0x01 5f: 0f 85 52 02 00 00 jnz 0x000002b7 65: 49 8b 57 10 mov rdx, qword [r15+0x00000010] 69: 48 89 14 24 mov qword [rsp], rdx 6d: eb 00 jmp 0x0000006f 6f: 48 8b 14 24 mov rdx, qword [rsp] 73: 48 89 94 24 18 02 00 00 mov qword [rsp+0x00000218], rdx 7b: 48 c7 84 24 20 02 00 00 01 00 mov qword [rsp+0x00000220], 0x01 87: 00 00 48 83 bc 24 18 02 00 cmp qword [rsp+0x00000218], 0x03 90: 00 03 jb 0x0000009e 92: 72 0c 48 c7 84 24 20 02 00 00 mov qword [rsp+0x00000220], 0x00 9e: 00 00 00 00 48 83 bc 24 20 cmp qword [rsp+0x00000220], 0x00 a7: 02 00 jz 0x000000d3 a9: 00 00 74 2a mov rdx, qword [rsp] ad: 48 8b 14 24 48 89 94 24 mov qword [rsp+0x00000228], rdx b5: 28 02 00 00 48 8b 84 24 mov rax, qword [rsp+0x00000228] bd: 28 02 00 00 48 81 c4 add rsp, 0x268 c4: 68 02 pop r12 c6: 00 pop rbp c7: 00 41 pop r13 c9: 5c 5d pop r14 cb: 41 5d pop r15 cd: 41 pop rbx ce: 5e ret cf: 41 5f jmp 0x0000006d d1: 5b c3 jmp 0x000000d3 d3: eb 9c eb 00 mov rdx, qword [rsp] d7: 48 8b 14 24 48 89 94 24 mov qword [rsp+0x00000230], rdx df: 30 02 00 00 48 8b 94 24 mov rdx, qword [rsp+0x00000230] e7: 30 02 00 00 sub rdx, 0x02 eb: 48 83 ea 02 48 89 94 24 mov qword [rsp+0x00000238], rdx f3: 38 02 00 00 48 8b 94 24 mov rdx, qword [rsp+0x00000238] fb: 38 02 00 00 48 mov qword [rsp+0x00000018], rdx 100: 89 54 24 18 48 c7 84 24 18 01 mov qword [rsp+0x00000118], 0x01 10c: 00 00 01 00 mov rcx, qword [rbx+0x00000018] 110: 00 00 48 mov rax, rbx 113: 8b 4b 18 48 mov rdx, qword [r15+0x00000008] 117: 89 d8 49 8b 57 mov qword [rsp+0x00000010], rdx 11c: 08 48 89 54 24 lea rsi, [rsp+0x00000008] 121: 10 48 8d mov rdi, rax 124: 74 24 call rcx 126: 08 48 89 c7 ff d1 48 89 mov qword [rsp+0x00000240], rax 12e: 84 24 40 02 00 00 48 c7 84 24 mov qword [rsp+0x00000248], 0x01 13a: 48 02 00 00 mov rax, qword [rbx+0x00000020] 13e: 01 00 00 00 cmp rax, 0x00 142: 48 8b jnz 0x00000150 144: 43 20 48 83 f8 00 75 0c 48 c7 mov qword [rsp+0x00000248], 0x00 150: 84 24 48 02 00 00 00 00 00 cmp qword [rsp+0x00000248], 0x00 159: 00 48 83 bc 24 48 jz 0x0000018e 15f: 02 00 00 00 0f 84 2f 00 mov qword [rbx+0x00000020], 0x00 167: 00 00 48 c7 43 20 00 00 mov qword [rbx+0x00000020], 0x01 16f: 00 00 48 c7 43 20 01 00 mov rax, qword [rsp+0x00000210] 177: 00 00 48 8b 84 24 10 add rsp, 0x268 17e: 02 00 pop r12 180: 00 pop rbp 181: 48 81 pop r13 183: c4 68 pop r14 185: 02 00 pop r15 187: 00 pop rbx 188: 41 ret 189: 5c 5d 41 5d 41 jmp 0x0000006d 18e: 5e 41 5f 5b mov rdx, qword [rsp] 192: c3 e9 df fe ff ff 48 8b mov qword [rsp+0x00000248], rdx 19a: 14 24 48 89 94 24 48 02 mov rdx, qword [rsp+0x00000248] 1a2: 00 00 48 8b sub rdx, 0x01 1a6: 94 24 48 02 00 00 48 83 mov qword [rsp+0x00000250], rdx 1ae: ea 01 48 89 94 24 50 02 mov rdx, qword [rsp+0x00000250] 1b6: 00 00 48 8b 94 mov qword [rsp+0x00000018], rdx 1bb: 24 50 02 00 00 48 89 54 24 18 mov qword [rsp+0x00000118], 0x01 1c7: 48 c7 84 24 mov rcx, qword [rbx+0x00000018] 1cb: 18 01 00 mov rax, rbx 1ce: 00 01 00 00 mov rdx, qword [r15+0x00000008] 1d2: 00 48 8b 4b 18 mov qword [rsp+0x00000010], rdx 1d7: 48 89 d8 49 8b lea rsi, [rsp+0x00000008] 1dc: 57 08 48 mov rdi, rax 1df: 89 54 call rcx 1e1: 24 10 48 8d 74 24 08 48 mov qword [rsp+0x00000258], rax 1e9: 89 c7 ff d1 48 89 84 24 58 02 mov qword [rsp+0x00000260], 0x01 1f5: 00 00 48 c7 mov rax, qword [rbx+0x00000020] 1f9: 84 24 60 02 cmp rax, 0x00 1fd: 00 00 01 00 00 00 jnz 0x0000020f 203: 48 8b 43 20 48 83 f8 00 0f 85 mov qword [rsp+0x00000260], 0x00 20f: 0c 00 00 00 48 c7 84 24 60 cmp qword [rsp+0x00000260], 0x00 218: 02 00 00 00 00 00 jz 0x0000024d 21e: 00 48 83 bc 24 60 02 00 mov qword [rbx+0x00000020], 0x00 226: 00 00 0f 84 2f 00 00 00 mov qword [rbx+0x00000020], 0x01 22e: 48 c7 43 20 00 00 00 00 mov rax, qword [rsp+0x00000210] 236: 48 c7 43 20 01 00 00 add rsp, 0x268 23d: 00 48 pop r12 23f: 8b pop rbp 240: 84 24 pop r13 242: 10 02 pop r14 244: 00 00 pop r15 246: 48 pop rbx 247: 81 ret 248: c4 68 02 00 00 jmp 0x0000006d 24d: 41 5c 5d 41 5d 41 5e 41 mov rdx, qword [rsp+0x00000240] 255: 5f 5b c3 e9 20 fe ff ff add rdx, qword [rsp+0x00000258] 25d: 48 8b 94 24 40 02 00 00 mov qword [rsp+0x00000260], rdx 265: 48 03 94 24 58 02 00 00 mov rax, qword [rsp+0x00000260] 26d: 48 89 94 24 60 02 00 add rsp, 0x268 274: 00 48 pop r12 276: 8b pop rbp 277: 84 24 pop r13 279: 60 02 pop r14 27b: 00 00 pop r15 27d: 48 pop rbx 27e: 81 ret 27f: c4 68 02 00 00 jmp 0x0000006d 284: 41 5c 5d 41 5d 41 5e 41 mov rax, qword [rsp+0x00000210] 28c: 5f 5b c3 e9 e9 fd ff add rsp, 0x268 293: ff 48 pop r12 295: 8b pop rbp 296: 84 24 pop r13 298: 10 02 pop r14 29a: 00 00 pop r15 29c: 48 pop rbx 29d: 81 ret 29e: c4 68 02 00 00 41 5c mov rax, 0x00 2a5: 5d 41 5d 41 5e 41 5f add rsp, 0x268 2ac: 5b c3 pop r12 2ae: 48 pop rbp 2af: c7 c0 pop r13 2b1: 00 00 pop r14 2b3: 00 00 pop r15 2b5: 48 pop rbx 2b6: 81 ret 2b7: c4 68 02 00 00 41 5c 5d mov qword [rbx+0x00000020], 0x01 2bf: 41 5d 41 5e 41 5f 5b c3 mov qword [rbx+0x00000028], 0x07 2c7: 48 c7 43 20 01 00 00 mov rax, 0x00 2ce: 00 48 c7 43 28 07 00 add rsp, 0x268 2d5: 00 00 pop r12 2d7: 48 pop rbp 2d8: c7 c0 pop r13 2da: 00 00 pop r14 2dc: 00 00 pop r15 2de: 48 pop rbx 2df: 81 ret最適化
最適化は 全く やってない。JITに関してはそんなに頑張らなくてもいいかなーと。
というか、実は VM コードに対しても現時点では全くやってない。定数の畳み込みすらやってないので、まずはそっちから。
今後
優先順位によってすぐにはできないかもしれないが、上記問題の解決はどこかでやるつもり。
それ以外、せっかく型指定できるようにしているので、
native
以外での部分 JIT はできるようにした方がいいよね。逆にそれができればnative
いらない。というか、単なるint
省略記法になる。JIT とは言っても、一般的なトレーシングとかではなく、コンパイル時にコード出力してしまうのだけどね。この辺の言葉の使い方も難しい。Just In Time の範囲がどこまでかによって変わるが、一般的に 実行時に ということのようなので、これも JIT のうちでしょう。おわりに
こうして記事にしてみると、何気に具体的な課題が見つかっていい感じですね。予定立てます。
ではでは、今回もここまで読んでいただいてありがとうございます。最後はいつもの以下の定型フォーマットです。興味がありましたら ★ とか 「
いいねLGTM」 ボタンとか押してもらえるとモチベーションにつながります。どうぞよろしくお願いします。
- 最初の動機は スクリプト言語 KINX(ご紹介) を参照してください(もし宜しければ「
いいねLGTM」ボタンをポチっと)。- リポジトリは ここ(https://github.com/Kray-G/kinx) です。こちらももし宜しければ★をポチっと。
- 投稿日:2020-03-24T22:31:16+09:00
商品出品画面を作る
メルカリの商品出品画面を参考にコピーを作る
参考画面メルカリ
使用する機能
- ActiveStorage(画像投稿)リンク
- ancestry(多階層カテゴリー)
- active_hash(静的データ作成)
とりあえず出来たコード
new.html.haml.sell %header.sell-header = link_to root_path do = image_tag 'mercari_top_logo.svg', alt: 'mercari', height: '49', width: '185' -#メイン部分 %main %section.sell-container = form_with model: @item do |f| -# 画像部分 .sell-container__content .sell-title %h3.sell-title__text 出品画像 %span.sell-title__require 必須 .sell-container__content__max-sheet 最大10枚までアップロードできます .sell-container__content__upload .sell-container__content__upload__items .sell-container__content__upload__items__box %ul#output-box %div#image-input{tabindex:"0"} = f.label :images, for: "item_images0", class: 'sell-container__content__upload__items__box__label', data: {label_id: 0 } do = f.file_field :images, multiple: true, class: "sell-container__content__upload__items__box__input", id: "item_images0", style: 'display: none;' %pre %i.fas.fa-camera.fa-lg ドラッグアンドドロップ またはクリックしてファイルをアップロード .error-messages#error-image -#商品名部分 .sell-container__content .sell-title %h3.sell-title__text 商品名 %span.sell-title__require 必須 = f.text_field :name, {class:'sell-container__content__name', required: "required", placeholder: '商品名(必須 40文字まで)'} .error-messages#error-name .sell-title %h3.sell-title__text 商品の説明 %span.sell-title__require 必須 = f.text_area :text,{class: 'sell-container__content__description', required: "required", rows: '7', maxlength: '1000', placeholder: text_placeholder} -# placeholderでtems_helperを呼び出す .sell-container__content__word-count %span#word-count 0 /1000 .error-messages#error-text -# 詳細部分 .sell-container__content %h3.sell-sub-head 商品の詳細 .sell-container__content__details .sell-title %h3.sell-title__text カテゴリー %span.sell-title__require 必須 .sell-collection_select = f.label :category_id, {class: 'sell-collection_select__label'} do = f.collection_select :category_id, @category_parent, :id, :name, {prompt: "選択して下さい"},{ class: 'sell-collection_select__input', id: 'category-select', required: "required"} %i.fas.fa-chevron-down .error-messages#error-category .sell-title %h3.sell-title__text 商品の状態 %span.sell-title__require 必須 .sell-collection_select = f.label :condition_id, {class: 'sell-collection_select__label'} do = f.collection_select :condition_id, Condition.all, :id, :condition, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'condition-select', required: "required"} %i.fas.fa-chevron-down .error-messages#error-condition -# 配送部分 .sell-container__content %h3.sell-sub-head %p 配送について = link_to '/delivery',target: '_blank',class: 'sell-sub-head__guides-link' do %i.far.fa-question-circle .sell-container__content__delivery .sell-title %h3.sell-title__text 配送料の負担 %span.sell-title__require 必須 .sell-collection_select = f.label :deliverycost_id, {class: 'sell-collection_select__label'} do = f.collection_select :deliverycost_id, Deliverycost.all, :id, :payer, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'deliverycost-select', required: "required"} %i.fas.fa-chevron-down .error-messages#error-deliverycost .sell-title %h3.sell-title__text 発送元の地域 %span.sell-title__require 必須 .sell-collection_select = f.label :pref_id, class: 'sell-collection_select__label' do = f.collection_select :pref_id, Pref.all, :id, :name, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'pref-select', required: "required"} %i.fas.fa-chevron-down .error-messages#error-pref .sell-title %h3.sell-title__text 発送までの日数 %span.sell-title__require 必須 .sell-collection_select = f.label :delivery_days_id, class: 'sell-collection_select__label' do = f.collection_select :delivery_days_id, DeliveryDays.all, :id, :days, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'delivery_days-select', required: "required"} %i.fas.fa-chevron-down .error-messages#error-delivery_days -# 価格部分 .sell-container__content %h3.sell-sub-head %p 販売価格(300〜9,999,999) = link_to '/price',target: '_blank', class: 'sell-sub-head__guides-link' do %i.far.fa-question-circle .sell-container__content__price .sell-title %h3.sell-title__text 販売価格 %span.sell-title__require 必須 .sell-container__content__price__form = f.label :price, class: 'sell-container__content__price__form__label' do ¥ = f.number_field :price, {placeholder: '0', value: '', autocomplete:"off", class: 'sell-container__content__price__form__box', required: "required"} .error-messages#error-price .sell-container__content__commission .sell-container__content__commission__left 販売手数料 (10%) .sell-container__content__commission__right ー .sell-container__content__profit .sell-container__content__profit__left 販売利益 .sell-container__content__profit__right ー .submit-btn = f.submit '出品する', class: 'submit-btn__sell-btn' = link_to 'もどる', root_path, class: 'submit-btn__return-btn' .attention-box %p 禁止されている = link_to '行為', '/prohibited_conduct', target: '_blank' および = link_to '出品物', '/prohibited_item', target: '_blank' を必ずご確認ください。 = link_to '偽ブランド品', '/counterfeit_goods', target: '_blank' や = link_to '盗品物', '/stolen_goods', target: '_blank' などの販売は犯罪であり、法律により処罰される可能性があります。また、出品をもちまして = link_to '加盟店規約', '/seller_terms', target: '_blank' に同意したことになります。 %footer.sell-footer %nav %ul.clearfix %li = link_to '#' do プライバシーポリシー %li = link_to '#' do メルカリ利用規約 %li = link_to '#' do 特定商取引に関する表記 = link_to root_path, class: 'footer__logo' do = image_tag 'logo-gray.svg', alt: 'mercari', height: '65', width: '80' %p %small © Mercari, Inc.items_new.scssa { color: inherit; text-decoration: none; box-sizing: border-box; } img { vertical-align: middle; box-sizing: border-box; } .error-messages{ color: #ff0211; font-size: 14px; line-height: 1.4em; margin: 16px 0; box-sizing: border-box; } .sell-title{ align-items: center; margin: 0!important; box-sizing: border-box; &__text{ font-size: 14px; font-weight: 600; line-height: 1.4em; } &__require{ margin-left: 8px; font-size: 12px; padding: 0 4px; background-color: #ff0211; color: #fff; border-radius: 2px; display: inline-block; font-style: normal; font-weight: 600; line-height: 1.4em; margin: 0; } } .sell-sub-head{ box-sizing: border-box; color: rgb(136, 136, 136); font-size: 14px; font-weight: 600; line-height: 1.4em; margin-bottom: 24px; display: flex; &__guides-link{ color: rgb(0, 149, 238); margin-left: 4px; } } .sell-collection_select{ box-sizing: border-box; margin-top: 16px; &__label{ display: inline-block; position: relative; width: 100%; .fas.fa-chevron-down{ box-sizing: border-box; pointer-events: none; position: absolute; right: 16px; top: 40%; color: rgb(136, 136, 136); height: 48px; } } &__input{ -webkit-appearance: none; -moz-appearance: none; appearance: none; background-color: #fff; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; color: #222; font-size: 16px; height: 48px; line-height: 1; margin: 0; outline: none; padding: 0 56px 0 16px; width: 100%; } } // ここより上は繰り返し使用するパーツ .sell { box-sizing: border-box; position: relative; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); font-family: Arial, 游ゴシック体, YuGothic, メイリオ, Meiryo, sans-serif; font-size: 14px; line-height: 1; box-sizing: border-box; .sell-header{ box-sizing: border-box; height: 128px; align-items: center; display: flex; justify-content: center; } .sell-container{ box-sizing: border-box; max-width: 700px; width: 100%; margin: 0px auto; background-color: rgb(255, 255, 255); // 画像部分 &__content{ height: auto; padding: 40px; border-bottom: 1px; border-bottom-color: #efefef; border-bottom-style: solid; &__max-sheet{ margin-top: 16px; } &__upload{ margin-top: 16px; display: flex; flex-wrap: wrap; &__items{ height: auto; width: 100%; &__box{ height: auto; align-content: center; align-items: center; cursor: pointer; display: flex; flex-wrap: wrap; justify-content: center; position: relative; border-width: 1px; #output-box{ box-sizing: border-box; display: flex; flex-wrap: wrap; width: 100%; height: auto; .preview-image{ box-sizing: border-box; height: 150px; width: 20%; padding: 0px 4px; margin-top: 8px; &__figure{ margin:0 auto; height: 118px; background-color: rgb(245, 245, 245); img{ box-sizing: border-box; width: 100%; height: 100%; object-fit: contain; } } &__button{ border-top-width: 1px; border-top-color: rgb(204, 204, 204); border-top-style: solid; background-color: rgb(245, 245, 245); justify-content: space-around; display: flex; align-items: center; height: 32px; color: rgb(0, 149, 238); } } #image-input{ box-sizing: border-box; height: 100%; -webkit-flex: 1; flex: 1; margin-top: 8px; .sell-container__content__upload__items__box__label{ box-sizing: border-box; background-color: rgb(245, 245, 245); height: 150px; border-width: 1px; border-style: dashed; border-color: rgb(204, 204, 204); text-align: center; display: flex; align-items: center; justify-content: center; i{ box-sizing: border-box; margin-bottom: 8px; } } } } } } } } // 商品名部分 &__content{ &__name{ margin-top: 16px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; height: 48px; padding: 0 16px; width: 100%; } &__description{ margin-top: 16px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; padding: 16px; width: 100%; font-size: 16px; display: block; } &__word-count{ text-align: right; color: #888; font-size: 12px; line-height: 1.4em; } } // 詳細は共通パーツのみ // 配送は共通パーツのみ // 価格部分 &__content{ &__price{ -webkit-box-align: center; align-items: center; box-sizing: content-box; display: flex; height: 46px; justify-content:space-between; &form__label{ font-size: 14px; } &__form__box{ border: 1px solid #ccc; border-radius: 4px; height: 48px; margin-left: 8px; padding: 0 16px; width: 300px; align-items: center; display: inline-flex; text-align: right; } } #error-price{ box-sizing: border-box; text-align: right; } &__commission{ display: flex; justify-content:space-between; height: 70px; padding: 12px 0px; align-items: center; border-bottom: 1px; border-bottom-color: #efefef; border-bottom-style: solid; } &__profit{ display: flex; justify-content:space-between; height: 70px; padding: 12px 0px; align-items: center; } } .submit-btn{ box-sizing: border-box; margin: 0 auto; width: 360px; margin-bottom: 32px; &__sell-btn{ background-color: #ea352d; color: #fff; margin-bottom: 24px; width: 100%; font-size: 17px; height: 48px; font-weight: 600; border-radius: 4px; } &__return-btn{ background-color: #ccc; color: #222; width: 100%; font-size: 17px; height: 48px; font-weight: 600; padding: 14px 0; border-radius: 4px; display: inline-block; text-align: center; } } .attention-box{ box-sizing: border-box; font-size: 12px; line-height: 1.4em; word-break: keep-all; a{ box-sizing: border-box; color: #0095ee; } } } .sell-footer{ box-sizing: border-box; padding: 40px 0px; text-align: center; nav{ box-sizing: border-box; .clearfix{ box-sizing: border-box; display: inline-block; font-size: 12px; display: flex; justify-content: center; margin-bottom: 40px; li{ box-sizing: border-box; margin: 0px 8px; } } } } }items_new.js$(document).on('turbolinks:load', function(){ // 画像が選択された時プレビュー表示、inputの親要素のulをイベント元に指定 $('#image-input').on('change', function(e){ //ファイルオブジェクトを取得する let files = e.target.files; $.each(files, function(index, file) { let reader = new FileReader(); //画像でない場合は処理終了 if(file.type.indexOf("image") < 0){ alert("画像ファイルを指定してください。"); return false; } //アップロードした画像を設定する reader.onload = (function(file){ return function(e){ let imageLength = $('#output-box').children('li').length; // 表示されているプレビューの数を数える let labelLength = $("#image-input>label").eq(-1).data('label-id'); // #image-inputの子要素labelの中から最後の要素のカスタムデータidを取得 // プレビュー表示 $('#image-input').before(`<li class="preview-image" id="upload-image${labelLength}" data-image-id="${labelLength}"> <figure class="preview-image__figure"> <img src='${e.target.result}' title='${file.name}' > </figure> <div class="preview-image__button"> <a class="preview-image__button__edit" href="">編集</a> <a class="preview-image__button__delete" data-image-id="${labelLength}">削除</a> </div> </li>`); $("#image-input>label").eq(-1).css('display','none'); // 入力されたlabelを見えなくする if (imageLength < 9) { // 表示されているプレビューが9以下なら、新たにinputを生成する $("#image-input").append(`<label for="item_images${labelLength+1}" class="sell-container__content__upload__items__box__label" data-label-id="${labelLength+1}"> <input multiple="multiple" class="sell-container__content__upload__items__box__input" id="item_images${labelLength+1}" style="display: none;" type="file" name="item[images][]"> <i class="fas fa-camera fa-lg"></i> </label>`); }; }; })(file); reader.readAsDataURL(file); }); }); //削除ボタンが押された時 $(document).on('click', '.preview-image__button__delete', function(){ let targetImageId = $(this).data('image-id'); // イベント元のカスタムデータ属性の値を取得 $(`#upload-image${targetImageId}`).remove(); //プレビューを削除 $(`[for=item_images${targetImageId}]`).remove(); //削除したプレビューに関連したinputを削除 let imageLength = $('#output-box').children('li').length; // 表示されているプレビューの数を数える if (imageLength ==9) { let labelLength = $("#image-input>label").eq(-1).data('label-id'); // 表示されているプレビューが9なら,#image-inputの子要素labelの中から最後の要素のカスタムデータidを取得 $("#image-input").append(`<label for="item_images${labelLength+1}" class="sell-container__content__upload__items__box__label" data-label-id="${labelLength+1}"> <input multiple="multiple" class="sell-container__content__upload__items__box__input" id="item_images${labelLength+1}" style="display: none;" type="file" name="item[images][]"> <i class="fas fa-camera fa-lg"></i> </label>`); }; }); // f.text_areaの文字数カウント $("textarea").keyup(function(){ let txtcount = $(this).val().length; $("#word-count").text(txtcount); }); //販売価格入力時の手数料計算 $('#item_price').keyup(function(){ let price= $(this).val(); if (price >= 300 && price <= 9999999){ let fee = Math.floor(price * 0.1); // 小数点以下切り捨て let profit = (price - fee); $('.sell-container__content__commission__right').text('¥'+fee.toLocaleString()); // 対象要素の文字列書き換える $('.sell-container__content__profit__right').text('¥'+profit.toLocaleString()); } else{ $('.sell-container__content__commission__right').html('ー'); $('.sell-container__content__profit__right').html('ー'); } }); // 各フォームの入力チェック $(function(){ //画像 $('#image-input').on('focus',function(){ $('#error-image').text(''); $('#image-input').on('blur',function(){ $('#error-image').text(''); let imageLength = $('#output-box').children('li').length; if(imageLength ==''){ $('#error-image').text('画像がありません'); }else if(imageLength >10){ $('#error-image').text('画像を10枚以下にして下さい'); }else{ $('#error-image').text(''); } }); }); //送信しようとした時 $('form').on('submit',function(){ let imageLength = $('#output-box').children('li').length; if(imageLength ==''){ $('body, html').animate({ scrollTop: 0 }, 500); $('#error-image').text('画像がありません'); }else if(imageLength >10){ $('body, html').animate({ scrollTop: 0 }, 500); $('#error-image').text('画像を10枚以下にして下さい'); }else{ return false; } }); //画像を削除した時 $(document).on('click','.preview-image__button__delete',function(){ let imageLength = $('#output-box').children('li').length; if(imageLength ==''){ $('#error-image').text('画像がありません'); }else if(imageLength >10){ $('#error-image').text('画像を10枚以下にして下さい'); }else{ $('#error-image').text(''); } }); //商品名 $('.sell-container__content__name').on('blur',function(){ let value = $(this).val(); if(value == ""){ $('#error-name').text('入力してください'); $(this).css('border-color','red'); }else{ $('#error-name').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); //商品説明 $('.sell-container__content__description').on('blur',function(){ let value = $(this).val(); if(value == ""){ $('#error-text').text('入力してください'); $(this).css('border-color','red'); }else{ $('#error-text').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); //カテゴリー $('#category-select').on('blur',function(){ let value = $(this).val(); if(value == ""){ $('#error-category').text('選択して下さい'); $(this).css('border-color','red'); }else{ $('#error-category').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); //状態 $('#condition-select').on('blur',function(){ let value = $(this).val(); if(value == ""){ $('#error-condition').text('選択して下さい'); $(this).css('border-color','red'); }else{ $('#error-condition').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); //送料負担 $('#deliverycost-select').on('blur',function(){ let value = $(this).val(); if(value == ""){ $('#error-deliverycost').text('選択して下さい'); $(this).css('border-color','red'); }else{ $('#error-deliverycost').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); //発送元 $('#pref-select').on('blur',function(){ let value = $(this).val(); if(value == ""){ $('#error-pref').text('選択して下さい'); $(this).css('border-color','red'); }else{ $('#error-pref').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); //発送までの日数 $('#delivery_days-select').on('blur',function(){ let value = $(this).val(); if(value == ""){ $('#error-delivery_days').text('選択して下さい'); $(this).css('border-color','red'); }else{ $('#error-delivery_days').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); //価格 $('.sell-container__content__price__form__box').on('blur',function(){ let value = $(this).val(); if(value < 300 || value > 9999999){ $('#error-price').text('300以上9999999以下で入力してください'); $(this).css('border-color','red'); }else{ $('#error-price').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); }); });items.controller.rbdef new @item = Item.new @category_parent = Category.where("ancestry is null") end def create @item = Item.new(item_params) if @item.save redirect_to root_path else render :new end end private def item_params params.require(:item).permit(:name, :text, :category_id, :condition_id, :deliverycost_id, :pref_id, :delivery_days_id, :price, images: []).merge(user_id: current_user.id, boughtflg_id:"1") end enditem.rb(モデル)class Item < ApplicationRecord extend ActiveHash::Associations::ActiveRecordExtensions belongs_to_active_hash :condition belongs_to_active_hash :pref belongs_to_active_hash :deliverycost belongs_to_active_hash :delivery_days belongs_to_active_hash :boughtflg # 上記active_hashのアソシエーション validate :images_presence validates :name, :text, :category_id, :condition_id, :deliverycost_id, :pref_id, :delivery_days_id, :boughtflg_id, presence: true validates :price, presence: true, inclusion: 300..9999999 has_many_attached :images belongs_to :user, foreign_key: 'user_id' # optional: true後で消す belongs_toのnotnull制約解放のため使用している belongs_to :category #imageのバリデーション def images_presence if images.attached? # inputに保持されているimagesがあるかを確認 if images.length > 10 errors.add(:image, '10枚まで投稿できます') end else errors.add(:image, '画像がありません') end end endカテゴリーの2階層以降がまだ未実装でした・・・
とりあえず、後から実装するとして、先ずは画像投稿から
- 投稿日:2020-03-24T22:19:00+09:00
7つのアクション
今回は、コントローラで使用する7つのアクションについて紹介します!
アクションとは、ルーティングがリクエストを受け取った時に動くものですアクション毎に行われる処理を分けて記述することによって、役割をわかりやすく分類化できます!
アクション名 どんなリクエストに対応して動く? index 一覧ページを表示する new 新規投稿ページを表示する create データの投稿を行う show 個別の詳細ページを表示する edit 投稿編集ページを表示する update データの編集を行う destroy データの削除を行う この7つのアクションは基本になる所なので、ぜひ覚えて活用できるようにしてみましょう^^!
- 投稿日:2020-03-24T21:58:42+09:00
【Rails】アクセス制限を記述する手順と、その思考手順をまとめてみました
はじめに
Railsを学習した直後のアウトプット時、いざアクセス制限機能を実装しようと試みた際に、何をどうしたらいいのか思い浮かばなかったのが悔しかったため
次に同じ轍を踏まないよう、自分用にアクセス制限の実装順序を整理してみました。
まず、アクセス制限って「何を」実装する機能なのん?
主にWebアプリケーションにおいてログイン機能を実装した後で
ユーザーがログインしている場合と、ログインしていない場合とで、使える機能を制限する機能。
それじゃあ、「どうすれば」実装できるのか?
例えば
・ユーザーがログインしている場合で一つ条件式を記述し、制限用の処理を記述する。
・ユーザーがログインしていない場合でもう一つ条件式を書き、制限用の処理を記述する。例
if #(ユーザーがログインしている場合の条件式) redirect_to("URL") #制限用の処理 end
コードは「どこに」記述するん?
簡単な掲示板アプリ作成を例として、「ユーザーがログインしていない場合」のアクセス制限機能を実装したいとします。
1.Railでは、主にルーティング→アクション→ビューの順でデータのやり取りが行われるので
2.例えば、ログインしていないユーザーに投稿機能のアクセス制限を実装したい場合は
3.投稿機能用のアクションにデータが渡されたタイミングで、ログインしていないユーザーを弾けば良いということになりますので
4.投稿用のアクション最上部に、「ユーザーがログインしていない場合」の条件式と処理を記述してあげます。
5.そうすれば、投稿用のアクションが実行される前に、アクセス制限の処理が実行されます。よってコードをどこに記述すればいいのかと言うと、制限を実装したい機能のアクション内の上部、ということになります。
例
posts_controller.rbdef #投稿用アクション if #(ユーザーがログインしていない場合の条件式) redirect_to("URL") #指定したURLに飛ばす end #投稿用の処理 endbefore_actionとは文字通り、actionのbeforeに実行される
ただ投稿制限の他にも、投稿を閲覧する機能や投稿を編集する機能なども制限したい場合もあるかと思います。
その際、制限したいアクション全てに対し条件式を書こうとするのは、同じ記述をすることになり好ましくない上に面倒臭いです……。そこで、全てのアクションに対して一括で処理を行う方法が存在します。
どのように全てのアクションに対し一括で処理を行うかというと、applicationコントローラに処理を記述してあげます。
applicationコントローラとは、全てのコントローラを管理している親のような存在です。
画像の一番上にあるのが、applicationコントローラで
その直下に他のコントローラが羅列され、applicationコントローラの処理を継承しています。
そのため、applicationコントローラで記述された処理は、別のコントローラで実行されることになります。ただし注意する点があり、「before_action :メソッド名」という処理をコントローラ内の上部に記述してあげる必要があります。
言葉だけでは伝わりにくいと思いますので、コード例を記述してみます。application_controller.rbbefore_action :#メソッド名 # ログインしていない場合のアクセス制限 def #メソッド名 if #(アクセスしたユーザーがログインしていない場合の条件式) redirect_to("URL") #指定したURLに飛ばす end endこのようにapplicationコントローラ内に「before_action :メソッド名」と記述してあげることで、メソッド内で定義した処理が全てのコントローラのアクションが実行される前に実行されることになります。
before_actiontに関しては文字通り、全てのアクションの前に実行される処理という風に覚えておけば、用途をしっかり理解して使えるようになるかと思います。
before_actionを特定のアクションにだけ適用させる方法があるだと!?
全てのアクションへ処理が適用されるとは言っても、特定のアクションにだけアクセス制限を適用させたいという場合もあるでしょう。
そう言った場合には、特定のアクションにだけアクセス制限の処理を適用させる方法が存在します。applicationコントローラ内で記述した「before_action :メソッド名」を
適用させたいアクションが記述されているコントローラの上部に記述した上で
メソッド名に続けて「, {only: [:アクション名, :アクション名,...]}」として
[ ]内に、各コントローラ内のアクセス制限を適用させたいアクション名を記述してあげることで、特定のアクションにだけアクセス制限を適用することができます。例
posts_controller.rbbefore_action :#メソッド名, {only: [:投稿用アクション名, 投稿表示アクション名,...]} def #投稿用アクション名 #処理 end def #投稿表示用アクション名 #処理 endこうすることにより、各々アクションが実行される前に、before_actionの処理が呼び出され、各アクションへのアクセス制限が可能となります。
まとめ
「アウトプットでアクセス制限を実装したい!」となった場合、考える順序として
1.まずアクセス制限とは、ログイン機能実装後に「ログインしているユーザーへの機能制限」や「ログインしていないユーザーへの機能制限」を行うこと。
2.「投稿を制限したい」「投稿の閲覧を制限したい」など、「どんなアクセス制限」を実装したいか思い浮かべる。
3.もし投稿機能を制限したいなら、「投稿用のアクション」など、コードを記述すべき場所を思い浮かべる。
4.該当アクション内の上部に、「ユーザーがログインしていない場合は〜〜の処理を行う」などの「条件式」を記述する。
5.他の機能も制限したいけれど、複数個に渡って同じ記述をするのは面倒臭いな……。
6.複数アクションに制限を掛けたい場合は、applicationコントローラを使えば、一つの記述で済むことを思い出す。
7.applicationコントローラ内に、条件式とアクセス制限の処理を記述する。
8.各コントローラ内のアクションが実行される前に、アクセス制限処理を実行しなければならないことを思い出す(before_action :メソッド名)。
(9.特定のアクションにアクセス制限を掛けたい場合の処理は、「 before_action :メソッド名, {only: [:アクション名, :アクション名,...]} 」を、各コントローラ内の上部に記述する。)
最後に
周りくどい説明をしてきましたが、このあたりは基礎の基礎ですし
始めはRailsそのもののデータやり取りの仕組みを理解することが重要だと思ったので、今回のように遠回りするような解説をさせて頂きました。
僕も慣れるまでは、この思考手順で学習を進めるつもりです。参考
- 投稿日:2020-03-24T21:58:42+09:00
【Rails】アクセス制限を記述する手順をまとめてみました!
はじめに
Railsを学習した直後のアウトプット時、いざアクセス制限機能を実装しようと試みた際に、何をどうしたらいいのか思い浮かばなかったのが悔しかったため
次に同じ轍を踏まないよう、自分用にアクセス制限の実装順序を整理してみました。
まず、アクセス制限って「何を」実装する機能なのん?
主にWebアプリケーションにおいてログイン機能を実装した後で
ユーザーがログインしている場合と、ログインしていない場合とで、使える機能を制限する機能。
それじゃあ、「どうすれば」実装できるのか?
例えば
・ユーザーがログインしている場合で一つ条件式を記述し、制限用の処理を記述する。
・ユーザーがログインしていない場合でもう一つ条件式を書き、制限用の処理を記述する。例
if #(ユーザーがログインしている場合の条件式) redirect_to("URL") #制限用の処理 end
コードは「どこに」記述するん?
簡単な掲示板アプリ作成を例として、「ユーザーがログインしていない場合」のアクセス制限機能を実装したいとします。
1.Railsのデータのやり取りは、基本的にルーティング→アクション→ビューの順で行われ
2.例えば、ログインしていないユーザーに投稿機能のアクセス制限を掛けたい場合は
3.投稿用のアクション最上部に、「ユーザーがログインしていない場合」の条件式と処理を記述してあげます。
4.そうすれば、投稿用のアクションが実行される前に、アクセス制限の処理が実行されます。なので、位置的に言えばアクション内の上部に書けばいいということなのですね。
例
posts_controller.rbdef #投稿用アクション if #(ユーザーがログインしていない場合の条件式) redirect_to("URL") #指定したURLに飛ばす end #投稿用の処理 endbefore_actionとは文字通り、actionのbeforeに実行される
ただ投稿制限の他にも、投稿を閲覧する機能や投稿を編集する機能なども制限したい場合もあるかと思います。
その際、制限したいアクション全てに対し条件式を書こうとするのは、同じ記述をすることになり好ましくない上に面倒臭いです……。そこで、全てのアクションに対して一括で処理を行う方法が存在します。
どのように全てのアクションに対し一括で処理を行うかというと、applicationコントローラに処理を記述してあげます。
applicationコントローラとは、全てのコントローラを管理している親のような存在です。
画像の一番上にあるのが、applicationコントローラで
その直下に他のコントローラが羅列され、applicationコントローラの処理を継承しています。
そのため、applicationコントローラで記述された処理は、別のコントローラで実行されることになります。ただし注意する点があり、「before_action :メソッド名」という処理をコントローラ内の上部に記述してあげる必要があります。
言葉だけでは伝わりにくいと思いますので、コード例を記述してみます。application_controller.rbbefore_action :#メソッド名 # ログインしていない場合のアクセス制限 def #メソッド名 if #(アクセスしたユーザーがログインしていない場合の条件式) redirect_to("URL") #指定したURLに飛ばす end endこのようにapplicationコントローラ内に「before_action :メソッド名」と記述してあげることで、メソッド内で定義した処理が全てのコントローラのアクションが実行される前に実行されることになります。
before_actiontに関しては文字通り、全てのアクションの前に実行される処理という風に覚えておけば、用途をしっかり理解して使えるようになるかと思います。
before_actionを特定のアクションにだけ適用させる方法があるだと!?
全てのアクションへ処理が適用されるとは言っても、特定のアクションにだけアクセス制限を適用させたいという場合もあるでしょう。
そう言った場合には、特定のアクションにだけアクセス制限の処理を適用させる方法が存在します。applicationコントローラ内で記述した「before_action :メソッド名」を
適用させたいアクションが記述されているコントローラの上部に記述した上で
メソッド名に続けて「, {only: [:アクション名, :アクション名,...]}」として
[ ]内に、各コントローラ内のアクセス制限を適用させたいアクション名を記述してあげることで、特定のアクションにだけアクセス制限を適用することができます。例
posts_controller.rbbefore_action :#メソッド名, {only: [:投稿用アクション名, 投稿表示アクション名,...]} def #投稿用アクション名 #処理 end def #投稿表示用アクション名 #処理 endこうすることにより、各々アクションが実行される前に、before_actionの処理が呼び出され、各アクションへのアクセス制限が可能となります。
まとめ
「アウトプットでアクセス制限を実装したい!」となった場合、考える順序として
1.まずアクセス制限とは、ログイン機能実装後に「ログインしているユーザーへの機能制限」や「ログインしていないユーザーへの機能制限」を行うこと。
2.「投稿を制限したい」「投稿の閲覧を制限したい」など、「どんなアクセス制限」を実装したいか思い浮かべる。
3.例えば、投稿を制限したいなら「投稿用のアクション」など、コードを記述すべき場所を押さえる。
4.該当アクション内に、「ユーザーがログインしていない場合は〜〜の処理を行う」などの「条件式」を記述する。
5.ただし複数個に渡って同じ記述をするのは面倒臭い……。
6.複数アクションに制限を掛けたい場合は、applicationコントローラを使えば、一つの記述で済むことを思い出す。
7.applicationコントローラ内に、条件式とアクセス制限の処理を記述する。
8.各コントローラ内のアクションが実行される前に、アクセス制限処理を実行しなければならないことを思い出す(before_action :メソッド名)。
(9.特定のアクションにアクセス制限を掛けたい場合の処理は、「 before_action :メソッド名, {only: [:アクション名, :アクション名,...]} 」を、各コントローラ内の上部に記述する。)
参考
- 投稿日:2020-03-24T21:56:51+09:00
exifrを使用したフォルダ内画像のexifデータ一括抽出
プログラミング初心者です。
任意のフォルダ内に格納されている画像のexifを抽出するのに手間取ったため、備忘録として残します。require 'exifr/jpeg' require 'csv' puts Dir.pwd dir_name = "/Users/任意のフォルダパス" Dir.chdir(dir_name) dir = Dir.open(dir_name) puts Dir.pwd CSV.open('exif.csv','w') do |csv| csv << ['model','lens','f','exposure','iso','date'] name = p Dir.glob('*.JPG') name.each do |syashin| exif = EXIFR::JPEG.new(syashin) a = exif.model b = exif.lens_model c = exif.f_number.to_f d = exif.exposure_time e = exif.iso_speed_ratings f = exif.date_time csv << [a,b,c,d,e,f] end end画像と同じフォルダにcsvファイルが作成されます。
- 投稿日:2020-03-24T20:43:05+09:00
[WIP]RuboCop規約一覧表
一覧
区分 規約 Cop数 Bundler Bundlerファイル 4 Gemspec gemspecファイル 4 Layout インデント、行端揃え、スペースの一貫性 88 Lint 曖昧性と潜在エラー 82 Metrics プロパティ 9 Migration rubocop.yml内のCop名 1 Naming 命名規則 16 Security メソッド呼び出し、構造に潜むセキュリティホール 5 Style Ruby Style Guideに準拠したスタイルの一貫性 172 Bundler
Cop名 解析対象 デフォルト Bundler/DuplicatedGem Gemfile内のライブラリが重複していないか 有効 Bundler/GemComment Gemfile内のライブラリ用途を説明するコメントがあるか 無効 Bundler/InsecureProtocolSource ・シンボル引数の :gemcutter
,:rubygems
,:rubyforge
が使われていないか
・ソースURLに https://rubygems.org が指定されているか有効 Bundler/OrderedGems Gemfile内のライブラリがアルファベット昇順でソートされているか 有効 Gemspec
Cop名 解析対象 デフォルト Gemspec/DuplicatedAssignment gemspec内の属性定義メソッド呼出しが重複していないか 有効 Gemspec/OrderedDependencies gemspec内の依存関係がアルファベット昇順でソートされているか 有効 Gemspec/RequiredRubyVersion 以下ファイル内の指定Rubyバージョンが互いに一致しているか
・gemspec:required_ruby_version
・.rubocop.yml:TargetRubyVersion
有効 Gemspec/RubyVersionGlobalsUsage gemspec内で定数 RUBY_VERSION
が使われていないか有効 Layout
Cop名 解析対象 デフォルト Layout/AccessModifierIndentation 単体のアクセス修飾子(e.g. private
)のインデントの深さがdef
,class
,module
と同等か有効 Layout/ArgumentAlignment 複数行メソッド呼出しにおける引数が統一されたインデントがされているか 有効 Layout/ArrayAlignment 複数行配列の要素が統一されたインデントがされているか 有効 Layout/AssignmentIndentation 代入式における複数行右辺の一行目が統一されたインデントがされているか 有効 Layout/BlockAlignment 複数行ブロックの do ~ end
が統一されたインデントがされているか有効 Layout/BlockEndNewline 複数行ブロックの do ~ end
,{ ~ }
が独立した行に書かれているか有効 Layout/CaseIndentation ・case文における case ~ when ~ end
が統一されたインデントがされているか
・代入式における右辺のcase文の揃い: デフォルトはEnforcedStyle: case
有効 Layout/ClassStructure クラススコープ( class ~ end
)内の要素が以下の構造に準拠しているか
・moduleのミックスイン:include
,prepend
,extend
・定数定義
・クラス関係定義:belongs_to
,has_one
,has_many
・クラスマクロ:attr_accessor
,attr_writer
,attr_reader
・検証等その他マクロ:validates
,validate
・Publicクラスメソッド
・Initializer
・Publicインスタンスメソッド
・Protectedクラスマクロ:attr_accessor
,attr_writer
,attr_reader
・Protectedインスタンスメソッド
・Privateクラスマクロ:attr_accessor
,attr_writer
,attr_reader
・Privateインスタンスメソッド無効 Layout/ClosingHeredocIndentation ヒアドキュメントの識別子の揃いと、閉じ識別子が内部要素のより内側にあるか 有効 Layout/ClosingParenthesisIndentation 複数行のメソッド定義、メソッド呼出し、グループにおける閉じ括弧 )
が統一されたインデントがされているか有効 Layout/CommentIndentation コメント行が統一されたインデントがされているか 有効 Layout/ConditionPosition 条件文の判断条件が if
,while
,until
と同じ行に書かれているか有効 Layout/DefEndAlignment メソッド定義スコープ( def ~ end
)におけるend
が統一されたインデントがされているか
※ デフォルトはEnforcedStyleAlignWith: start_of_line
有効 Layout/DotPosition 複数行におけるレシーバーとメソッドの区切り .
が統一されたインデントがされているか
※ デフォルトはEnforcedStyle: leading
有効 Layout/ElseAlignment 条件文における else
がif
,while
,until
と揃っているか有効 Layout/EmptyComment 空のコメント行が統一されたインデントがされているか
※ デフォルトはAllowBorderComment: true
,AllowMarginComment: true
有効 Layout/EmptyLineAfterGuardClause ガード構文の直後に空行が存在するか 有効 Layout/EmptyLineAfterMagicComment マジックコメント最終行の直後に空行が存在するか 有効 Layout/EmptyLineBetweenDefs メソッド定義スコープ( def ~ end
)間に空行が存在しないか有効 Layout/EmptyLines 二行以上連続した空行が存在しないか 有効 Layout/EmptyLinesAroundAccessModifier アクセス修飾子(e.g. private
)前後に空行が存在するか有効 Layout/EmptyLinesAroundArguments 複数行メソッド呼出しの引数に空行が存在しないか 有効 Layout/EmptyLinesAroundBeginBody begin ~ end
スコープ内に空行が存在しないか有効 Layout/EmptyLinesAroundBlockBody ブロック内に空行が存在しないか 有効 Layout/EmptyLinesAroundClassBody クラススコープ( class ~ end
)内に空行が存在しないか
※ デフォルトはEnforcedStyle: no_empty_lines
有効 Layout/EmptyLinesAroundExceptionHandlingKeywords 例外処理スコープ( begin ~ rescue ~ else ~ ensure ~ end
)内に空行が存在しないか有効 Layout/EmptyLinesAroundMethodBody メソッド定義スコープ( def ~ end
)内に空行が存在しないか有効 Layout/EmptyLinesAroundModuleBody モジュールスコープ(module ~ end)内に空行が存在しないか
※ デフォルトは EnforcedStyle: no_empty_lines有効 Layout/EndAlignment スコープの end
が統一されたインデントがされているか
※ デフォルトはEnforcedStyleAlignWith: keyword
有効 Layout/EndOfLine WindowsOSにおける改行コード
※ デフォルトはEnforcedStyle: native
(WindowsOS: CR+LF / その他OS: LF)有効 Layout/ExtraSpacing 無意味なスペースが存在しないか
※ デフォルトはAllowForAlignment: true
,AllowBeforeTrailingComments: false
,ForceEqualSignAlignment: false
有効 Layout/FirstArgumentIndentation メソッド呼出しの第一引数にインデントがされているか
※ デフォルトはEnforcedStyle: special_for_inner_method_call_in_parentheses
有効 Layout/FirstArrayElementIndentation 複数行配列において、[ と第一要素が別々の行にある場合の第一要素にインデントがされているか
※ デフォルトはEnforcedStyle: special_inside_parentheses
有効 Layout/FirstArrayElementLineBreak 複数行配列における第一要素の前に改行がされているか 無効 Layout/FirstHashElementIndentation 複数行ハッシュにおける最初のキーにインデントがされているか
※ デフォルトはEnforcedStyle: special_inside_parentheses
有効 Layout/FirstHashElementLineBreak 複数行ハッシュにおける最初のキーの前に改行がされているか 無効 Layout/FirstMethodArgumentLineBreak メソッド呼出しの第一引数の前に改行がされているか 無効 Layout/FirstMethodParameterLineBreak メソッド定義の最初のパラメーターの前に改行がされているか 無効 Layout/FirstParameterIndentation メソッド定義の最初のパラメーターにインデントがされているか
※ デフォルトはEnforcedStyle: consistent
有効 Layout/HashAlignment 複数行ハッシュのキー、セパレーター、値にインデントがされているか
・ハッシュロケット / コンマ: デフォルトはEnforcedHashRocketStyle: key
,EnforcedColonStyle: key
・メソッド呼出しの最後の引数としての扱い: デフォルトはEnforcedLastArgumentHashStyle: always_inspect
有効 Layout/HeredocArgumentClosingParenthesis メソッド呼出しの引数としてのヒアドキュメントの閉じ識別子にインデントがされているか 無効 Layout/HeredocIndentation ヒアドキュメント内部にインデントがされているか
※ デフォルトはEnforcedStyle: squiggly
有効 Layout/IndentationConsistency インデントに一貫性があるか
※ デフォルトはEnforcedStyle: normal
有効 Layout/IndentationWidth インデントにおける半角スペースの数が正しいか
※ デフォルトは半角スペース2つ有効 Layout/InitialIndentation ファイル内の非ブロック、非コメントの最初の行にインデントがされているか 有効 Layout/LeadingCommentSpace コメントの #
識別子の直後に半角スペースが存在するか有効 Layout/LeadingEmptyLines ファイル一行目に空行が存在しないか 有効 Layout/LineLength 一行あたりの文字数は多すぎないか 有効 Layout/MultilineArrayBraceLayout 複数行の括弧における (
,]
,}
にインデントがされているか
※ デフォルトはEnforcedStyle: symmetrical
有効 Layout/MultilineArrayLineBreaks 複数行配列のそれぞれの要素が改行によって独立しているか 有効 Layout/MultilineAssignmentLayout 複数行の右辺を持つ代入式における、演算子の後に改行がされているか 無効 Layout/MultilineBlockLayout do ~ end
,{ ~ }
ブロックのブロック引数と改行
・ブロック引数: ブロックの最初の行のdo
または{
の後
・改行: ブロックの最初の行のdo
,{
またはブロック引数の後有効 Layout/MultilineHashBraceLayout 複数行ハッシュの {
,}
にインデントがされているか
※ デフォルトはEnforcedStyle: symmetrical
有効 Layout/MultilineHashKeyLineBreaks 複数行ハッシュのキー: 値がそれぞれ改行によって独立しているか 無効 Layout/MultilineMethodArgumentLineBreaks メソッド呼出しの複数行の引数がそれぞれ改行によって独立しているか 無効 Layout/MultilineMethodCallBraceLayout 複数行メソッドの呼出しの閉じ括弧 )
にインデントがされているか
※ デフォルトはEnforcedStyle: symmetrical
有効 Layout/MultilineMethodCallIndentation .メソッド名
にインデントがされているか
※ デフォルトはEnforcedStyle: aligned
有効 Layout/MultilineMethodDefinitionBraceLayout メソッド定義の複数行のパラメーターにおける閉じ括弧 )
にインデントがされているか
※ デフォルトはEnforcedStyle: symmetrical
有効 Layout/MultilineOperationIndentation 複数行の演算式にインデントがされているか
※ デフォルトはEnforcedStyle: aligned
有効 Layout/ParameterAlignment 複数行のメソッド定義、メソッド呼出しにインデントがされているか
※ デフォルトはEnforcedStyle: with_first_parameter
有効 Layout/RescueEnsureAlignment 例外処理スコープ内の rescue
,ensure
にインデントがされているか有効 Layout/SpaceAfterColon コロン( :
)の直後に半角スペースが存在するか有効 Layout/SpaceAfterComma コンマ( ,
)の直後に半角スペースが存在するか有効 Layout/SpaceAfterMethodName メソッド名の直後に半角スペースが存在するか 有効 Layout/SpaceAfterNot 否定演算子( !
)の直後に半角スペースが存在しないか有効 Layout/SpaceAfterSemicolon セミコロン( ;
)の直後に半角スペースが存在するか有効 Layout/SpaceAroundBlockParameters ブロック引数前後に半角スペースが存在するか 有効 Layout/SpaceAroundEqualsInParameterDefault メソッド定義のデフォルト引数における =
演算子の前後に半角スペースが存在するか有効 Layout/SpaceAroundKeyword do
,if
等キーワード前後に半角スペースが存在するか有効 Layout/SpaceAroundOperators 演算子前後に半角スペースが存在するか
※ デフォルトはAllowForAlignment: true
有効 Layout/SpaceBeforeBlockBraces ブロックの始まりの {
直前に半角スペースが存在するか
※ デフォルトはEnforcedStyle: space
有効 Layout/SpaceBeforeComma コンマ( ,
)の直前に半角スペースが存在しないか有効 Layout/SpaceBeforeComment コメントの直線に半角スペースが存在するか 有効 Layout/SpaceBeforeFirstArg ()
のないメソッド呼出しで、メソッド名と引数の間に連続して半角スペースが複数存在しないか有効 Layout/SpaceBeforeSemicolon セミコロン( ;
)の直前に半角スペースが存在しないか有効 Layout/SpaceInLambdaLiteral lambdaの引数の (
の直後に半角スペースが存在しないか
※ デフォルトはEnforcedStyle: require_no_space
有効 Layout/SpaceInsideArrayLiteralBrackets 配列の [
の直後と]
の直前に半角スペースが存在しないか
※ デフォルトはEnforcedStyle: no_space
有効 Layout/SpaceInsideArrayPercentLiteral %記法の ()
内に連続して半角スペースが複数存在しないか有効 Layout/SpaceInsideBlockBraces ブロックの {
の直後と}
の直前に半角スペースが存在するか
※ デフォルトはEnforcedStyle: space
有効 Layout/SpaceInsideHashLiteralBraces ハッシュの {
の直後と}
の直前に半角スペースが存在するか
※ デフォルトはEnforcedStyle: space
有効 Layout/SpaceInsideParens (
の直後と)
の直前に半角スペースが存在しないか
※ デフォルトはEnforcedStyle: no_space
有効 Layout/SpaceInsidePercentLiteralDelimiters %記法の (
の直後と)
の直前に半角スペースが存在しないか有効 Layout/SpaceInsideRangeLiteral Rangeの ..
,...
の前後に半角スペースが存在しないか有効 Layout/SpaceInsideReferenceBrackets ハッシュのキー取得における [
の直後と]
の直前に半角スペースが存在しないか
※ デフォルトはEnforcedStyle: no_space
有効 Layout/SpaceInsideStringInterpolation 式展開の {
の直後と}
の直前に半角スペースが存在しないか
※ デフォルトはEnforcedStyle: no_space
有効 Layout/Tab インデントにタブが使われていないか 有効 Layout/TrailingEmptyLines ファイルの最後に空行が存在するか 有効 Layout/TrailingWhitespace 無意味な半角スペースが存在しないか 有効 Lint
Cop名 解析対象 デフォルト Lint/AmbiguousBlockAssociation メソッド呼び出しの引数に ()
がないことによって、ブロックの結合度が不明確になっていないか有効 Lint/AmbiguousOperator メソッド呼び出しの第一引数に ()
がないことによって、演算子の意味が不明確になっていないか有効 Lint/AmbiguousRegexpLiteral メソッド呼び出しの第一引数に ()
がないことによって、正規表現のエスケープシーケンスの意味が不明確になっていないか有効 Lint/AssignmentInCondition if
,while
,until
などの条件文において、判断条件の代入式の右辺true
を代入するときに()
が使われているか
※ デフォルトはAllowSafeAssignment: true
有効 Lint/BigDecimalNew 非推奨の BigDecimal.new()
が使われていないか有効 Lint/BooleanSymbol boolean型がシンボルで使われていないか(e.g. :true
,:false
)有効 Lint/CircularArgumentReference 調査中 有効 Lint/Debugger 開発環境以外(テスト環境、本番環境)でデバッグ用のメソッド(pry, byebug)が使われていないか 有効 Lint/DeprecatedClassMethods 非推奨のクラスメソッド( File.exists?
,Dir.exists?
,iterator?
)が使われていないか有効 Lint/DisjunctiveAssignmentInConstructor initializerで、インスタンス変数の代入に `\ =` 演算子が使われていないか Lint/DuplicateCaseCondition case文において、when句の判断条件が重複していないか 有効 Lint/DuplicateHashKey ハッシュのキーが重複していないか 有効 Lint/DuplicateMethods メソッド定義が重複していないか 有効 Lint/EachWithObjectArgument Enumerable#each_with_object
において、引数にイミュータブルな値が使われていないか
※Enumerable#each_with_object
の引数は、当該メソッドが可算集合対して行うループ処理に基づいてあるデータを作るために与えられたブロックから呼び出されるオブジェクトであるので、イミュータブルな引数は意味をなさない上、これは明らかなバグである。有効 Lint/ElseLayout if
,while
,until
などの条件文において、else
句に条件判断が書かれていないか有効 Lint/EmptyEnsure 例外処理において、ensure 句に処理が書かれているか 有効 Lint/EmptyExpression 変数や条件判断に空値が使われていないか 有効 Lint/EmptyInterpolation 空の式展開が使われていないか 有効 Lint/EmptyWhen case文の when句に処理が書かれているか 有効 Lint/EndInMethod 調査中 有効 Lint/EnsureReturn 例外処理において、 ensure
句でreturn
が使われていないか
※return
を明記するとコントロールフローが変わる。理由は以下の2つ。
・raise
された例外より優先してreturn
が実行されるため
・rescue
されたが如く、キャッチした例外は通知なしに放棄される有効 Lint/ErbNewArguments 調査中 有効 Lint/FlipFlop flip-flop演算子が含まれていないか
※ Ruby 2.6.0 ~ 非推奨有効 Lint/FloatOutOfRange floatの数値が大きすぎないか 有効 Lint/FormatParameterMismatch Kernel#format
メソッド呼出しで、渡される引数の数とデータ型が一致しているか有効 Lint/HeredocMethodCallPosition ヒアドキュメントのメソッド呼出しにおいて、レシーバーが呼び出される順序づけが正しいか 有効 Lint/ImplicitStringConcatenation 同じ行の文字列結合が正しく行われているか 有効 Lint/IneffectiveAccessModifier アクセス修飾子(e.g. protected
,private
)が特異メソッドの定義に使われていないか
※ 特異メソッドの定義にはprivate_class_method
を使う有効 Lint/InheritException 継承する例外のスーパークラスが正しく指定されているか
※ デフォルトはEnforcedStyle: runtime_error
有効 Lint/InterpolationCheck シングルクオーテーション内で式展開が行われていないか 有効 Lint/LiteralAsCondition if
,while
,until
などの条件文において、&&
, || の中で判断条件や演算対象として使われているリテラルが正しく使われているか有効 Lint/LiteralInInterpolation 変数やメソッド呼出し以外で不要な式展開がされていないか 有効 Lint/Loop begin ~ end
,until
,while
等でループ処理が正しく書かれているか有効 Lint/MissingCopEnableDirective copを一部無効にする際、 # rubocop:disable ...
の後に# rubocop:enable ...
が書かれて有効な状態に戻されているか有効 Lint/MultipleComparison x < y < z
のような、3つ以上の比較演算を行っている式が存在しないか
※ Rubyでは使用不可有効 Lint/NestedMethodDefinition メソッド定義がネストされていないか 有効 Lint/NestedPercentLiteral %記法がネストされていないか 有効 Lint/NextWithoutAccumulator reduce
ブロック内である条件でループ処理をスキップする際、累算器がnext
メソッド呼出しの引数に取られているか有効 Lint/NonDeterministicRequireOrder Dir[...]
,Dir.glob(...)
で取得したファイルを格納した配列がソートされているか
※ 上記メソッドで取得したファイルの順番はOSやファイルシステムに依存するため、アルファベット昇順とは限らないため有効 Lint/NonLocalExitFromIterator 調査中 有効 Lint/NumberConversion 安全でない数値変換が行われていないか
・to_i
,to_f
,to_c
メソッドが使われていないか
・Integer()
,Float()
,Complex()
が使われているか無効 Lint/OrderedMagicComments マジックコメントが正しい順序で書かれており、 #!
がマジックコメントの先頭に書かれているか有効 Lint/ParenthesesAsGroupedExpression 引数ありのメソッド呼出しと (
の間に半角スペースが存在しないか有効 Lint/PercentStringArray 文字列配列の %w
記法において、クオーテーションやコンマが使われていないか有効 Lint/PercentSymbolArray シンボル配列の %i
記法において、コロン(:
)やコンマが使われていないか有効 Lint/RandOne rand(1)
が呼び出されていないか有効 Lint/RedundantCopDisableDirective 省略できる rubocop:disable
コメントが存在しないか有効 Lint/RedundantCopEnableDirective 省略できる rubocop:enable
コメントが存在しないか有効 Lint/RedundantRequireStatement 無意味な require
が存在しないか有効 Lint/RedundantSplatExpansion 配列展開の splat( *
)が無意味に使われていないか有効 Lint/RedundantStringCoercion 式展開で文字列変換( to_s
)がされていないか有効 Lint/RedundantWithIndex 無意味な with_index
メソッドが使われていないか有効 Lint/RedundantWithObject 無意味な with_object
メソッドが使われていないか有効 Lint/RegexpAsCondition 条件文の判断条件で match-current-line
として正規表現が使われているか有効 Lint/RequireParentheses 以下の条件に合致する場合に引数が ()
で囲まれているか
・少なくとも一つ以上の引数があるメソッド呼出しが並列されている
・並列されているメソッド呼出しの引数がいずれも()
で囲まれていない
・並列されている最後のメソッド呼出しの前に&&
, || がある有効 Lint/RescueException 例外処理における rescue
のブロックの中で、最上位の Exception クラスを指定していないか有効 Lint/RescueType 例外が発生した時に rescue
の引数がTypeError
を発生させるものではないか有効 Lint/ReturnInVoidContext return
メソッドの引数の値に関して、それが無視される文脈(initializer
やセッター)で引数に取られていないか有効 Lint/SafeNavigationChain NoMethodError
の例外が発生しないようにnilガードが適切にチェーンされているか有効 Lint/SafeNavigationConsistency &&
, || などの論理演算子でnilガードを使う場合において、両辺で同じオブジェクトを返す書き方がされているか
※ デフォルトはAllowedMethods: [present?, blank?, presence, try, try!]
有効 Lint/SafeNavigationWithEmpty 条件式の中でnilガードの後に empty?
メソッドがチェーンされていないか
・nil.empty?
=>NoMethodError
・nil&.empty?
=>nil
有効 Lint/ScriptPermission #!
から始まるマジックコメントが一行目に書かれているファイルが実行権限を有しているか有効 Lint/SendWithMixinArgument モジュールのミックスインにおいて、プライベートメソッド呼出しの send
,public_send
,__send__
が使われていないか
※ Ruby2.0までinclude
,prepend
はプライベートメソッドであったが、Ruby2.1以降はパブリックメソッドである。extend
は元々パブリックメソッドである。有効 Lint/ShadowedArgument 引数のシャドーイングがなされていないか
※ デフォルトはIgnoreImplicitReferences: false
有効 Lint/ShadowedException 例外処理において、抽象度の高い例外クラスが具体性の高い例外クラスに先んじて例外を補足していないか 有効 Lint/ShadowingOuterLocalVariable ブロック引数がスコープ外のローカル変数と同名であることによってシャドーイングがなされていないか 有効 Lint/SuppressedException rescue
ブロック内に処理が書かれているか
※ デフォルトはAllowComments: false
有効 Lint/Syntax 厳密にはcopではなく、診断情報やエラーをRubocopの規約違反に組み入れ直すメソッド群を提供する 有効 Lint/ToJSON to_json
メソッドがオプション引数を取っているか有効 Lint/UnderscorePrefixedVariableName アンダースコア付ブロック変数が内部処理に使われていないか
※ デフォルトはAllowKeywordBlockArguments: false
有効 Lint/UnifiedInteger Fixnum
,Bignum
等の非推奨の定数が使われていないか有効 Lint/UnreachableCode return
でメソッドを抜けた後に処理されないロジックが存在しないか有効 Lint/UnusedBlockArgument 利用されていないブロック引数が存在しないか
※ デフォルトはIgnoreEmptyBlocks: true
,AllowUnusedKeywordArguments: false
有効 Lint/UnusedMethodArgument 利用されていないメソッド引数が存在しないか
※ デフォルトはAllowUnusedKeywordArguments: false
,IgnoreEmptyMethods: true
,IgnoreNotImplementedMethods: true
有効 Lint/UriEscapeUnescape 非推奨のメソッドがユースケースに応じて推奨のメソッドに代替出来るか
・URI.escape
=>CGI.escape
,URI.encode_www_form
,URI.encode_www_form_component
・URI.unescape
=>CGI.unescape
,URI.decode_www_form
,URI.decode_www_form_component
有効 Lint/UriRegexp ・ URI.regexp
が使われていないか
・URI::DEFAULT_PARSER.make_regexp
が使われているか有効 Lint/UselessAccessModifier 無意味に public
,private
などのアクセサが使われていないか有効 Lint/UselessAssignment 使われていないローカル変数が存在しないか 有効 Lint/UselessComparison 同一のオブジェクトが比較演算子の両辺で使われていないか 有効 Lint/UselessElseWithoutRescue 例外処理において、 begin ~ end
の中でrescue
句なしでelse
が使われていないか有効 Lint/UselessSetterCall セッターメソッドの中で、ローカル変数の値が最終行で返されているか
※ ローカルスコープ外からアクセス出来る値をローカル変数が参照するエッジケースが存在する。copには検出されず、誤判定が生じる可能性がある。有効 Lint/Void 調査中
※ デフォルトはCheckForMethodsWithNoSideEffects: false
有効 Metrics
Cop名 解析対象 デフォルト Metrics/AbcSize 算出されたABC(代入式、メソッド呼出し、条件式)の大きさが最大値を超えていないか
※ デフォルトはMax: 15
有効 Metrics/BlockLength ブロックの行数が最大値を超えていないか
※ デフォルトは以下の通り
・CountComments: false
・Max: 25
・ExcludedMethods: [refine]
・Exclude: [**/*.gemspec]
有効 Metrics/BlockNesting 条件式や繰返し処理等のブロック内の入れ子の数が最大値を超えていないか
※ デフォルトは以下の通り
・CountBlocks: false
・Max: 3
有効 Metrics/ClassLength クラスあたりの行数が最大値を超えていないか
※ デフォルトは以下の通り
・CountComments: false
・Max: 100
有効 Metrics/CyclomaticComplexity 循環的複雑度の値が最大値を超えていないか
※ デフォルトはMax: 6
有効 Metrics/MethodLength メソッド定義の行数が最大値を超えていないか
※ デフォルトは以下の通り
・CountComments: false
・Max: 10
・ExcludedMethods: []
有効 Metrics/ModuleLength モジュールあたりの行数が最大値を超えていないか
※ デフォルトは以下の通り
・CountComments: false
・Max: 100
有効 Metrics/ParameterLists メソッド定義のパラメーターの数が最大値を超えていないか
※ デフォルトは以下の通り
・Max: 5
・CountKeywordArgs: true
有効 Metrics/PerceivedComplexity コードの複雑度が最大値を超えていないか
※ デフォルトはMax: 7
有効 Migration
Cop名 解析対象 デフォルト Migration/DepartmentName rubocop:disable内のコメントでcop名が区分名を伴っているか 有効 Naming
Cop名 解析対象 デフォルト Naming/AccessorMethodName アクセサのメソッド名が適切に命名されているか 有効 Naming/AsciiIdentifiers メソッド名や変数名などの識別名に非ASCII文字が使われていないか 有効 Naming/BinaryOperatorParameterName 二項演算子メソッド定義のパラメーターの引数名が other
になっているか有効 Naming/BlockParameterName ブロックパラメーターは意味のある命名がされているか
※ デフォルトは以下の通り
・MinNameLength: 1
・AllowNamesEndingInNumbers: true
・AllowedNames: []
・ForbiddenNames: []
有効 Naming/ClassAndModuleCamelCase クラス名とモジュール名がキャメルケースで統一されているか 有効 Naming/ConstantName 定数はスネークケースかつ大文字のみで構成されているか 有効 Naming/FileName ファイル名はスネークケースで統一されているか
※ デフォルトは以下の通り
・Exclude: []
・ExpectMatchingDefinition: false
・Regex: <none>
・IgnoreExecutableScripts: true
・AllowedAcronyms: [CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS]
有効 Naming/HeredocDelimiterCase ヒアドキュメントの識別子は大文字のみで構成されているか
※ デフォルトはEnforcedStyle: uppercase
有効 Naming/HeredocDelimiterNaming ヒアドキュメントの識別子は意味のある命名がされているか
※ デフォルトはForbiddenDelimiters:
(?-mix:(^。また、デフォルトで
ENDと
EO*` の使用は不許可。有効 Naming/MemoizedInstanceVariableName メモ化インスタンスメソッド名とスコープ内のインスタンス変数名が一致しているか
※ デフォルトはEnforcedStyleForLeadingUnderscores: disallowed
有効 Naming/MethodName メソッド名が決められた形式(スネークケース/キャメルケース)で命名されているか
※ デフォルトはEnforcedStyle: snake_case
有効 Naming/MethodParameterName メソッド定義のパラメーターは意味のある命名がされているか
※ デフォルトは以下の通り
・MinNameLength: 3
・AllowNamesEndingInNumbers: true
・AllowedNames: [io, id, to, by, on, in, at, ip, db, os, pp]
・ForbiddenNames: []
有効 Naming/PredicateName true/false を返す叙述メソッドが適切な命名をされているか
※ デフォルトは以下の通り
・NamePrefix: [is_, has_, have_]
・ForbiddenPrefixes: [is_, has_, have_]
・AllowedMethods: [is_a?]
・MethodDefinitionMacros: [define_method, define_singleton_method]
・Exclude: [spec/**/*]
有効 Naming/RescuedExceptionsVariableName 例外処理において、例外を代入する変数が適切な命名がされているか
※ デフォルトはPreferredName: e
有効 Naming/VariableName 変数名が決められた形式(スネークケース/キャメルケース)で命名されているか
※ デフォルトはEnforcedStyle: snake_case
有効 Naming/VariableNumber アラビア数字を含む変数名における書式が正しいか
※ デフォルトはEnforcedStyle: normalcase
有効 Security
Cop名 検査対象 デフォルト Security/Eval 非推奨の Kernel#eval
やBinding#eval
が使われていないか有効 Security/JSONLoad 潜在的なセキュリティ問題を孕む JSON
クラスメソッド が使われていないか有効 Security/MarshalLoad 信頼していないソースコードを読み込んだ際に遠隔でのコード実行に繋がる潜在的なセキュリティ問題を孕む Marshal
クラスメソッド が使われていないか有効 Security/Open Kernel#open
が危険な使われ方をされていないか
※Kernel#open
はファイルアクセスだけでなく、パイプシンボル(|)を前に置くことでプロセス呼出しも可能にする。よって、引数に取る変数によってな重大なセキュリティリスクを孕む
※File.open
,IO.popen
,URI#open
を使う方が安全である有効 Security/YAMLLoad 信頼していないソースコードを読み込んだ際に遠隔でのコード実行に繋がる潜在的なセキュリティ問題を孕む YAML
クラスメソッド が使われていないか有効 Style
作業中
- 投稿日:2020-03-24T18:24:44+09:00
gem install mysql2エラー(Error installing mysql2)
mysql2をインストールした際エラーが出たため、情報共有します。
インストールコマンド
gem install mysql2エラー内容
Building native extensions. This could take a while... ERROR: Error installing mysql2: ERROR: Failed to build gem native extension. current directory: /Users/aki/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/mysql2-0.5.3/ext/mysql2 /Users/aki/.rbenv/versions/2.7.0/bin/ruby -I /Users/aki/.rbenv/versions/2.7.0/lib/ruby/2.7.0 -r ./siteconf20200324-90394-m50btx.rb extconf.rb checking for rb_absint_size()... yes checking for rb_absint_singlebit_p()... yes checking for rb_wait_for_single_fd()... yes ----- Using mysql_config at /usr/local/bin/mysql_config ----- checking for mysql.h... yes checking for errmsg.h... yes checking for SSL_MODE_DISABLED in mysql.h... yes checking for SSL_MODE_PREFERRED in mysql.h... yes checking for SSL_MODE_REQUIRED in mysql.h... yes checking for SSL_MODE_VERIFY_CA in mysql.h... yes checking for SSL_MODE_VERIFY_IDENTITY in mysql.h... yes checking for MYSQL.net.vio in mysql.h... yes checking for MYSQL.net.pvio in mysql.h... no checking for MYSQL_ENABLE_CLEARTEXT_PLUGIN in mysql.h... yes checking for SERVER_QUERY_NO_GOOD_INDEX_USED in mysql.h... yes checking for SERVER_QUERY_NO_INDEX_USED in mysql.h... yes checking for SERVER_QUERY_WAS_SLOW in mysql.h... yes checking for MYSQL_OPTION_MULTI_STATEMENTS_ON in mysql.h... yes checking for MYSQL_OPTION_MULTI_STATEMENTS_OFF in mysql.h... yes checking for my_bool in mysql.h... no ----- Don't know how to set rpath on your system, if MySQL libraries are not in path mysql2 may not load ----- ----- Setting libpath to /usr/local/Cellar/mysql/8.0.19/lib ----- creating Makefile current directory: /Users/aki/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/mysql2-0.5.3/ext/mysql2 make "DESTDIR=" clean current directory: /Users/aki/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/mysql2-0.5.3/ext/mysql2 make "DESTDIR=" compiling client.c client.c:787:14: warning: incompatible pointer types passing 'VALUE (void *)' (aka 'unsigned long (void *)') to parameter of type 'VALUE (*)(VALUE)' (aka 'unsigned long (*)(unsigned long)') [-Wincompatible-pointer-types] rb_rescue2(do_send_query, (VALUE)&args, disconnect_and_raise, self, rb_eException, (VALUE)0); ^~~~~~~~~~~~~ /Users/aki/.rbenv/versions/2.7.0/include/ruby-2.7.0/ruby/ruby.h:1988:25: note: passing argument to parameter here VALUE rb_rescue2(VALUE(*)(VALUE),VALUE,VALUE(*)(VALUE,VALUE),VALUE,...); ^ client.c:795:16: warning: incompatible pointer types passing 'VALUE (void *)' (aka 'unsigned long (void *)') to parameter of type 'VALUE (*)(VALUE)' (aka 'unsigned long (*)(unsigned long)') [-Wincompatible-pointer-types] rb_rescue2(do_query, (VALUE)&async_args, disconnect_and_raise, self, rb_eException, (VALUE)0); ^~~~~~~~ /Users/aki/.rbenv/versions/2.7.0/include/ruby-2.7.0/ruby/ruby.h:1988:25: note: passing argument to parameter here VALUE rb_rescue2(VALUE(*)(VALUE),VALUE,VALUE(*)(VALUE,VALUE),VALUE,...); ^ 2 warnings generated. compiling infile.c compiling mysql2_ext.c compiling result.c compiling statement.c linking shared-object mysql2/mysql2.bundle ld: library not found for -lssl clang: error: linker command failed with exit code 1 (use -v to see invocation) make: *** [mysql2.bundle] Error 1 make failed, exit code 2 Gem files will remain installed in /Users/aki/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/mysql2-0.5.3 for inspection. Results logged to /Users/aki/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/extensions/x86_64-darwin-19/2.7.0/mysql2-0.5.3/gem_make.out原因
どうやらsslが見つからないとのこと。
library not found for -lsslとりあえずインストールはされていたため、アップグレード。
brew upgrade opensslすでにインストールされていた。
Updating Homebrew... Warning: openssl 1.1.1d already installedgem installにオプションを付けインストールしてみることにしたら実行できた!
gem install mysql2 -v '0.5.2' -- --with-ldflags=-L/usr/local/opt/openssl/lib --with-cppflags=-I/usr/local/opt/openssl/includeまとめ
SSLのエラーがgithubでも起こったため、一度集中的に調べて記事にしておこうと思います。
参考
もはやこのページの通り実行しただけです笑
githubページ
- 投稿日:2020-03-24T18:23:56+09:00
Rails 投稿機能
Railsで投稿機能の作成です
部分的な備忘録ですので分かりづらいと思いますrailsアプリケーション作成
$ rails _5.2.3_ new sample_boad -d mysqlGem追加
bundle installとbundle updateを実行
gem 'pry-rails' gem 'compass-rails', '3.1.0' gem 'sprockets', '3.7.2'データベース作成
$ rake db:createモデル作成
$ rails g model tweetテーブル作成
マイグレーションファイルはテーブルの設計図のようなものです。
t.の後に続いている記述は、どんなデータが入るのかを示す型です。srting 文字(少なめ)
text 文字(多め)
integer 数字
などがあります。timespasはcreated_at(作成時間) や updated_at(更新時間) を定義する時に使用します。
:nameや:textはカラム名です。この場合、nameカラム(ユーザー名)をstring型(ユーザー名は文字少なめ)、textカラム(投稿された文章)をtext型(文章だから文字多め)、imageカラム(画像)をstring型としています。
2020XXXXXXXXXXXX_create_tweets.rbclass CreateTweets < ActiveRecord::Migration[5.2] def change create_table :tweets do |t| t.string :name t.text :text t.string :image t.timestamps null: true end end endrake db:migrateでマイグレーションファイルを実行
ルーティング
投稿一覧を表示するindexアクション 投稿画面を作成するためにnewアクション 投稿された内容を保存するためにcreateアクションを使います。
route.rbRails.application.routes.draw do resources :tweets only: [:index,:new,:create] endコントローラー
private以下はストロングパラメーターです。
ビューでフォームに入力された情報は、コントローラにキーと一緒にパラメーターとして送られます。
ストロングパラメーターは、指定したキーを持つパラメーターのみを受け取るようにするものです。
不正な情報を送信しようとしたときに、ストロングパラメーターを設定しておくと、不正な情報を受け取らずにすみます。$ rails g controller tweetstweets.controller.rbclass TweetsController < ApplicationController def index @tweets = Tweet.order("created_at DESC").page(params[:page]).per(5) end def new end def create Tweet.create(tweet_params) end private def tweet_params params.permit(:name,:image,:text) end endTweet.order("created_at DESC")は、投稿された内容を降順に並び替えて表示させています。
つまり、新しい投稿ほど上に表示されます。
page(params[:page]).per(5)はページネーションです。説明は省略します。Gemはkaminariを使用しています。ビュー作成
投稿一覧
index.html<html> <head> <title> Sample boad</title> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> <%= csrf_meta_tags %> </head> <body> <header class="header"> <h1>掲示板</h1> <%= link_to "投稿する", "/tweets/new" ,class: "send_btn"%> </header> <div class="comment"> <% @tweets.each do |tweet|%> <div class="comment-list"> <div class="title"> <%= tweet.title%> </div> <div class="comments"> <%= tweet.text %> </div> </div> <% end %> </div> <%= paginate(@tweets)%> </div> </body> </html>投稿完了画面
create.html<div class="contents row"> <div class="succes"> <h3> 投稿が完了しました </h3> <a class="btn" href="/tweets">投稿一覧へ戻る</a> </div> </div>投稿画面
7〜9行目に:name :image :textの記述があります。
これが、コントローラーに送信されるキーを表しています。ユーザーに入力された情報はキーと一緒にパラメーターとして送られます。
コントローラーでストロングパラメーターを使っているので、:userなど指定されていないキーは受け取られません。new.html<div class="contents row"> <div class="container"> <%= form_with(model: @tweet, local: true) do |form| %> <h3> 投稿する </h3> <%= form.text_field :name, placeholder: "Nickname" %> <%= form.text_field :image, placeholder: "Image Url" %> <%= form.text_area :text, placeholder: "text", rows: "10" %> <%= form.submit "SEND" %> <% end %> </div> </div>
- 投稿日:2020-03-24T18:16:04+09:00
RailsでRSpecを追加しようとした時に発生したエラー「Could not find diff-lcs-1.3 in any of the sources Run `bundle install` to install missing gems.」の対処法
Gemfilegroup :development, :test do gem 'rspec-rails' endGemfileにRSpecを追加して、以下のコマンドを実行したらエラーが発生した。
エラーメッセージ
$ bundle install ・・・ $ rails generate rspec:install Could not find diff-lcs-1.3 in any of the sources Run `bundle install` to install missing gems.解決方法
$ ps axu | grep spring maiamea 53324 0.1 0.1 4336604 9524 ?? Ss 4:41PM 0:00.92 spring app | api-project | started 5 mins ago | development mode maiamea 36768 0.0 0.0 4366272 748 s002 S+ 月04PM 0:00.58 spring server | api-project | started 24 hours ago maiamea 53389 0.0 0.0 4286728 716 s003 S+ 4:46PM 0:00.00 grep spring $ spring stop Spring stopped. $ rails generate rspec:install Running via Spring preloader in process 53437 create .rspec create spec create spec/spec_helper.rb create spec/rails_helper.rb原因
rails server を高速で起動させるSpringが悪さをしていた。
Springが起動していたことによりGemfileの変更が反映されなかった。Gemfileに変更を加えた場合は、まず rails server を止め、
次にSpringをstopさせる。
その後 rails server を起動させる必要があった。Springとは?
Railsアプリケーションのプリローダーのgem (Rubyのライブラリ)
Railsアプリケーションをバックグラウンドで走らせたままにしておくことにより、2回目以降のコマンド実行時の待ち時間が減り開発効率が上がる。プリローダー:preloader (pre + load = 前もってロードしておく)
参考
- 投稿日:2020-03-24T18:13:39+09:00
線分の交差判定と交点特定
はじめに
二つの折れ線グラフがある時、そのグラフの交点を探したいことがあります。こんな感じです。
典型的には異なるサイズのBinder比の交点を見つける時とかに必要です。で、そのためには「二つの線分の交差判定」をするコードと「交差する二つの線分の交点」を求めるコードが必要になるのですが、なんかこれ、必要になる度に毎回書いている気がするので、以下では交差判定と交点を求めるアルゴリズムを真面目に導出して、それをコードに落としてCC0で公開しました。
https://github.com/kaityo256/find_intersection
なお、線分直上に点があったり、二つの線分が平行だったりといった条件は考慮していないので注意。また、やや冗長に書いているので、高速化が必要な場合は適宜修正してください。
二つの線分の交差判定
4つの点、$P_1$, $P_2$, $P_3$, $P_4$があるとしましょう。点$P_i$の座標を$(x_i, y_i)$とします。この時、線分$P_1 P_2$と$P_3 P_4$は交点を持つか、交点を持つならその交点の座標を知りたいとします。
まず、線分$P_1 P_2$を通る直線の式を求めましょう。この直線の傾き$a$は
a = \frac{y_2 - y_1}{x_2 - x_1}です。これが点$(x_1, y_1)$を通るので、
y - y_1 = a (x - x_1)が求める直線の式です。分母を払って整理すると
(x_2 - x_1)(y - y_1) - (y_2 - y_1) (x-x_1) = 0となります。さて、この式の左辺を$f_{12}(x, y)$としましょう。点$P_3$と点$P_4$が、この直線を挟んで反対側にあるためには、$f_{12}(x_3, y_3)$と$f_{12}(x_4, y_4)$が異符号でなければなりません。
つまり、
f_{12}(x_3, y_3) f_{12}(x_4, y_4) < 0が成り立つ必要があります。
同様に、線分$P_3 P_4$が作る直線の式を$f_{34}(x,y) = 0$とすると、
f_{34}(x_1, y_1) f_{34}(x_2, y_2) < 0が成り立つ必要があります。もし線分が交差していない場合は、同符号になる組み合わせが出てきます。例えば以下は、線分$P_3 P_4$が作る直線は線分$P_1 P_2$と交点を持つのに対し、線分$P_1 P_2$が作る直線は線分$P_3 P_4$とは交点を持たない例です。点$P_3$と$P_4$が直線の同じ側にあるため、$f_{12}(x_3, y_3)$と$f_{12}(x_4, y_4)$が同符号になります。
さて、上記の判定アルゴリズムをそのままコードに落としましょう。まずは点を表す
Struct
を作るんですかね。Point = Struct.new(:x, :y)線分$P_1 P_2$が作る直線を$f_{12}(x, y) = 0$として、そこに点$P_3$を代入した時の値を返す関数を
f(p1, p2, p3)
として定義しましょう。def f(p1, p2, p3) (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) endこれを使うと、線分$P_1 P_2$と$P_3 P_4$が交差しているかの判定は以下のように書けます。
def intersect?(p1, p2, p3, p4) t1 = f(p1, p2, p3) t2 = f(p1, p2, p4) t3 = f(p3, p4, p1) t4 = f(p3, p4, p2) t1 * t2 < 0.0 and t3 * t4 < 0.0 end二つの線分の交点
二つの線分$P_1 P_2$と$P_3 P_4$が交差していることがわかったとします。次に、交点を求めましょう。
線分$P_1 P_2$上の点$X$が、線分を$t$対$1-t$に分割したとします$(0 < t < 1)$。すると、点$X$の座標は
t \begin{pmatrix} x_1 \\ y_1 \end{pmatrix} + (1-t) \begin{pmatrix} x_2 \\ y_2 \end{pmatrix}と書けます。また、$X$は線分$P_3 P_4$上の点でもあるので、パラメータ$s (0<s<1)$を用いて
s \begin{pmatrix} x_3 \\ y_3 \end{pmatrix} + (1-s) \begin{pmatrix} x_4 \\ y_4 \end{pmatrix}とも書けます。
したがって、
t \begin{pmatrix} x_1 \\ y_1 \end{pmatrix} + (1-t) \begin{pmatrix} x_2 \\ y_2 \end{pmatrix} = s \begin{pmatrix} x_3 \\ y_3 \end{pmatrix} + (1-s) \begin{pmatrix} x_4 \\ y_4 \end{pmatrix}です。これを$t, s$について整理すると、
\begin{pmatrix} x_1-x_2 \\ y_1-y_2 \end{pmatrix} t - \begin{pmatrix} x_3-x_4 \\ y_3-y_4 \end{pmatrix} s = \begin{pmatrix} x_4-x_2 \\ y_4-y_2 \end{pmatrix}となります。これを、
A \begin{pmatrix} t \\ s \end{pmatrix} = bという連立一次方程式の形に書き直すと、
\begin{pmatrix} x_1-x_2 & x_4 - x_3 \\ y_1-y_2 & y_4 - y_3 \end{pmatrix} \begin{pmatrix} t \\ s \end{pmatrix} = \begin{pmatrix} x_4-x_2 \\ y_4-y_2 \end{pmatrix}です。$t, s$について解く(両辺に$A^{-1}$をかける)と、
\begin{pmatrix} t \\ s \end{pmatrix} = \frac{1}{|A|} \begin{pmatrix} y_4-y_3 & x_3 - x_4 \\ y_2-y_1 & x_4 - x_3 \end{pmatrix} \begin{pmatrix} x_4-x_2 \\ y_4-y_2 \end{pmatrix}となります。ただし、$|A|$は$A$の行列式で、
|A| = (x_1-x_2)(y_4-y_3) - (x_4-x_3)(y_1-y_2)です。以上から、
t = \frac{(y_4-y_3)(x_4-x_2) + (x_3-x_4) (y_4-y_2)}{|A|}です。点$X$の座標は
\begin{pmatrix} t x_1 + (1-t) x_2 \\ t y_1 + (1-t) y_2 \\ \end{pmatrix}と求まりました。
上記をそのままコードに落としましょう。
def intersection(p1, p2, p3, p4) det = (p1.x - p2.x) * (p4.y - p3.y) - (p4.x - p3.x) * (p1.y - p2.y) t = ((p4.y - p3.y) * (p4.x - p2.x) + (p3.x - p4.x) * (p4.y - p2.y)) / det x = t * p1.x + (1.0 - t) * p2.x y = t * p1.y + (1.0 - t) * p2.y return x, y endまぁ、そのままですね。
折れ線グラフの交点
なんかデータがこんな感じで与えられているとしましょう。
-0.00929403432032172 10.06733812455202 1.059235593057012 11.146562883395475 2.064831028312785 14.24963231445969 2.968107972293056 18.723725728939 4.0981278592926005 26.724011540146748 5.099077600475496 36.01584720448775 5.960053040855195 45.56438755538092 6.9441326742759495 58.20967188095562 8.050356615496234 74.89595746961379 9.02282603720683 91.33504895073239こんなファイルを二つ読み込んで、交点を返す関数
find_intersection
はこんな感じに書けるでしょう。def read_file(filename) File.read(filename).split(/\n/).map do |n| x, y = n.split(/\s+/) Point.new(x.to_f, y.to_f) end end def find_intersection(file1, file2) puts "reading #{file1}" data1 = read_file(file1) puts "reading #{file2}" data2 = read_file(file2) (data1.size - 1).times do |i| p1 = data1[i] p2 = data1[i + 1] (data2.size - 1).times do |j| p3 = data2[j] p4 = data2[j + 1] if intersect?(p1, p2, p3, p4) return intersection(p1, p2, p3, p4) end end end end要するにすべての線分の組み合わせについて交差判定をして、最初に見つけた組の交点を返す関数です。$O(N^2)$になってますが、点の数が多くなければ問題ないと思います。また、複数の交点を持つ場合は想定していません。
まとめ
線分の交差判定と交点を求めるアルゴリズムを導出して、コードに落としました。ネットにも似たようなコードが落ちているんですが、ライセンスが不明瞭だったりして、自分の公開コードに埋め込みづらかったりするんですよね。
上記のコードはCC0で公開しているので、「線分 交点」「線分 交差」等のキーワードでググってこの記事にたどり着いた人は好きに使ってください。コードはCC0なので好きに修正してくださってかまいませんが、それを公開する時には適切なライセンスを付与してもらえるとうれしいなぁ。
- 投稿日:2020-03-24T16:11:23+09:00
rails 新規アプリ作成
はじめに
今まで学習した知識を使って、オリジナルアプリを作るカリキュラムの段階まできました。
自身のアウトプット、皆さんへの共有の目的で、これから個人アプリが完成するまでの流れを投稿していきます。Railsで新規アプリを作成する
ターミナルにて
$ rails _5.2.3_ new 〇〇 -d mysqlを、実行する。
railsのあとの『5.2.3』はrailsのバーション指定。
〇〇は作成したいアプリ名。
-dはデータベースの意味。
私の場合、データベースはmysqlを使用したかったので、-dで指定しました。作成するアプリのディレクトリへ入り、ターミナルにて
rails db:createを実行してデータベースを作る。
ここでひとまずアプリの大枠は完成です。
試しにターミナルにて
rails sでサーバーを立ち上げ、localhost:3000へアクセスすると
こちらの画像が表示され、正常にアプリが作られていることがわかります。
- 投稿日:2020-03-24T15:52:46+09:00
deviseでログイン機能を実装したものの、データベースへ書き込めない問題
起きた問題
deviseにてユーザーのログイン機能やユーザー編集機能などを実装したが、ユーザー編集機能のページedit.html.erbにて入力した内容がうまくデータベースに反映されない。
具体的にいうとnameのカラムだけなぜかdbに反映されなかったので、調べてみた。
schema.rbを見てもちゃんとカラムが追加されている。。。
解決した方法とソース
まず、解決した方法は簡単で、application_controller.rbに以下のコードを追加するだけ。
application_controller.rbclass ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters # 「登録時(sign_up)」に許可するパラメータを追加 devise_parameter_sanitizer.permit(:sign_up, keys: [:name]) # 「更新時(account_update)」に許可するパラメータを追加 devise_parameter_sanitizer.permit(:account_update, keys: [:name]) end enddeviseではどうやらデフォルトでemail、password以外は許可しない設定になっているらしく、上記のようなコードを追加することで許可してあげないといけないようだった。
以下のURLはdeviseのGithub READMEから。
- 投稿日:2020-03-24T11:49:54+09:00
S3バケットのオブジェクト名を1000個以上取得する
公式リファレンスに記載があるように、list_objects_v2は1000個以上のS3オブジェクトを取得する際には面倒なnext_tokenの処理が必要になります。
そこで、1000個以上のS3オブジェクトを扱う際には、Aws::S3::Bucket#objectsを使うと、このようにシンプルに書けます。
list-objects.rbrequire 'aws-sdk' bucket = Aws::S3::Bucket.new('<バケット名>') obj_array = bucket.objects.map(&:key) obj_array.size # => オブジェクト数 obj_array.first # => プレフィックス/オブジェクト名
- 投稿日:2020-03-24T11:49:54+09:00
S3バケットのオブジェクトを1000個以上取得する
公式リファレンスに記載があるように、list_objects_v2は1000個以上のS3オブジェクトを取得する際には面倒なnext_tokenの処理が必要になります。
そこで、1000個以上のS3オブジェクトを扱う際には、Aws::S3::Bucket#objectsを使うと、このようにシンプルに書けます。
list-objects.rbrequire 'aws-sdk' bucket = Aws::S3::Bucket.new('<バケット名>') obj_array = bucket.objects.map(&:key) obj_array.size # => オブジェクト数 obj_array.first # => プレフィックス/オブジェクト名
- 投稿日:2020-03-24T10:09:32+09:00
既存RailsアプリにCircleCIを導入した手順
自作のRailsアプリにCircleCIを使ってCI/CDを導入しました
今回は、CircleCIとGitHubとの連携までを行います。自動化したいこと
- テスト
- コード解析
- デプロイ
ただ、今回は導入しか行いませんので、正直以上の3つは関係ありません。
CircleCIを採用した理由
- 無料プランがついている
- メジャーどころっぽい
- 日本語の資料が多い(死活問題)
1. アカウント取得・連携
やることは以下の3つ。
- CicleCIに登録
- GitHubと連携(登録と同時にできる)
- CircleCIを導入するアプリ(リポジトリ)を選ぶ
CircleCI公式(英語)でSignUpします。
基本的には画面の指示に従っていたら間違いないはずです。英語じゃねえか... と心が折れかかりますが、私でもなんとかなったので多分大丈夫です。
少し時間が経っていて画面デザインが少し違いますが、次のブログは参考になります。
【CircleCI】CircleCI 2.0からはじめる個人での簡単なCI導入方法 - githubとの連携までCircleCIを導入するアプリを選んだら、
ブランチを作って設定ファイルを自動で作ってあげるよ!
と言ってくれますが、私は勉強も兼ねて自分で作りました。2. 設定ファイルを作成する
CircleCIに登録後、表示されたconfig.ymlのサンプルをコピペします。
その前に、コピペ先を導入アプリのルートディレクトリにフォルダと設定ファイルを作成します。
$ cd ~/Rails_App $ midir .circleci $ touch .circleci/config.yml作成したconfig.ymlを開いて(vimでもエディタでも)、設定サンプルをコピペします。
私のサンプルはこうなってました。
config.ymlversion: 2.1 orbs: ruby: circleci/ruby@0.1.2 jobs: build: docker: - image: circleci/ruby:2.6.3-stretch-node executor: ruby/default steps: - checkout - run: name: Which bundler? command: bundle -v - ruby/bundle-install試しにpushしてみます。
$ git add -A $ git commit -m "CircleCIの設定ファイルを追加" $ git push origin master or $ git push origin branch-name開発ブランチなどで作業していた場合は、GitHubでプルリクエストします。
CircleCIのダッシュボードを確認し、pushに反応してアイコンがグルグルしていると思うので、待ちます。
少し待つと(私の環境で3分)SUCCESSとなるので、
こうなったら、CircleCIとGitHubとの連携は完了です。導入完了
これからは自動化したいことによって、.circleci/config.ymlを書き換えていきます。
Railsの場合は、公式が日本語ガイドを用意してくれています。
言語ガイドCI/CDの概略やCircleCIの設定ファイルを優しく解説してくれている
ネット記事もありました。
「エンジニアのためのCI/CD再入門」連載一覧ご参考までに。
最後までお読みいただきありがとうございました!
- 投稿日:2020-03-24T07:25:55+09:00
Kinx 実現技術 - VM(Virtual Machine)
Virtual Machine
はじめに
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。作ったものの紹介だけではなく実現のために使った技術を紹介していくのも貢献。その道の人には当たり前でも、そうでない人にも興味をもって貰えるかもしれない。
前回のテーマは Switch-Case、今回のテーマは VM (Virtual Machine)。
- 参考
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
- 個別記事へのリンクは全てここに集約してあります。
- リポジトリ ... https://github.com/Kray-G/kinx
- Pull Request 等お待ちしております。
Virtual Machine
仮想機械。Kinx の場合、構文解析した AST から IR (Intermediate Representation) を構築、それを直接実行する。命令数は現時点で 190。include/ir.h に一覧がある。 一応、一通り第二オペランドの型がわかるものに関しては型ごとに命令を用意することで実行時コストを下げるようにはしてある。
出力例
今回は出力例から。以下のフィボナッチ数列のベンチマーク;
function fib(n) { if (n < 3) return n; return fib(n-2) + fib(n-1); } System.println("fib(34) = ", fib(34));これをコンパイルしてみると、以下のようになる。
_startup: .L1 0: jmp .L2(2) 1: halt _main1: .L2 2: enter 37, vars(35), args(1) 3: pushf __anonymous_func58 => .L3(10) 4: call 0 5: pop 6: pushf fib => .L460(d0a) 7: storevx $(0,33) 8: pushi 34 9: callvl0 $0(33), 1 a: pushs "fib(34) = " b: pushvl0 $0(1) c: calls "println", 2 d: pop e: ret null f: halt fib: .L460 d0a: enter 23, vars(1), args(1) .L461 d0b: lt_v0i $0(0), 3 d0c: jz .L463(d0e) .L462 d0d: retvl0 $0(0) .L463 d0e: pushvl0 $0(0) d0f: subi 2 d10: callvl1 $1(33), 1 d11: pushvl0 $0(0) d12: subi 1 d13: callvl1 $1(33), 1 d14: add d15: ret d16: haltアドレスが飛んでいるのは、標準ライブラリの読み込みなど、スタートアップルーチンの表示を省略するようにしているからです(
__anonymous_func58
のあたり)。ダイレクト・スレッデッド・コード
gcc の場合、ラベルに対するジャンプ命令を生成できるので俗にいう ダイレクト・スレッデッド・コード が実現できる。残念ながら Visual Studio では実現できない。
まず、最初にラベルに対するアドレス用のテーブルを作成する。この時、命令コードの番号と配列位置を合わせておくことでアドレスを一発で引けるようにしておく。ちなみに、C のラベルは関数をまたげないので、関数内で閉じてないといけない。
static void *jumptable[] = { &&LBL_KX_HALT, &&LBL_KX_NOP, &&LBL_KX_DUP, &&LBL_KX_IMPORT, &&LBL_KX_ENTER, &&LBL_KX_CALL, &&LBL_KX_CALLV, &&LBL_KX_CALLVL0, ... };次に、命令を一通りスキャンして、命令に対するアドレスを設定する。
for (int i = 0; i < code_len; ++i) { kx_code_t *c = fixcode[i]; c->gotolabel = jumptable[c->op]; ... }準備が整っていれば、命令実行の最後で次の命令に移動して
goto
する。LBL_KX_ENTER: ... cur = cur->next; goto *(cur->gotolabel);この辺が、include/kxexec.h にマクロとして定義してある。マクロは gcc と Visual Studio での共通化用です。
ダイレクト・スレッディングに関しては、結構古い記事だが YARV Maniacs 【第 3 回】 命令ディスパッチの高速化 がやっぱり分かりやすいと思う。
スタック構造
スタックは演算で使用されるが、関数呼び出し時にフレームを作成する。フレームにはローカル変数を格納するバッファが用意されており、フレームが GC されない限り参照できる。関数呼び出し時のスタック構造は以下の通り。
[ 0] frame obj .lex = previous lexical frame. --------------------------------------------------------- [-1] return address [-2] param count [-3] function obj (.lex) [-4] param 1 [-5] param 2 [ ] ... [..] param n [ ] ... [ ] frame obj -- previous frame ---------------------------------------------------------
.lex
はレキシカル・フレームへのポインタ。リンクリストの形でさかのぼることができる。余談だが、ここで演算用のスタックをフレームごとに個別に持たせれば Fiber でスタック状態も復元できると想定しているのだが、そこまで頑張る必要があるかどうかよく分からない。自分自身は困っていない。というのも、スタック状態を復元して良くなる点といえば式の中に
yield
を書くといった以下のようなコードだが、こういうの使えなくても良い気がする。var x = (yield 10)[0] + 50; var y = func(yield a, ++a);ちなみに現在の関数呼び出し時の引数評価は 後ろから だが、一般的にこれをあまり保証したくないので、上記のうちの関数呼び出しで使われる
yield
のa
の値はインクリメント後か前かは保証されないことになるだろう。レキシカル変数
レキシカル変数は関数オブジェクト作成時にリストとして関数オブジェクト自体に設定される。関数呼び出しの際、関数オブジェクトに格納されていたレキシカルスコープへのポインタが、作成されたフレームに設定される。関数内で関数が定義されると、その時のフレームをレキシカル・フレームとして登録することで数珠つなぎの形で参照できるようになる、といった算段。
スタック上のイメージは、フレームごとにレキシカル参照が設定されている状態。
|↑| frame -> lex -> lex -> ... |ス| │ |タ| │ |ッ| ↓ |ク| frame -> lex -> lex -> ... |↓| │尚、レキシカル参照されているフレームは GC で回収されないようにマークを付けなければならない。
スプレッド演算子(
...
)関数呼び出しの際のスプレッド演算子(例:
func(...a)
等)は、呼び出しの段階で個別のパラメータに展開される。その際、その数に合わせてparam count
の値が調整された形で格納される。例えば以下のコード;
b = [1,2,3]; func(a, ...b);これは実行時に以下のように展開されたのと同じ動作をする。
b = [1,2,3]; func(a, 1, 2, 3);もちろんコンパイル時には決定できないので、実行時に展開される。
eval
eval()
は、VM をネストさせるのが大変そうだったので、動的にコンパイルした結果を現在のコードの最後に追記し、同じ VM 上で単にジャンプするように実装している。こうしておくと例外発生時の扱いも単純になる。その他
どうも Visual Studio は Switch-Case の Case 数がある閾値を超えると 最適化をやめてしまう 模様。ちゃんとした公式文書が発見できなかったので、このあたりに詳しい方がいたら教えてください。目に見えてコンパイル時間が短くなり、パフォーマンスが悪くなる。
それにしても最適化が有効な時の src/ir_exec.c の Visual Studioでのコンパイル時間が泣きたいほどに遅い。かといってココを最適化しとかないと実行時速度がやばことになる。(今は WSL 上でやっている)gcc だと全然苦にならないほどコンパイルが速い。それでもダイレクト・スレッデッド・コードのおかげか、gcc でコンパイルしたほうがパフォーマンスも良い。実行ファイルに依存性を持たせない意味で Windows 上でのビルドは Visual Studio がいいんだけどなー。どうにかならんかな。
今後
IR のセーブ・ロード機能を付けたいところ。命令ごとに使うフィールドは決まっているので、それをルール化して書き出せばよいし、同じように読めばよい。やることはわかっているのだが、時間が追い付いていないな。
おわりに
今回も時間を割いて読んでいただいてありがとうございます。最後はいつもの以下の定型フォーマットです。興味がありましたら ★ とか 「
いいねLGTM」 ボタンとか押してもらえるとモチベーションにつながります。どうぞよろしくお願いします。
- 最初の動機は スクリプト言語 KINX(ご紹介) を参照してください(もし宜しければ「
いいねLGTM」ボタンをポチっと)。- リポジトリは ここ(https://github.com/Kray-G/kinx) です。こちらももし宜しければ★をポチっと。
- 投稿日:2020-03-24T01:29:00+09:00
git pull時に 【Your local changes to the following files would be overwritten by merge】とエラー
エラー内容
$ git pull origin masterをすると
error: Your local changes to the following files would be overwritten by merge:
config/routes.rb
Please commit your changes or stash them before you merge.が出てくる。
原因
pullした内容と自分の編集した箇所(ここではconfig/routes.rb)が被っている。
解決策
pullした内容の箇所が自分も編集している所の為、mergeする前にcommitするかstashしてと言われる。
なので、今回はコミットを選択。
$ git add *
$ git commit -m "コミット名"
$ git push origin 自分の作業ブランチ
でpushし、Github上で
コンフリクトが起きなければ再度、
$ git pull origin master
とpullすればOK。
コンフリクトの場合はコンフリクト
内容を修正してから再度、pull。
- 投稿日:2020-03-24T00:18:27+09:00
rails form_withでフォロー機能作成
はじめに
今回はフォロー機能を作成します。railsチュートリアルではform_forを利用していましたが、今回はform_withを利用して作成します。
(題材は自分のポートフォリオです。Userモデルはある前提で進めます。)対象読者
railsチュートリアル終わったレベルくらいの人
作成の流れ
1.relationshipモデルの作成
2.各モデルの関連付け、フォロー機能のメゾット作成
3.対応するコントローラーの作成
4.form_withを利用したフォローボタン作成
5.jsファイルの作成
6.フォロー機能の流れ今回のAjaxの流れ
①fomr_withで作成したフォロー/アンフォローボタンを押すとPost/deleteリクエストが送られる。
↓
②対応するコントローラのcreate/destroyアクションを実行
↓
③js.erbファイルをレンダリング
↓
④Ajax対象範囲のフォロー/アンフォローボタンを非同期で切り替え1.relationshipモデルの作成
relationshipモデルの中身はこんな感じです。中間テーブルを作成しますが別にフォローしているユーザーの情報を記録するようなテーブルは作りません。実際のフォローする際のテーブルの流れは
Userモデル→Follow_relationship→Userモデルと帰ってくる流れになります。
follower,followingと名前がついていますがどちらもUserのidが入ります。
カラム 型 id integer follower_id integer following_id integer モデルを作成します。
それぞれのカラムに外部キー制約とindexをこの後つけるのでreferencesを付けて生成します。(referencesをつけると自動で〇〇_idの形にしてくれます。)rails g model Follow_Relationship follower:references following:references2.各モデルの関連付け、フォロー機能のメゾット作成
作成されたマイグレーションファイルを確認します。
db/migrateclass CreateFollowRelationships < ActiveRecord::Migration[5.1] def change create_table :follow_relationships do |t| t.references :follower, foreign_key: { to_table: :users } t.references :following, foreign_key: { to_table: :users } t.timestamps end add_index :follow_relationships, [:follower_id, :following_id], unique: true end endここでforegin_key(外部キー)を設定しています。よく外部キーを設定する時に使う「foreign_key: true」としてしまうと存在しないfollowersデーブルを参照してしまうので「to_table: :users」として参照はusersテーブルのidであることを指定してます。
外部キーを設定することによりindexの追加と存在するuserのidのみをdbに保存するようになります。
また、unique: trueとすることにより、同じ組み合わせでデータを保存するのを防ぐようにしているので同じユーザーを2回フォローできなくしています。各モデルの関連付けはこのような形になります。
app/model/follow_relationship.rbbelongs_to :follower, class_name: "User" belongs_to :following, class_name: "User" validates :follower_id, presence: true validates :following_id, presence: trueapp/model/user.rbhas_many :following_relationships,foreign_key: "follower_id", class_name: "FollowRelationship", dependent: :destroy has_many :followings, through: :following_relationships has_many :follower_relationships,foreign_key: "following_id",class_name: "FollowRelationship", dependent: :destroy has_many :followers, through: :follower_relationshipsそれぞれのオプションの意味は
foreign_key: "follower_id" 外部キーの名前を直接参照します。
※マイグレーションで設定した物はあくまでもUserのidがこのカラムに入りますと指定しただけなのでフォローしているユーザーを検索等するときはfollowingカラムを参照するよと指定しないといけません。
class_name:関連名(following_relationships)としていますがそのようなテーブルは存在しないので実際に参照するテーブルであるFollowRelationshipを指定してあげます。
dependent: :destroy Userが削除された時にrelationshipのuseridが削除されるようにしています。has_many :followings, through: :following_relationships
とすると先ほど指定したfollowing_relationshipsを通してフォローしているユーザーのidを取得できるようになります。フォロー関連のメゾットを作成します。
app/model/user.rb#すでにフォロー済みであればture返す def following?(other_user) self.followings.include?(other_user) end #ユーザーをフォローする def follow(other_user) self.following_relationships.create(following_id: other_user.id) end #ユーザーのフォローを解除する def unfollow(other_user) self.following_relationships.find_by(following_id: other_user.id).destroy end3.コントローラーの作成
まずはコントローラーを作成します。
rails g controller follow_relationships:
続いてルーディングの設定です。フォロー/フォロワーの一覧ページ用のアクションはUserオブジェクト絡みの機能なのでコントローラー内に記載します。
そのために、resources: userにmemberを使用してルートを追加します。
memberを使用するとURL内にユーザーを識別するidが追加されます。
idは後ほどUserコントローラーの対応するアクションないで必要となるのでmemberでルートを追加しないといけません。(idを追加しないcollectionというメゾットもあります。)confing.routes.rbresources :users do member do get :following, :followers end end resources :follow_relationships, only: [:create, :destroy]一覧ページ用のアクションはこのようになります。(kaminariのページネーション機能を利用しています。)
※それぞれ対応するビューを作成してください。app/controllers/users.rbdef followings @user =User.find(params[:id]) @users =@user.followings.page(params[:page]).per(5) render 'show_followings' end def followers @user =User.find(params[:id]) @users =@user.followers.page(params[:page]).per(5) render 'show_followers' endfollow_relationshipのコントローラーはこんな感じです。
followとunfollowが先ほどUser.rbに記載したメゾットになります。app/controllers/follow_relationships.rbdef create @user =User.find(params[:follow_relationship][:following_id]) current_user.follow(@user) respond_to do |format| format.html {redirect_back(fallback_location: root_url)} format.js end end def destroy @user = User.find(params[:follow_relationship][:following_id]) current_user.unfollow(@user) respond_to do |format| format.html {redirect_back(fallback_location: root_url)} format.js end end endrespond_to do |format|はリクエストの種類によってフォロー/アンフォローした際にレンダリングするビューを指定しています。クライアント側の設定でjsが無効になっている場合にhtml側の処理を書いておかないとエラーが出るので注意してください。
[format.html]ではredirect_backメゾットで直前のページを表示、表示できなければroot_urlに戻しています。※redirect_toを使用してflashを表示するパターンも設定できます。
jsでリクエストがくれば[format.js]を通って、follow_relationshipのcreate.js.erbへ処理が向かいます。format.jsの後にrender先を指定しない場合は自動的にアクションに対応するjsファイルを読み込みます。この場合はこの後記載するcreate.js.erbが該当するファイルになります。app/controllers/follow_relationships.rb※flashバージョン def create @user =User.find(params[:follow_relationship][:following_id]) current_user.follow(@user) respond_to do |format| format.html { redirect_to @user, flash: {success: 'フォローしました!'} } format.js end end4.フォローボタン作成
コントローラーのアクションができたのフォローボタンを作成します。その前にフォロー一覧とフォロワー一覧ページを作成しておきます。今回はUserのshowページにリンクを用意してます。※フォロー機能には直接の関係はありません。
app/views/users/show_html<%= link_to "フォロー(#{@user.followings.count})", followings_user_path(@user), class: "nav-link" %> <%= link_to "フォロワー(#{@user.followers.count})", followers_user_path(@user), class: "nav-link" %>フォローボタンも同じくshowページに配置しますがコードがグチャグチャにならないようにボタンのフォームはrenderしてます。
app/views/users/show_html<% if logged_in? && @user != current_user%> <div id="follow_form"> <% if current_user.following?(@user) %> <%= render "unfollow" %> <% else %> <%= render "follow" %> <% end %> </div> <% end %>if logged_in? && @user != current_user
→ログイン済みであることと、このshowページが自分以外のユーザーのページであることを確認しています。&&はAND条件を表すので全ての条件を満たさないとボタンが
表示されません。!=は等しくないときtureとなります。
if current_user.following?(@user)
→ログイン中のユーザーがすでにフォロー済みであればunfollowをまだフォローしていなければfollowボタンを表示するようにしています。
次は実際のボタンの中身です。app/views/users/follow_html<%= form_with(model: current_user.following_relationships.build) do |f| %> <%= f.hidden_field :following_id, value: @user.id %> <%= f.submit "フォローする", class: "btn btn-outline-secondary" %> <% end %>from_withはデフォルトでAjax通信をするようになっています。
modelにはモデルクラスのインスタンス(@userとか)を渡す必要があるので、空のfollow_relationshipインスタンスを作成してpostリクエストを動作させ、follow_relationshipのコントローラーのcreateアクションに渡します。(userを作成するときにコントローラーのアクションでUser.newをする意味と同じです。多分)
f.hidden_fieldでfollowing_idにユーザーidを入れるようにしています。
※クライアント側には見えずに、ボタンを押した時にパラメーターに入ります。app/views/users/unfollow_html<%= form_with(model: current_user.following_relationships.find_by(following_id: @user.id),method: :delete) do |f| %> <%= f.hidden_field :following_id %> <%= f.submit "フォロー", class: "btn btn-outline-secondary" %> <% end %>フォロー解除ボタンはフォローボタンとは違ってdeleteリクエストを指定してdestroyアクションを動作させています。
5.jsファイルの作成
フォームからのPostリクエストをcreateアクションで処理したあとはcreate.js.erbにたどり着きます。そこでAjaxでレンダリングする部分(今回の場合はボタン)を指定します。create.js.erbにたどり着くのはcreateアクション後なのでレンダリングするのはフォロー解除ボタンになります。
app/views/follow_relationships/create.js.erb$("#follow_form").html("<%= j(render("users/unfollow")) %>");app/views/follow_relationships/destroy.js.erb$("#follow_form").html("<%= j(render("users/follow")) %>");それぞれ対応するフォームを読み込みます。follow_formはshowページに記載したcssのidです。
以上でフォロー機能が動作するようになりました。
最後にフォロー機能の流れだけまとめで記載しておきます。6.フォロー機能の流れ
実際にフォローボタンを押すとこのような動きをサーバーのログから確認できます。
※流れを追うだけなので一部省略してます。Started POST "/follow_relationships"→フォローボタンが押される Processing by FollowRelationshipsController#create as JS Parameters:"follow_relationship"=>{"following_id"=>"3"}→パラメータにfollow_relationshipをキーとしたフォロー対象のユーザーidが入ります。 コントローラーのアクション完了! Rendering follow_relationships/create.js.erb→アクション後にjsのファイル読み込み。 Rendered users/_unfollow.html.erb (2.1ms) Rendered follow_relationships/create.js.erb (3.2ms)以上となります。
かなり長くなってしまいましたが、最後までお読みいただきありがとうございました。