20210724のRubyに関する記事は15件です。

ラムダ式プログラミング一時間体験講座(Python/Ruby/JavaScript同時並行版)

筆者『一時間体験講座』第4弾(1,2,3).内容的にはこちらの方が近いかもしれませんが,より実践的な内容となっています.複数言語対応の体験ということもあって,用語や仕組みの正確なところの解説はほとんどありません1. 前書き この記事は,ともすれば難読化扱いされかねないラムダ式を用いたプログラミングの普及の一助になればという筆者の被害妄想想いから生まれました.3言語同時並行で進めますが,いずれの言語も事前知識はあまり必要ありません.講座の目標として,次の記述が何を行うものかわかるようになればいいなあという感じです. Python $ python >>> (lambda u: u(u))(lambda u: (lambda n,a,b: a if n == 0 else u(u)(n - 1, b, a + b)))(40,0,1) 102334155 >>> exit() $ Ruby $ irb --simple-prompt >> ->u{u[u]}[->u{->n,a,b{n == 0 ? a : u[u][n-1,b,a+b]}}][40,0,1] => 102334155 >> exit $ JavaScript $ node > (u=>u(u))(u=>(n,a,b)=> n == 0 ? a : u(u)(n-1,b,a+b))(40,0,1) 102334155 > (Ctrlキーを押しながらD) $ 実行例はいずれも,UNIXシェルからの対話モード(REPL)で記載しています.それぞれの環境で適宜読み替えてもらえればと思います. ※JavaScriptについては,上記Node.jsの他,Chrome/EdgeでCtrl+Shift+Jで呼び出されるconsoleでも実行可能です. ラムダ式の基本 ラムダ式は無名関数とも呼ばれ,名前のない関数です.関数そのものは数学のそれと基本的には同じで,ある変数に依存して決まる値あるいはその対応を表す式によって構成されています.プログラミングでは,変数やその変数の値を引数(ひきすう)と呼んでいます.次の図は,関数処理の大まかな流れです. 引数として変数$x$,$y$をとってその変数の値を足した結果を返すラムダ式は,次のように記述できます. Python >>> lambda x, y: x + y Ruby >> ->x,y{x+y} JavaScript > (x,y)=>x+y 上記を実行2しただけでは『関数だよ』という結果表示しかありません.ですが,ラムダ式はこの実行時に,変数と値の対応を保持するための記憶領域を内部に作ります3. 変数に値を対応させて処理を行わせる4には,実際の値を引数として指定します. Python >>> (lambda x, y: x + y)(10, 20) 30 Ruby >> ->x,y{x+y}[10,20] => 30 JavaScript > ((x,y)=>x+y)(10,20) 30 実際の値については,ラムダ式を指定5することもできます.次は,3つの引数$f$,$x$,$y$をとり,$f(x,y)$,すなわち,関数$f$に$x$,$y$の値を渡して計算させるラムダ式です. Python >>> lambda f, x, y: f(x, y) Ruby >> ->f,x,y{f[x,y]} JavaScript > (f,x,y)=>f(x,y) $f$に上記の足し算を行うラムダ式を,$x$,$y$にそれぞれ10,20を引数として指定して実行すると,足し算のラムダ式の時と同じ結果が得られます6. Python >>> (lambda f, x, y: f(x, y))(lambda x, y: x + y, 10, 20) 30 Ruby >> ->f,x,y{f[x,y]}[->(x,y){x+y},10,20] => 30 JavaScript > ((f,x,y)=>f(x,y))((x,y)=>x+y,10,20) 30 注意してほしいのは,$f$,$x$,$y$のラムダ式の$x$,$y$と,足し算を行うラムダ式の$x$,$y$は全くの別物であるということです7.先の記憶領域の仕組みにより,$x$,$y$はそれぞれのラムダ式で独自に値の対応付けの管理が行われます. もし,あるラムダ式が別のラムダ式の変数を参照したい場合は,ラムダ式を返すラムダ式を定義して親子関係とし8,子が親の変数を参照可能とします.次は,引数$x$をとって『引数$y$をとって$x+y$を計算するラムダ式』を返すラムダ式の例です. Python >>> lambda x: lambda y: x + y Ruby >> ->x{->y{x+y}} JavaScript > x=>y=>x+y この式に値10を指定すると,引数xが10に対応付けられ,その対応付けを引き継いだ『引数$y$をとって$x+y$を計算するラムダ式』が返ります.最初のラムダ式の例と同じく,ラムダ式だけが返っても『関数だよ』という表示しか行われません. Python >>> (lambda x: lambda y: x + y)(10) Ruby >> ->x{->y{x+y}}[10] JavaScript > (x=>y=>x+y)(10) このラムダ式に値20を指定すると,引数$y$に20が対応付けられ,なおかつ,$x+y$が計算されますから,晴れて$10+20⇒30$が表示されます. Python >>> (lambda x: lambda y: x + y)(10)(20) 30 Ruby >> ->x{->y{x+y}}[10][20] => 30 JavaScript > (x=>y=>x+y)(10)(20) 30 ループ式と条件分岐 次は,ループを発生させるラムダ式です. Python >>> (lambda u: u(u))(lambda u: u(u)) Ruby >> ->u{u[u]}[->u{u[u]}] JavaScript > (u=>u(u))(u=>u(u)) 今回取り上げているプログラミング言語の処理系は,一定回数以上のループ(正確には,再帰)が発生するとエラーとなり停止します. Python >>> (lambda u: u(u))(lambda u: u(u)) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 1, in <lambda> File "<stdin>", line 1, in <lambda> File "<stdin>", line 1, in <lambda> [Previous line repeated 996 more times] RecursionError: maximum recursion depth exceeded Ruby >> ->u{u[u]}[->u{u[u]}] Traceback (most recent call last): 16: from (irb):1:in `block in irb_binding' 15: from (irb):1:in `block in irb_binding' 14: from (irb):1:in `block in irb_binding' 13: from (irb):1:in `block in irb_binding' 12: from (irb):1:in `block in irb_binding' 11: from (irb):1:in `block in irb_binding' 10: from (irb):1:in `block in irb_binding' 9: from (irb):1:in `block in irb_binding' 8: from (irb):1:in `block in irb_binding' 7: from (irb):1:in `block in irb_binding' 6: from (irb):1:in `block in irb_binding' 5: from (irb):1:in `block in irb_binding' 4: from (irb):1:in `block in irb_binding' 3: from (irb):1:in `block in irb_binding' 2: from (irb):1:in `block in irb_binding' 1: from (irb):1:in `block in irb_binding' SystemStackError (stack level too deep) JavaScript > (u=>u(u))(u=>u(u)) Thrown: RangeError: Maximum call stack size exceeded at u (repl:1:11) at u (repl:1:14) at u (repl:1:14) at u (repl:1:14) at u (repl:1:14) at u (repl:1:14) at u (repl:1:14) at u (repl:1:14) at u (repl:1:14) at u (repl:1:14) なぜ,ループが発生するのでしょうか?それは,上記の式が,引数に値として同じラムダ式を指定することで,全く同じ式が構成されるからです.構成された式は再び引数に同じラムダ式を指定していますから,引数への値の指定が次々と行われます. Python (lambda u: u(u))(lambda u: u(u)) ➡ u(u) ※ラムダ式の記憶領域に『u = lambda u: u(u)』を保持 ➡ (lambda u: u(u))(lambda u: u(u)) ➡ u(u) ※ラムダ式の記憶領域に『u = lambda u: u(u)』を保持 ➡ (lambda u: u(u))(lambda u: u(u)) ➡ … Ruby ->u{u[u]}[->u{u[u]}] ➡ u[u] ※ラムダ式の記憶領域に『u = ->u{u[u]}』を保持 ➡ ->u{u[u]}[->u{u[u]}] ➡ u[u] ※ラムダ式の記憶領域に『u = ->u{u[u]}』を保持 ➡ ->u{u[u]}[->u{u[u]}] ➡ … JavaScript (u=>u(u))(u=>u(u)) ➡ u(u) ※ラムダ式の記憶領域に『u = u=>u(u)』を保持 ➡ (u=>u(u))(u=>u(u)) ➡ u(u) ※ラムダ式の記憶領域に『u = u=>u(u)』を保持 ➡ (u=>u(u))(u=>u(u)) ➡ … このループ式の意味するところは,適切な脱出方法があれば,繰り返し相当の処理をラムダ式のみで記述できることです.ここでは,値として指定している(右側の)ラムダ式の中に,『繰り返すたびに値が変化する変数』と『その値によって条件分岐する仕組み』を組み込むことを考えます. まず,値として指定しているラムダ式について,同じラムダ式を指定している箇所の代わりに,単に『引数の値を1減らして返す』だけのラムダ式としてみます. Python (lambda u: u(u))(lambda u: u(u)) ⬇ (lambda u: u(u))(lambda u: (lambda n: n - 1)) Ruby ->u{u[u]}[->u{u[u]}] ⬇ ->u{u[u]}[->u{->n{n-1}}] JavaScript (u=>u(u))(u=>u(u)) ⬇ (u=>u(u))(u=>n=>n-1) この場合,ラムダ式の実行は2巡だけであり,ループで用いていた変数がなくなるため,『引数の値を1減らして返す』ラムダ式がそのまま戻ります.したがって,実際の値として数値を与えると,1減った値が結果として返ります. Python >>> (lambda u: u(u))(lambda u: (lambda n: n - 1))(3) # ➡ (lambda u: (lambda n: n - 1))(lambda u: (lambda n: n - 1))(3) # 左のラムダ式は,uがどのような値であっても(lambda n: n - 1)を返す # ➡ (lambda n: n - 1)(3) 2 Ruby >> ->u{u[u]}[->u{->n{n-1}}][3] # ➡ ->u{->n{n-1}}[->u{->n{n-1}}][3] # 左のラムダ式は,uがどのような値であっても->n{n-1}を返す # ➡ ->n{n-1}[3] => 2 JavaScript > (u=>u(u))(u=>n=>n-1)(3) // ➡ (u=>n=>n-1)(u=>n=>n-1)(3) // 左のラムダ式は,uがどのような値であってもn=>n-1を返す // ➡ (n=>n-1)[3] 2 そして,この『引数の値を1減らして返す』ラムダ式を更に,『引数の値が0ならば1を返し,そうでなければ,引数の値を1減らした値を返す』ラムダ式に修正します. Python (lambda u: u(u))(lambda u: (lambda n: n - 1)) ⬇ (lambda u: u(u))(lambda u: (lambda n: 1 if n == 0 else n - 1)) Ruby ->u{u[u]}[->u{->n{n-1}}] ⬇ ->u{u[u]}[->u{->n{n == 0 ? 1 : n - 1}}] JavaScript (u=>u(u))(u=>n=>n-1) ⬇ (u=>u(u))(u=>n=>n == 0 ? 1 : n - 1) 上記の条件分岐には,各言語の三項演算子を用いています.三項演算子は,条件式,条件式が真の時の式,条件式が偽の時の式から構成されています(PythonとRuby/JavaScriptで条件式の順番が異なることに注意して下さい). Python >>> a = -100 >>> a * -1 if a < 0 else a * 1 100 >>> a * -1 if a > 0 else a * 1 -100 Ruby >> a = -100 >> a < 0 ? a * -1 : a * 1 => 100 >> a > 0 ? a * -1 : a * 1 => -100 JavaScript > a = 100 100 > a < 0 ? a * -1 : a * 1 100 > a > 0 ? a * -1 : a * 1 -100 話を戻し,条件分岐を組み込んだラムダ式を用いて,引数が0の場合とそれ以外の場合で実行してみます. Python >>> (lambda u: u(u))(lambda u: (lambda n: 1 if n == 0 else n - 1))(0) # ➡ (lambda u: (lambda n: 1 if n == 0 else n - 1))(lambda u: (lambda n: 1 if n == 0 else n - 1))(0) # ➡ (lambda n: 1 if n == 0 else n - 1)(0) 1 >>> (lambda u: u(u))(lambda u: (lambda n: 1 if n == 0 else n - 1))(3) # ➡ (lambda u: (lambda n: 1 if n == 0 else n - 1))(lambda u: (lambda n: 1 if n == 0 else n - 1))(3) # ➡ (lambda n: 1 if n == 0 else n - 1)(3) 2 Ruby >> ->u{u[u]}[->u{->n{n == 0 ? 1 : n - 1}}][0] # ➡ ->u{->n{n == 0 ? 1 : n - 1}}[->u{->n{n == 0 ? 1 : n - 1}}][0] # ➡ ->n{n == 0 ? 1 : n - 1}[0] => 1 >> ->u{u[u]}[->u{->n{n == 0 ? 1 : n - 1}}][3] # ➡ ->u{->n{n == 0 ? 1 : n - 1}}[->u{->n{n == 0 ? 1 : n - 1}}][3] # ➡ ->n{n == 0 ? 1 : n - 1}[3] => 2 JavaScript > (u=>u(u))(u=>n=>n == 0 ? 1 : n - 1)(0) // ➡ (u=>n=>n == 0 ? 1 : n - 1)(u=>n=>n == 0 ? 1 : n - 1)(0) // ➡ (n=>n == 0 ? 1 : n - 1)(0) 1 > (u=>u(u))(u=>n=>n == 0 ? 1 : n - 1)(3) // ➡ (u=>n=>n == 0 ? 1 : n - 1)(u=>n=>n == 0 ? 1 : n - 1)(3) // ➡ (n=>n == 0 ? 1 : n - 1)(3) 2 引数が0の時は1を返し,それ以外では1を減らした値を返すようになりました.ただしこれは先と同じく,ループで用いていた変数がなくなるためであり,条件分岐を組み込んだラムダ式のみを用いた場合と同じです.異なるのは,この条件分岐を組み込んだラムダ式が2巡してから同じラムダ式として戻ってくる(そして,引数が0の時は1を返し,それ以外では1を減らした値を返す),ということです. ここで,条件分岐を組み込んだラムダ式について,次のように,『引数の値を1減らした値を返す』箇所に,一度なくした『同じラムダ式を指定する』記述を組み込み,そして,引数に0以外の値を渡してみます. Python >>> (lambda u: u(u))(lambda u: (lambda n: 1 if n == 0 else u(u)(n - 1)))(3) # ➡ (lambda u: (lambda n: 1 if n == 0 else u(u)(n - 1)))(lambda u: (lambda n: 1 if n == 0 else u(u)(n - 1)))(3) # ➡ (lambda n: 1 if n == 0 else u(u)(n - 1))(3) ➡ u(u)(2) # ➡ (lambda u: (lambda n: 1 if n == 0 else u(u)(n - 1)))(lambda u: (lambda n: 1 if n == 0 else u(u)(n - 1)))(2) # ➡ (lambda n: 1 if n == 0 else u(u)(n - 1))(2) ➡ u(u)(1) # ➡ (lambda u: (lambda n: 1 if n == 0 else u(u)(n - 1)))(lambda u: (lambda n: 1 if n == 0 else u(u)(n - 1)))(1) # ➡ (lambda n: 1 if n == 0 else u(u)(n - 1))(1) ➡ u(u)(0) # ➡ (lambda u: (lambda n: 1 if n == 0 else u(u)(n - 1)))(lambda u: (lambda n: 1 if n == 0 else u(u)(n - 1)))(0) # ➡ (lambda n: 1 if n == 0 else u(u)(n - 1))(0) 1 Ruby >> ->u{u[u]}[->u{->n{n == 0 ? 1 : u[u][n - 1]}}][3] # ➡ ->u{->n{n == 0 ? 1 : u[u][n - 1]}}[->u{->n{n == 0 ? 1 : u[u][n - 1]}}][3] # ➡ ->n{n == 0 ? 1 : u[u][n - 1]}[3] ➡ u[u][2] # ➡ ->u{->n{n == 0 ? 1 : u[u][n - 1]}}[->u{->n{n == 0 ? 1 : u[u][n - 1]}}][2] # ➡ ->n{n == 0 ? 1 : u[u][n - 1]}[2] ➡ u[u][1] # ➡ ->u{->n{n == 0 ? 1 : u[u][n - 1]}}[->u{->n{n == 0 ? 1 : u[u][n - 1]}}][1] # ➡ ->n{n == 0 ? 1 : u[u][n - 1]}[1] ➡ u[u][0] # ➡ ->u{->n{n == 0 ? 1 : u[u][n - 1]}}[->u{->n{n == 0 ? 1 : u[u][n - 1]}}][0] # ➡ ->n{n == 0 ? 1 : u[u][n - 1]}[0] => 1 JavaScript > (u=>u(u))(u=>n=>n == 0 ? 1 : u(u)(n - 1))(3) // ➡ (u=>n=>n == 0 ? 1 : u(u)(n - 1))(u=>n=>n == 0 ? 1 : u(u)(n - 1))(3) // ➡ (n=>n == 0 ? 1 : u(u)(n - 1))(3) ➡ u(u)(2) // ➡ (u=>n=>n == 0 ? 1 : u(u)(n - 1))(u=>n=>n == 0 ? 1 : u(u)(n - 1))(2) // ➡ (n=>n == 0 ? 1 : u(u)(n - 1))(2) ➡ u(u)(1) // ➡ (u=>n=>n == 0 ? 1 : u(u)(n - 1))(u=>n=>n == 0 ? 1 : u(u)(n - 1))(1) // ➡ (n=>n == 0 ? 1 : u(u)(n - 1))(1) ➡ u(u)(0) // ➡ (u=>n=>n == 0 ? 1 : u(u)(n - 1))(u=>n=>n == 0 ? 1 : u(u)(n - 1))(0) // ➡ (n=>n == 0 ? 1 : u(u)(n - 1))(0) 1 ループが復活しましたが,『繰り返すたびに値が変化する変数』と『その値によって条件分岐する仕組み』が揃いましたので,停止する繰り返し処理となりました.次は,この仕組みを応用した,5の階乗を求めるラムダ式です. Python >>> (lambda u: u(u))(lambda u: (lambda n,r: r if n < 1 else u(u)(n - 1, n * r)))(5, 1) 120 Ruby >> ->u{u[u]}[->u{->n,r{n < 1 ? r : u[u][n-1,n*r]}}][5,1] => 120 JavaScript > (u=>u(u))(u=>(n,r)=>n < 1 ? r : u(u)(n-1,n*r))(5,1) 120 今回のラムダ式による繰り返しパターン9を各言語でまとめると次のようになります. Python (lambda u: u(u))(lambda u: (lambda 引数変数: 停止時に返す式 if 停止の条件式 else u(u)と引数の変数を含む繰り返し計算))(引数値) Ruby ->u{u[u]}[->u{->引数変数{停止の条件式 ? 停止時に返す式 : u[u]と引数変数を含む繰り返し計算}}][引数値] JavaScript (u=>u(u))(u=>(引数変数)=>停止の条件式 ? 停止時に返す式 : u(u)と引数の変数を含む繰り返し計算)(引数値) 備考 記事に関する補足 説明はするけど対応する用語は出さないという….あとで用語一覧を追加するかも.→脚注にしてみました. 更新履歴 2021-07-25:用語の類を脚注に記載 2021-07-24:初版公開 用語の類は脚注にしました. ↩ 評価(eval) ↩ クロージャ ↩ 関数適用(apply) ↩ 高階関数(引数が関数) ↩ ラムダ計算のβ簡約相当 ↩ レキシカルスコープ ↩ 高階関数(戻り値が関数) ↩ Uコンビネータ ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

"after_sign_in_path_for" device実装 ログイン後の遷移先を指定する

"after_sign_in_path_for" の使い方について 使用例 application_controller protected def after_sign_in_path_for(resource) root_path end 説明 「after_sign_in_path_for」の役割は、ログイン後に遷移先を指定することができます。 今回はroot_pathをしているので、ログイン後ホーム画面に遷移します。 今回出てきた単語について ・「protected」 ・「(resourse)」 ・「root_path」
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

"after_sign_in_path_for" device実装 ログイン後の遷移先を指定する[Ruby on Rails]

"after_sign_in_path_for" の使い方について 使用例 application_controller protected def after_sign_in_path_for(resource) root_path end 説明 「after_sign_in_path_for」の役割は、ログイン後に遷移先を指定することができます。 今回はroot_pathをしているので、ログイン後ホーム画面に遷移します。 今回出てきた単語について ・protected ・(resourse) ・root_path
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

a

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

ab

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

herokuデプロイで環境変数関連で積んだ話

開発環境 ruby:3.0.1 rails:6.0.4 heroku Mysql 起こったこと ruby on railsにてアプリケーションを作成して一度テストでherokuにデプロイしてみようと思い下記コマンドを実行 git push heroku master とワクワクしながらデプロイしてみて無事に成功したと思い、さっそくURLをたたいてみたところ We're sorry, but something went wrongとでかでかと表示されて「いやなんでだよ!」と思いながら、とりあえず正しく表示されないってことはどこか間違いやエラーがているのではと思い、デプロイした際に出てくる細かい文字は読み返して見たところ気になる記述がありました。 remote: -----> Preparing app for Rails asset pipeline remote: Running: rake assets:precompile remote: rake aborted! remote: NoMethodError: undefined method `[]' for nil:NilClass ##省略 remote: ! remote: ! Precompiling assets failed. remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed remote: Verifying deploy... remote: remote: ! Push rejected to hogehoge. remote: To https://git.heroku.com/hogehoge.git ! [remote rejected] master -> master (pre-receive hook declined) error: failed to push some refs to 'https://git.heroku.com/appname.git' 「え?なにこれ?」って一瞬思いましたが、よくよく見てみるとNoMethodError: undefined method `[]' for nil:NilClassと表示されており本番環境では aws s3に画像を保存するように記述しており、そこで設定した環境変数をちゃんと代入できないと思い下記コードを実行 rails c これでAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_S3_REGION, AWS_S3_BUCKETのすべての環境変数がちゃんと設定できているか確認しました。 つまり環境変数はちゃんと定義されており、問題ないと思い再度プッシュしてみましたが、やはり先ほどと同じエラーが出ます。 そしてherokuにおいてある環境変数を全部大文字から小文字にして、再度プッシュしたらちゃんとアプリケーションが表示されました。 以上です、まだまだ知識不足だと痛感しました・・・
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Ruby]曜日の設定

はじめに 本記事では、初学者なりに私が何度も何時間も考え、 ようやく解消したものになります。 備忘録として残したいと思いましたので投稿いたします。 曜日の設定 私がトライしたものは、 以下の画像の曜日を設置するものでした。 *すでに完成したものを添付しております。ちょっと遊んでましたw 回答(曜日追加部分についての回答) コントローラー def get_week wdays = ['日','月','火','水','木','金','土'] @week_days = [] @todays_date = Date.today plans = Plan.where(date: @todays_date..@todays_date + 6) 7.times do |x| today_plans = [] plans.each do |plan| today_plans.push(plan.plan) if plan.date == @todays_date + x end wday_num = Date.today.wday + x if wday_num >= 7 wday_num = wday_num -7 end days = {month: (@todays_date + x).month, date: (@todays_date + x).day, plans: today_plans, wday: wdays[wday_num]} @week_days.push(days) end end end ビュー <div class='calendar'> <% @week_days.each do |day| %> <div class='item'> <div class='date'> <%= day[:month] %>/<%= day[:date] %>・<%= day[:wday] %> </div> <ul class='content'> <% if day[:plans].length != 0 %> <% day[:plans].each do |plan| %> <li class='plan-list'>・<%= plan %></li> <% end %> <% end %> </ul> </div> <% end %> </div> ミスしたこと ビューは特に問題なかったですが、 コントローラーの記述がなかなかできずにいました。 ①曜日を自動的記述できない 日月火水・・・と記述することがなかなかできず、 繰り返し処理することだけが頭にあったため、 ひたすらwday_num = Date.today.wday + 1 と記述していました。 結果として、 毎日が翌日の曜日になるという表示になっていました。 ex.金金金金・・・・・ ②xという存在 まず、日付については繰り返し処理が完成していたため、 それに気づかなかったのは反省点でした。 ①で繰り返し処理ということがわかっていたのに、 それ同様に曜日も設定することの切り返しができていなかった。 ③そもそも作るアプリケーションの理解が浅かった まず、自分はカレンダーを想像していたため、 1ヶ月分まるまるの表示が必要になると勘違いをしてしまっていました。 つまり、30回、31回繰り返し処理が必要だと勘違いをしていたわけです。 今後は、どんなアプリケーションなのかをイメージし、概要を把握した上で、 取り組むことが必要だと感じました。 ④配列の理解の浅さ wdays = ['日','月','火','水','木','金','土'] wdays[0] # =>'日'を取得できる 初歩の初歩。 ここを理解していたかと言われたら本当はそうではなかったです。 プライドを捨て、できないこと、理解していなかったところは、 理解できるように前に進みます。 終わりに たくさんいろんな人にアドバイスをいただきながら完成しました。 これを完成させるのに20時間。。 できない自分に腹が立ち、それとともに自信が少しなくなりました。 ただ、 これがプログラミング、 これから就職したらこのようなことが起こるのだと感じました。 また、リアルに近いものを体験できたことは非常に価値のある時間だったとも感じました。 諦めず、自分をコントロールしながら乗り越えていけるよう引き続き頑張ります!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyでクラスが定義されていないエラーに遭遇(DRb::DRbUnknown)

dRubyというRubyのスタンダードライブラリにあるプロセス間の通信を可能にするものがあります。 これを使って、バックエンドジョブのGemを今作っています。 Belated 用途としては主にRedis, PostgreSQLが使えない、純粋にRuby, SQLiteだけを使う場合に使えるものとして作っています。 (まだちゃんと使えるものではないですが。。。) ちょっと試しているときに、DRb::DRbUnknownというエラーに遭遇しました。 以下のようなコードでした。 $ bundle exec belated # Belatedのプロセスを起動 Railsのコンソールのなか: class Hello def perform puts 'hello world' end end client = Belated::Client.new client.perform_belated(Hello.new) これでDRb::DRbUnknownというエラーになりましたが、原因はBelatedの方はこのコードにアクセスできていないからです。Railsのコンソールで新しく定義したクラスだからですが、ここでこのオブジェクトに対してreloadを実行して、定義されているかどうかを試すこともできるみたいです。 考えてみれば当たり前ですが、普段一つのプロセスの中でしか動いていないプログラムを書いているとなかなかここまで考えないですね。 Sidekiqのコードを読むと、Reloaderというクラスにも遭遇しますので、そこも開発環境の考慮が入っているみたいです。 詳細はこちらで確認できます: https://docs.ruby-lang.org/ja/latest/class/DRb=3a=3aDRbUnknown.html
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails6+jQuery】ページの最上部に戻るボタンの実装

1.背景 サイトを閲覧する際に目にする「PAGE TOP」などの戻るボタン。 今回、Railsで制作したアプリに戻るボタンを実装したので備忘録としてまとめます。 2.環境 mac.os バージョン10.15.6 Ruby 2.7.2 Rails 6.1.3.1 psql (PostgreSQL) 12.6 3.手順 ①jQueryの導入 今回はyarnを用いてjQueryの導入を行いました。 ターミナル. yarn add jquery ※yarnとは:JavaCcriptのパッケージマネージャのこと。 ②environment.jsを編集 config/webpack/environment.jsを編集します。 const { environment } = require('@rails/webpacker') const webpack = require('webpack') environment.plugins.append('Provide', new webpack.ProvidePlugin({   // ここから編集 $: 'jquery/src/jquery', jQuery: 'jquery/src/jquery' })) module.exports = environment ③application.jsを編集 app/javascript/packs/application.jsを編集します。 // コード省略 // 下記を追記 require ('jquery') ④jQueryを記述したいファイルの作成 今回は、application.jsと同じ階層にcustom.jsというファイル名で作成しました。 app/javascript/packs/custom.js そして、application.jsに記述します。 // コード省略 require ('jquery') // 下記を追記 require('custom.js') ⑤挙動の確認 app/javascript/packs/custom.jsにテストコードを書き、OKと表示されるか確認します。 $(function() { console.log("OK"); }); rails sでアプリを起動して検証の画面からconsoleタブを選択しました。 ⑥ボタンの実装(HTML/CSS) 今回はfontawesomeを使用し、下記のようなボタンを実装します。 (1) HTML app/views/layouts/application.html.erbのタグ内にfontawesomeを導入するためのコードを記述します。 また、タグ内にボタンを記述します。 // <head>タグ内 <link href="https://use.fontawesome.com/releases/v5.15.3(※バージョンを入れる)/css/all.css" rel="stylesheet"> // <body>タグ内 <div id="page_top"><a href="#"></a></div> (2) CSS(scss) app/assets/stylesheets/application.scssにコードを記述します。 // トップに戻るボタン #page_top{ width: 50px; height: 50px; position: fixed; right: 20px; bottom: 30px; background: #3f98ef; opacity: 0.6; border-radius: 50%; } #page_top a{ position: relative; display: block; width: 50px; height: 50px; text-decoration: none; text-align: center; } #page_top a::before{ font-family: "Font Awesome 5 Free"; content: '\f106'; font-weight: 900; font-size: 25px; color: #fff; position: absolute; width: 25px; height: 25px; top: 10px; bottom: 25px; right: 0; left: 0; margin: auto; } ⑦ボタンの実装(jQuery) (1) 非推奨の書き方をした→修正 app/javascript/packs/custom.jsにコードを記述します。 $(function(){ var pagetop = $('#page_top'); // ボタン非表示 pagetop.hide(); // 100px スクロールしたらボタン表示 $(window).scroll(function () { if ($(this).scrollTop() > 100) { pagetop.fadeIn(); } else { pagetop.fadeOut(); } }); pagetop.click(function () { $('body, html').animate({ scrollTop: 0 }, 500); return false; }); }); しかし、こちらのように書いたところ、scrollとclickに打ち消し線が入っていました。 調べたところ、非推奨だと打ち消し線が入るとのことだったのでon()を使った書き方に変更しました。 $(function(){ var pagetop = $('#page_top'); // ボタン非表示 pagetop.hide(); // scrollをon('scroll')に変更 $(window).on('scroll', function () { if ($(this).scrollTop() > 100) { pagetop.fadeIn(); } else { pagetop.fadeOut(); } });     // clickをon('click')に変更 pagetop.on('click', function () { $('body, html').animate({ scrollTop: 0 }, 500); return false; }); }); (2) 実装したはずが最初の1回しか実装できていない→修正 今回やりたかったのが、 最初はボタンを非表示 (100px)スクロールしたらボタンが表示される ボタンを押すと最上部に戻る ...でしたが、アプリを起動した最初の1回だけ思い通りの実装になり、 その後は他のページでもトップページ(アプリを起動した際に最初に表示されるページ)でも ボタンが表示されたままで、「jQueryが効いていないな・・・?」と思うことがありました。 原因を調べると、Turbolinksの影響があることがわかりました。 ※Turbolinksとは:Rails4から標準装備されている、ページの遷移を高速化する仕組みのこと。 全てのリンククリックに対するページ遷移を自動的にAjax化(Webブラウザ上で非同期通信を行い、ページの再読み込みなしにページを更新)することで高速化を図る。 Turbolinksはa要素のクリックイベントをフックして、遷移先のページをAjaxで取得します。そして取得したページが要求するJavaScriptやCSSが現在のものと同一であれば現在のものをそのまま使用し、titleやbody要素のみを置き換えます。(→ページ遷移は発生しない。JavaScriptやCSSをブラウザが評価しないので高速化ができる。) 今回の戻るボタンは<a href="#"></a>とaタグを使用しました。なのでボタンを押すとTurbolinksの仕組みが動きます。その影響で、 jQueryのコードの処理が実行されないという現象が起きていました。 (こちらのjQueryのreadyイベントが発火しないに該当します。) $(document).ready(function(){ // 処理内容 }); // (document).readyは省略も可能 $(function(){ // 処理内容 }); 最初だけjQueryが動作しているように見えたのは、戻るボタン(=aタグ)を押す前で Turbolinksが動いていなかったためだと考えられます。 今回はturbolinks:loadというオプションを使用することで正常な読み込みができるようになりました。 // turbolinks:loadを追記+(document).onという書き方に変更 $(document).on('turbolinks:load', function () { var pagetop = $('#page_top'); // ボタン非表示 pagetop.hide(); // 100px スクロールしたらボタン表示 $(window).on('scroll', function () { if ($(this).scrollTop() > 100) { pagetop.fadeIn(); } else { pagetop.fadeOut(); } }); pagetop.on('click', function () { $('body, html').animate({ scrollTop: 0 }, 500); return false; }); }); 4.まとめ 今回色々なサイトを参考に実装しましたが、使用言語のバージョンを踏まえた実装を行うことが大切だと思いました。 5.参考 1.【Rails6】Webpackerを用いてjQueryをインストールする手順を簡単にまとめてみた 2.Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 前編(翻訳) 3.[jQuery] トップへ戻るボタンの実装サンプル 4.【jQuery入門】on()によるイベント処理の使い方まとめ! 5.jQueryでクリックイベントで処理を実行する:on(), click() 6.【Rails】初心者向け!画面遷移の高速化を行うTurbolinksについて図を用いて詳しく解説 7.大場寧子他, 現場で使えるRuby on Rails5速修実践ガイド, マイナビ出版, 2018. 6.最後に 記事の感想や意見、ご指摘等あれば伝えていただけるとありがたいです。 読んでいただき、ありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】ファイルを分割し、ローカル引数を渡す

はじめに この記事を読めば、 ファイルを分割し、ローカル引数を渡す方法 について理解できましす。 ※この記事ではerbを使用しています。 やり方 分割前 index.html.erb <h2>レシピ一覧</h2> <% @posts.each do |post| %> <p><%= post.title %></p> <% end %> 分割後 index.html.erb <h2>レシピ一覧</h2> <%= render "posts", posts: @posts %> _posts.html.erb <% posts.each do |post| %> <p><%= post.title %></p> <% end %> 解説 <%= render "posts", posts: @posts %> render "posts" _posts.html.erbを呼び出しています。 posts: @posts postsに@postsを代入しています。 _posts.html.erb内で@postsをpostsとして使えます。 このページのみではわかりませんが、他の要素を代入したりする際にかなり有効です。 _posts.html.erbでは@postsがpostsに変わっているので気をつけましょう。 さいごに ファイルを分割し、ローカル引数を渡す方法 について解説しました。 参考になったら、LGTMしていただけると幸いです! 最後まで読んでいただきありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails5.2から6.1に更新する際に躓いたところ(zeitwerk)

Rails5.2から6.1に更新する bulk insertを使いたかった。 gem単体でも入れられるらしいけど、まあついでに。 6.xからはオートロード(requireしなくてもクラス参照できるアレ)の仕組みが刷新されている = zeitwerk さっとさらった概要 以前は下記参照ルールだったらしい。 使われている定数の名前 -> 定義しているファイル名の特定 新しいオートロードでは以下参照ルール。 定義しているファイル名の特定 -> 使われている定数の名前 今まで参照できていた、Concernsに入れてるクラスが参照できなくなった???(本題) controller/concerns/gas_api_callable.rb # GAS APIコールする共通モジュール module GasAPICallable extend ActiveSupport::Concern ### code #### end end controller/concerns/gas_controller.rb # GasAPICallableを使って色々するコントローラー class GasController < ApplicationController include GasAPICallable ### code ### end ------> uninitialized constants "GasAPICallable" エラー発生。 ファイル名のCapitalize(大文字化)から定数名を探すことで、「API」というアクロニム(略称)がひろえなくなっているみたい。(根本原因) gas_api_callable.rb -> GasApiCallable ----> GasAPICallableはあるけど、GasApiCallableはないよ???? -> const未定義エラー 対処1 アクロニムをやめる(大文字略称をやめる) controller/concerns/gas_api_callable.rb # GAS APIコールする共通モジュール - module GasAPICallable + module GasApiCallable extend ActiveSupport::Concern ### code #### end end controller/concerns/gas_controller.rb # GasAPICallableを使って色々するコントローラー class GasController < ApplicationController - include GasAPICallable + include GasApiCallable ### code ### end 参照できた。 対処2 アクロニムをzeitwerkに伝える。 config/initializeers/zeitwerk.rb ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym "API" end ※ただしこの場合すべてのAPIという単語をAPIとして定義しなければならない(Apiは探索できない) 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最低限覚えておきたいRSpecの基本構成

今までRSpecに関する記事を複数書いてきましたが、RSpecの基本構成に関する記事を上げてなかったので、今回はかんたんにまとめていきます。 かなり初歩的な内容になりますので、RSpecの基本的な部分が分かっている方は、他の記事をご覧ください。 [Rails]RSpecでテストを行う準備(FactoryBot使用) [Rails]RSpecでモデルのテスト(FactoryBot使用) [Rails]RSpecでよく使うマッチャー10選 [RSpec]request specでcontrollerテストをやってみる RSpecの構成 それではさっそく、RSpecの構成について説明していきます。 RSpecでは、テストしたい項目を入れ子構造で、分類していきます。 入れ子構造にすることで、テストを実行した際にどこでエラーが出ているかがすぐにわかるので、便利ですし、テストコードを書いている際もどこのテストをしているのかわかりやすいです。 構成の部分で使用されるものは下記です。 describe (テストの1番おおきいくくり  :必須) context (describeをさらに細分化する場合にのみ使用) it (具体的なテストをここで実施します :必須) before (itの前に実行したい項目がある場合にのみ使用) describeとitが必須でそれ以外は状況に合わせて使用していきます。 四則演算の例 では、四則演算を例にコードを記述していきます。 describe まず、describeは1番大きいくくりなので、四則演算を入れていきます。 describe '四則演算' do # この中にいろいろと入れていく end context 四則演算という大きなくくりができたので、足し算、引き算といくくりでさらに細分化したいので、contextを使っていきます。 describe '四則演算' do # 足し算に関するテスト context '足し算' do # テストはこの中で実施 end # 引き算に関するテスト context '引き算' do # テストはこの中で実施 end end it では、足し算と引き算という項目まで細分化できたので、足し算と引き算に関する具体的なテストをitで記述していきます。 describe '四則演算' do context '足し算' do it '1 + 1は2になるか' do # エクスペクテーションとマッチャーを使ってテストコード記述 expect(1 + 1).to eq 2 end it '100 + 1は101になるか' do # エクスペクテーションとマッチャーを使ってテストコード記述 expect(100 + 1).to eq 101 end end context '引き算' do it '7 - 5は2になるか' do # エクスペクテーションとマッチャーを使ってテストコード記述 expect(7 - 5).to eq 2 end end end このような入れ子構造がRSpecの基本構造です。 足し算というくくりの中に何個でもitを入れていくことが可能です。 エクスペクテーションとマッチャーってなんやねんって方はこちらどうぞ! [Rails]RSpecでよく使うマッチャー10選 before beforeの説明がまだだったので、beforeに関しても触れておきます。 itの前に実行したいことがあれば、記述していきます。 テストしたいページに移動やサインインがよく使われます。 そのページに遷移することで、そのページでのテストが実行できます。 describe 'トップ画面のテスト' do # itが実行される前にroot_pathに移動しています。 before do visit root_path end context "表示の確認" do it "top画面に投稿一覧へのリンクが表示されているか" do expect(page).to have_link "", href: posts_path end it "root_pathが / であるか" do expect(current_path).to eq('/') end end end まとめ RSpecの構成 describe (テストの1番おおきいくくり  :必須) context (describeをさらに細分化する場合にのみ使用) it (具体的なテストをここで実施します :必須) before (itの前に実行したい項目がある場合にのみ使用) 今回は超初歩的なことをまとめてみました。 RSpecのコードをよくわからず、コピペしていた方などの参考になれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】.envファイルの導入について

1.背景 現在、RubyにてLINEBotを開発中です。 LINEBotのIDやトークンなど、「必要だけどセキュリティ上隠したい」データを格納する dotenvを今回使用したので、備忘録としてまとめます。 2.環境 mac.os バージョン10.15.6 Ruby 2.7.3 psql (PostgreSQL) 12.6 3.手順 ①gemの導入 Gemfile上に下記のコードを記述してbundle(または bundle install)を行う gem "dotenv" ②.envファイルの作成 今回はBotのアカウント(チャネル)に必要な項目を追記しました。 ※隠したいファイルなのでGitの管理から外すよう.gitignoreを設定しましょう。 LINE_CHANNEL_ID = "12345678" LINE_CHANNEL_SECRET = "XXXXXXXXX" LINE_CHANNEL_TOKEN = "XXXXXXXXX" ③loadメソッドを記述する .envを読み込ませたい場所(今回はapp.rbとしました)に追記します。 ENVという配列に格納されているので、ENV['設定した項目']で取り出すことができます。 app.rb Dotenv.load # (例)LINE_CHANNEL_ID を取り出したい puts ENV["LINE_CHANNEL_ID"] #→12345678 4.補足 今回の場合、Dotenv.loadで読み込んだ後に取り出すことができるので、例えばコンソール上で取り出した場合はnil(該当の項目は未設定ということ)が返ってきます。 bot % irb irb(main):001:0> ENV['LINE_CHANNEL_ID'] => nil 5.参考 1.dotenv でパスワードやトークンを隠す 2..gitignore の書き方 3.Ruby + SinatraでLINE Botを作ろう 6.最後に 記事の感想や意見、ご指摘等あれば伝えていただけるとありがたいです。 読んでいただき、ありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails API + Reactで作る俺流アニメデータベース

概要 自分はアニメを見るのが趣味です。昨今のコロナ事情によってリモートワークが基本となった事もあり、以前よりもアニメに没頭する機会が増えました。 毎日のように「今期のアニメで面白そうな作品は無いかなぁ」なんて探しているわけですが、どうもアニメの情報って効率的に取得しづらい気がしています。 もちろん、世の中にはたくさんのアニメ情報サイトが存在しているものの、自分にとっては必要無い情報がたくさん羅列されていたりしてしっくり来ない事もしばしば。 たとえば、私が視聴するアニメを選ぶ基準としては、 どんなスタッフが携わっているか どんな声優さんが出演されているか キャラデザインは自分好みか 世間的な注目度は高そうか といったものが主な判断材料となっています。 要するに、製作陣やキャスト陣、キービジュアルやSNSのフォロワー数などが一目でわかれば情報としてはそれなりに十分というわけですね。 そこで今回は、↑の要件を満たすアプリを自分で作ってみる事にしました。 完成イメージ 年代・季節ごとに作品を絞り込み 作品のタイトルで個別に検索 作品のイメージ画像 製作陣やキャスト陣の情報一覧 公式サイトやTwitterアカウントへのリンク 必要最低限な機能・情報がコンパクトにまとまっていると思います。 主な使用技術・サービス バックエンド Ruby Rails API MySQL フロントエンド React TypeScript 外部サービス Annict 見たアニメを記録したり、見た感想を友達にシェアすることができるWebサービス。APIを公開しており、各作品の情報を取得する事ができる。 しょぼいカレンダー アニメの番組表などが確認できるWebサービス。こちらもAPIを公開しており、各作品の情報を取得する事ができる。 ※ 再現性を考慮してバックエンドのみDockerで環境構築を行います。 Annict、しょぼいカレンダーともにアニメ好きであれば一度は利用した事があるのではないでしょうか。簡単な情報からマニアックな情報まで網羅的に掲載してくれている素晴らしいWebサービスです。 それぞれAPIを公開しているため、素直にそれらを使えば良いんじゃねって思われるかもしれませんが、どちらも個人的には痒いところにあと一歩届かない感があったので、色々こねくり回して扱いやすい形に整形するためバックエンドを準備しました。 実装 前置きはほどほどに実装を開始しましょう。 バックエンド 先にバックエンド側から。 環境構築 何はともあれ環境構築を行います。 各種ディレクトリ・ファイルを作成 $ mkdir aninfo-backend && cd aninfo-backend $ touch Dockerfile docker-compose.yml entrypoint.sh Gemfile Gemfile.lock ./Dockerfile FROM ruby:2.6.3 RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs mariadb-client ENV APP_PATH /myapp RUN mkdir $APP_PATH WORKDIR $APP_PATH COPY Gemfile $APP_PATH/Gemfile COPY Gemfile.lock $APP_PATH/Gemfile.lock RUN bundle install COPY . $APP_PATH COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 CMD ["rails", "server", "-b", "0.0.0.0"] ./docker-compose.yml version: "3" services: db: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: password volumes: - mysql-data:/var/lib/mysql - /tmp/dockerdir:/etc/mysql/conf.d/ ports: - 4306:3306 api: build: context: . dockerfile: Dockerfile command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: - .:/myapp - ./vendor/bundle:/myapp/vendor/bundle environment: TZ: Asia/Tokyo RAILS_ENV: development ports: - "3001:3000" depends_on: - db volumes: mysql-data: ./entrypoint.sh #!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /myapp/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@" ./Gemfile # frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } gem "rails", "~> 6" ./Gemfile.lock # 空欄でOK rails new APIモードで作成します。 $ docker-compose run api rails new . --force --no-deps -d mysql --api database.ymlを編集 デフォルトの状態だとデータベースとの接続ができないので「database.yml」の一部を書き換えます。 ./config/database.yml default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password # デフォルトだと空欄になっているはずなので変更 host: db # デフォルトだとlocalhostになっているはずなので変更 development: <<: *default database: myapp_development test: <<: *default database: myapp_test production: <<: *default database: <%= ENV["DATABASE_NAME"] %> username: <%= ENV["DATABASE_USERNAME"] %> password: <%= ENV["DATABASE_PASSWORD"] %> コンテナを起動 & データベースを作成 $ docker-compose build $ docker-compose up -d $ docker-compose run api bundle exec rails db:create localhost:3001 にアクセス localhost:3001 にアクセスして初期状態の画面が表示されればOKです。 gemをインストール 後々の処理で必要になるgemをインストールしておきます。 ./Gemfile gem 'faraday' gem 'syobocal' gem 'dotenv-rails' fadaday HTTPクライアント用のgem syobocal しょぼいカレンダーから情報を取得しやすくしてくれるgem dotenv-rails 環境変数を管理するためのgem Gemfileを更新したので再度ビルド。 $ docker-compose build 各種モデルを作成 $ docker-compose run api rails g model Work title:string year:integer season:integer image:string twitter_username:string official_site_url:string media_text:string season_name_text:string syobocal_tid:integer $ docker-compose run api rails g model WorkDetail work_id:integer staffs:text casts:text syobocal_tid:integer $ docker-compose run api rails db:migrate Work(作品) ※ Annictから取得する情報 title 作品タイトル year 放送年 season 季節 image 作品イメージ twitter_username Twitterアカウント名 official_site_url 公式サイトURL media_text どのメディアで放送か(TV、映画、OVAなど) syobocal_tid しょぼいカレンダーのTID WorkDetail(作品の詳細)※ しょぼいカレンダーから取得する情報 work_id Workモデルとの関連付け用 staffs 製作陣 casts キャスト陣 syobocal_tid しょぼいカレンダーのTID ./app/models/work.rb class Work < ApplicationRecord enum season: { spring: 1, summer: 2, autumn: 3, winter: 4 } has_one :work_detail # Annictから情報を取得 def import_from_annict base_url = "https://api.annict.com/v1" access_token = ENV["ANNICT_ACCESS_TOKEN"] start_year = 1970 # どの年からデータを取得したいかを指定 end_year = Date.today.year seasons = ["spring", "summer", "autumn", "winter"] (start_year..end_year).each do |year| seasons.each.with_index(1) do |season, index| # 初回リクエストはデータの総数を調べるために実行 data = JSON.parse(Faraday.get("#{base_url}/works?fields=id&per_page=50&filter_season=#{year}-#{season}&sort_watchers_count=desc&access_token=#{access_token}").body) data_count = data["total_count"] # データの数 page_count = (data_count / 50.to_f).ceil # ページの数 current_page = 1 # 現在のページ <= ページの数になるまで繰り返し処理を実行 while current_page <= page_count do data = JSON.parse(Faraday.get("#{base_url}/works?fields=title,images,twitter_username,official_site_url,media_text,syobocal_tid,season_name_text&page=#{current_page}&per_page=50&filter_season=#{year}-#{season}&sort_watchers_count=desc&access_token=#{access_token}").body) works = data["works"] works.each do |work| # すでにレコードが存在する場合は更新、無ければ新規作成 Work.find_or_initialize_by(title: work["title"]).update( year: year, season: index, image: work["images"]["recommended_url"], twitter_username: work["twitter_username"], official_site_url: work["official_site_url"], media_text: work["media_text"], syobocal_tid: work["syobocal_tid"], season_name_text: work["season_name_text"] ) end current_page += 1 end end end end end ./app/models/work_detail.rb class WorkDetail < ApplicationRecord serialize :staffs, Array serialize :casts, Array belongs_to :work # しょぼいカレンダーから情報を取得 def import_from_syobocal titles = Syobocal::DB::TitleLookup.get({ "TID" => "*" }) titles.each do |title| comment = title[:comment] parser = Syobocal::Comment::Parser.new(comment) staffs = [] # 製作陣 casts = [] # キャスト陣 parser.staffs.each do |staff| staffs << { "role": staff.instance_variable_get("@role"), "name": staff.instance_variable_get("@people")[0].instance_variable_get("@name") } end parser.casts.each do |cast| casts << { "character": cast.instance_variable_get("@character"), "name": cast.instance_variable_get("@people")[0].instance_variable_get("@name") } end tid = title[:tid] work = Work.find_by(syobocal_tid: tid) # すでにレコードが存在する場合は更新、無ければ新規作成 WorkDetail.find_or_initialize_by(syobocal_tid: tid).update( work_id: work ? work.id : nil, staffs: staffs, casts: casts ) end end end 各情報をデータベースにインポート Annict、しょぼいカレンダーから各情報をデータベースにインポートします。ただし、Annictに関してはAPIを利用するためのアクセストークンが必要になるので、公式ドキュメントの手順に従い事前に取得しておいてください。 Annict API 公式ドキュメント アクセストークンが取得できたら、ルートディレクトリに「.env」ファイルを作成してそこに環境変数としてセットします。 $ touch .env .env ANNICT_ACCESS_TOKEN=*********************** その後、Railsコンソールを立ち上げてそれぞれインポートを開始してください。 $ docker-compose run api rails c irb(main):001:0> Work.new.import_from_annict Work Load (0.6ms) SELECT `works`.* FROM `works` WHERE `works`.`title` = 'あしたのジョー' LIMIT 1 TRANSACTION (0.4ms) BEGIN Work Create (0.6ms) INSERT INTO `works` (`title`, `year`, `season`, `image`, `twitter_username`, `official_site_url`, `media_text`, `season_name_text`, `created_at`, `updated_at`) VALUES ('あしたのジョー', 1970, 1, '', '', '', 'TV', '1970年春', '2021-07-23 16:04:36.882115', '2021-07-23 16:04:36.882115') TRANSACTION (2.2ms) COMMIT ... irb(main):002:0> WorkDetail.new.import_from_syobocal Work Load (4.5ms) SELECT `works`.* FROM `works` WHERE `works`.`syobocal_tid` = 1 LIMIT 1 WorkDetail Load (0.5ms) SELECT `work_details`.* FROM `work_details` WHERE `work_details`.`syobocal_tid` = 1 LIMIT 1 TRANSACTION (0.3ms) BEGIN Work Load (2.0ms) SELECT `works`.* FROM `works` WHERE `works`.`id` = 2194 LIMIT 1 WorkDetail Create (0.8ms) INSERT INTO `work_details` (`work_id`, `staffs`, `casts`, `syobocal_tid`, `created_at`, `updated_at`) VALUES (2194, '---\n- :role: 監督\n :name: 下田正美\n- :role: 原作・脚本\n :name: 山田典枝\n- :role: 掲載\n :name: 月刊コミックドラゴン\n- :role: キャラクター原案\n :name: よしづきくみち\n- :role: キャラクターデザイン\n :name: 千葉道徳\n- :role: 総作画監督\n :name: 川崎恵子\n- :role: コンセプト・ワークス\n :name: 横田耕三\n- :role: 美術監督\n :name: 西川淳一郎\n- :role: 色彩設定\n :name: 石田美由紀\n- :role: 撮影監督\n :name: 秋元央\n- :role: 編集\n :name: 西山茂\n- :role: 音響監督\n :name: 田中英行\n- :role: 音楽\n :name: 羽毛田丈史\n- :role: 音楽プロデューサー\n :name: 廣井紀彦\n- :role: 音楽ディレクター\n :name: 和田亨\n- :role: 音楽協力\n :name: テレビ朝日ミュージック\n- :role: 録音調整\n :name: 小原吉男\n- :role: 音響効果\n :name: 今野康之\n- :role: 選曲\n :name: 神保直史\n- :role: 録音助手\n :name: 国分政嗣\n- :role: 録音スタジオ\n :name: タバック\n- :role: 音響制作\n :name: オーディオ・タナカ\n- :role: キャスティング協力\n :name: 好永伸恵\n- :role: ポストプロダクション\n :name: 東京現像所\n- :role: 広報\n :name: 小出わかな\n- :role: 宣伝プロデュース\n :name: 小林 剛\n- :role: アシスタントプロデューサー\n :name: 佐々木美和\n- :role: プロデューサー\n :name: 清水俊\n- :role: アニメーションプロデューサー\n :name: 新崎力也\n- :role: 企画\n :name: 角川大映\n- :role: アニメーション制作\n :name: ヴューワークス\n- :role: 制作\n :name: 魔法局\n', '---\n- :character: 菊池ユメ\n :name: 宮﨑あおい\n- :character: 小山田雅美\n :name: 諏訪部順一\n- :character: ケラ(加藤剛)\n :name: 飯田浩志\n- :character: アンジェラ\n :name: 渡辺明乃\n- :character: 遠藤耕三\n :name: 中博史\n- :character: 古崎力哉\n :name: 清川元夢\n- :character: 森川瑠奈\n :name: 石毛佐和\n- :character: ギンプン\n :name: 辻谷耕史\n- :character: ミリンダ\n :name: 平松晶子\n', 1, '2021-07-23 16:12:43.543292', '2021-07-23 16:12:43.543292') TRANSACTION (2.2ms) COMMIT ... 最終的にこんな感じでそれぞれの情報が格納されていれば成功です。(※ 古い作品などは空欄になってしまう箇所多し) APIを作成 データベースに格納した情報をJSON形式で返すAPIを作成します。 コントローラー $ docker-compose run api rails g controller api/v1/works ./app/controllers/api/v1/works_controller.rb class Api::V1::WorksController < ApplicationController def index return if !params[:year] && !params[:season] && !params[:title] works = [] # paramsによって絞り込みの条件を変更 queried_works = params[:year] && params[:season] ? Work.where(year: params[:year], season: params[:season]) : Work.where("title like ?", "%#{params[:title]}%") queried_works.each do |work| works << { id: work.id, # ID title: work.title, # 作品のタイトル year: work.year, # 年 season: work.season, # 季節 image: work.image, # 画像 staffs: work.work_detail ? work.work_detail.staffs : [], # 製作陣 casts: work.work_detail ? work.work_detail.casts : [], # キャスト陣 twitter_username: work.twitter_username, # Twitterのユーザー名 official_site_url: work.official_site_url, # 公式サイトのURL media_text: work.media_text, # ex. TV、映画、OVA、Web season_name_text: work.season_name_text # ex. 2021年春 } end render json: { status: 200, works: works } end end クエリパラメータに「year」と「season」が含まれていた場合は放送タイミングで作品を絞り込み、「title」が含まれていた場合は合致するタイトルの作品を絞り込むようにしました。 なお、Annictから取得した情報(Work)には製作陣(staffs)やキャスト陣(staffs)が含まれていなかったため、しょぼいカレンダーから取得した情報(WorkDetail)をプラスして情報の網羅性を高めています。 ルーティング ./backend/config/routes.rb Rails.application.routes.draw do namespace :api do namespace :v1 do resources :works, only: %i[index] end end end 動作確認 $ curl -X GET http://localhost:3001/api/v1/works?year=2021&season=3 $ curl -X GET http://localhost:3001/api/v1/works?title=小林さんちのメイドラゴンS curlコマンドを叩くなり、直接URLを打ち込んでアクセスするなりしてJSONが返ってくればOK。 CORS設定 今回の構成ではバックエンドとフロントエンドを完全に分けているため、RailsとReactがそれぞれ別のドメインで立ち上がっています。(localhost:3001とlocalhost:3000) この場合、デフォルトの状態だとセキュリティの問題でReactからRailsのAPIを使用できない点に注意が必要です。 これを解決するためには「CORS(クロス・オリジン・リソース・シェアリング)」の設定を行わなければなりません。 参照記事: オリジン間リソース共有 (CORS) rack-corsをインストール RailsにはCORSの設定を簡単に行えるgemがあるのでそちらをインストールしましょう。 ./Gemfile gem 'rack-cors' APIモードで作成している場合、すでにGemfile内に記載されているのでコメントアウトを外すだけでOKです。 $ docker-compose build Gemfileを更新したので再度ビルド。 cors.rbを編集 「config/initializers/」に設定ファイルが存在するはずなので、外部からアクセス可能なように編集しておきます。 ./backend/config/initializers/cors.rb Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins "*" resource "*", headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end 設定の変更を反映させるためにコンテナを再起動。 $ docker-compose down $ docker-compose up -d これでバックエンド側の準備は完了です。 フロントエンド 次にフロントエンド側の実装に入ります。 環境構築 何はともあれ環境構築を行います。 各種ディレクトリ・ファイルを作成 おなじみの「create-react-app」でアプリの雛形を作ります。 $ mkdir aninfo-frontend && cd aninfo-frontend $ yarn create react-app . --template typescript tsconfig.jsonを修正 「./tsconfig.json」内に「"baseUrl": "src"」という1行を追記してください。 ./tsconfig.json { "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, ...省略... "baseUrl": "src" ← 追記 }, "include": [ "src" ] } これにより、インポート先を「./tsconfig.json」が配置されているディレクトリから相対パスで指定できるようになるので非常に楽です。 baseUrlを指定しない場合 import Hoge from "../../components/Hoge" // 呼び出すファイルからの相対パス baseUrlを指定した場合 import Hoge from "components/Hoge" // baseUrlからの相対パス いちいち「../../」みたいな記述をしなくて済むというわけですね。 不要なファイルを整理 この先使う事の無いファイルは邪魔なので今のうちに消しておきましょう。 $ rm src/App.css src/App.test.tsx src/logo.svg src/reportWebVitals.ts src/setupTests.ts 「./src/index.tsx」と「./src/App.tsx」を次のように変更します。 ./src/index.tsx import React from "react" import ReactDOM from "react-dom" import "./index.css" import App from "./App" ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") ) ./src/App.tsx import React from "react" const App: React.FC = () => { return ( <h1>Hello World!</h1> ) } export default App 一旦、動作確認してみましょう。 $ yarn start localhost:3000 にアクセスして「Hello World!」と返ってくればOK。 型定義 プロジェクト全体で使い回す事になるであろう型(今回であればWork)を「./src/interfaces/index.ts」の中に記述しておきます。 $ mkdir src/interfaces $ touch src/interfaces/index.ts ./src/interfaces/index.ts export interface Work { id: number title: string year: number season: number image?: string staffs?: Array<{ role: string name: string }> casts?: Array<{ character: string name: string }> twitterUsername?: string officialSiteUrl?: string mediaText: string seasonNameText: string } APIを呼び出すための関数を作成 Railsで作成したAPIを呼び出すための関数を作成します。 $ mkdir src/lib $ mkdir src/lib/api $ touch src/lib/api/client.ts $ touch src/lib/api/works.ts $ touch .env.local $ yarn add axios axios-case-converter $ yarn add -D @types/axios axios HTTPクライアント用ライブラリ @types/axios 型定義用ライブラリ axios-case-converter axios経由で受け取るレスポンスの値をスネークケース→キャメルケースに変換、または送信するリクエストの値をキャメルケース→スネークケースに変換してくれるライブラリ ./src/lib/api/client.ts import applyCaseMiddleware from "axios-case-converter" import axios from "axios" /* applyCaseMiddleware axiosで受け取ったレスポンスの値をスネークケース→キャメルケースに変換 または送信するリクエストの値をキャメルケース→スネークケースに変換 */ const railsApiBaseUrl = process.env.REACT_APP_RAILS_API_BASE_URL const client = applyCaseMiddleware(axios.create({ baseURL: `${railsApiBaseUrl}/api/v1` })) export default client 慣習的にRubyなどの言語がスネークケースが基本であるのに対し、JavaScriptはキャメルケースが基本なので、足並みを揃える(スネークケース→キャメルケースへの変換もしくはその逆)ために「applyCaseMiddleware」というライブラリを使わせてもらっています。 .env.local REACT_APP_RAILS_API_BASE_URL=http://localhost:3001 ※ Reactで環境変数を使用する場合、環境変数名の先頭にREACT_APP_を付ける必要があるので注意。 動作確認 ./src/App.tsx import React, { useEffect, useState } from "react" import { getWorks } from "lib/api/works" import { Work } from "interfaces/index" const App: React.FC = () => { const [works, setWorks] = useState<Work[]>() const handleGetWorks = async (year?: number, season?: number, title?: string): Promise<void> => { const res = await getWorks(year, season, title) if (res.status === 200) { setWorks(res.data.works) } } useEffect(() => { handleGetWorks(2021, 3) // 20201年夏季のアニメ情報を取得 }, []) return ( <React.Fragment> { works?.map((work: Work) => ( <p>{work.title}</p> )) } </React.Fragment> ) } export default App localhost:3000 にアクセスして作品タイトルがズラーっと表示されていればOK。ちゃんと通信ができています。 各種ライブラリをインストール 後に必要となるライブラリをインストールしておきます。 $ yarn add @material-ui/core @material-ui/icons @material-ui/lab react-select $ yarn add -D @types/react-select material-ui UIを整える用のライブラリ react-select セレクトボックスが簡単に作れるライブラリ @types/react-select 型定義用ライブラリ 各種ビューを作成 各種ビューを作成します。 $ mkdir src/components $ mkdir src/components/layouts $ mkdir src/components/utils $ mkdir src/components/work $ touch src/components/layouts/Header.tsx $ touch src/components/utils/theme.ts $ touch src/components/work/SelectBox.tsx $ touch src/components/work/WorkDetail.tsx $ touch src/components/work/Works.tsx ./src/components/layouts/Header.tsx import React, { useState } from "react" import AppBar from "@material-ui/core/AppBar" import Toolbar from "@material-ui/core/Toolbar" import IconButton from "@material-ui/core/IconButton" import Typography from "@material-ui/core/Typography" import InputBase from "@material-ui/core/InputBase" import { alpha, makeStyles } from "@material-ui/core/styles" import MenuIcon from "@material-ui/icons/Menu" import SearchIcon from "@material-ui/icons/Search" const useStyles = makeStyles((theme) => ({ root: { flexGrow: 1 }, menuButton: { marginRight: theme.spacing(2) }, title: { flexGrow: 1, display: "none", [theme.breakpoints.up("sm")]: { display: "block" } }, search: { position: "relative", borderRadius: theme.shape.borderRadius, backgroundColor: alpha(theme.palette.common.white, 0.15), "&:hover": { backgroundColor: alpha(theme.palette.common.white, 0.25), }, marginLeft: 0, width: "100%", [theme.breakpoints.up("sm")]: { marginLeft: theme.spacing(1), width: "auto", } }, searchIcon: { padding: theme.spacing(0, 2), height: "100%", position: "absolute", pointerEvents: "none", display: "flex", alignItems: "center", justifyContent: "center" }, inputRoot: { color: "inherit" }, inputInput: { padding: theme.spacing(1, 1, 1, 0), paddingLeft: `calc(1em + ${theme.spacing(4)}px)`, transition: theme.transitions.create("width"), width: "100%", [theme.breakpoints.up("sm")]: { width: "12ch", "&:focus": { width: "20ch", } } } })) interface HeaderProps { handleGetWorks: Function setLoading: Function } const Header: React.FC<HeaderProps> = ({ handleGetWorks, setLoading }) => { const classes = useStyles() const [title, setTitle] = useState<string>("") return ( <div className={classes.root}> <AppBar position="static"> <Toolbar> <IconButton edge="start" className={classes.menuButton} color="inherit" aria-label="open drawer" > <MenuIcon /> </IconButton> <Typography className={classes.title} variant="h6" noWrap> AnInfo </Typography> <div className={classes.search}> <div className={classes.searchIcon}> <SearchIcon /> </div> <InputBase placeholder="作品名で検索" classes={{ root: classes.inputRoot, input: classes.inputInput, }} inputProps={{ "aria-label": "search" }} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setTitle(e.target.value) console.log(title) }} onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === "Enter") { setLoading(true) handleGetWorks(null, null, title) } }} /> </div> </Toolbar> </AppBar> </div> ) } export default Header ./src/components/utils/theme.ts import { createTheme } from "@material-ui/core/styles" import blue from "@material-ui/core/colors/blue" import green from '@material-ui/core/colors/green' const theme = createTheme({ palette: { primary: { main: blue[500] }, secondary: { main: green[500] } }, typography: { h1: { fontSize: "3rem", fontWeight: 500 }, h2: { fontSize: "2rem", fontWeight: 500 }, h3: { fontSize: "1.25rem", fontWeight: 500 }, h4: { fontSize: "1rem", fontWeight: 500 } } }) export default theme ./src/components/work/SelectBox.tsx import React, { useState } from "react" import Select, { OptionTypeBase } from "react-select" import { makeStyles } from "@material-ui/core/styles" import Grid from "@material-ui/core/Grid" import IconButton from "@material-ui/core/IconButton" import SearchIcon from "@material-ui/icons/Search" import FormControl from "@material-ui/core/FormControl" const useStyles = makeStyles(() => ({ gridContainer: { marginBottom: "2rem" }, iconButton: { padding: 10 }, formControl: { margin: "3px", minWidth: 130 } })) interface SelectBoxProps { years: Array<{ value: number label: string }> seasons: Array<{ value: number label: string }> handleGetWorks: Function setLoading: Function } const SelectBox: React.FC<SelectBoxProps> = ({ years, seasons, handleGetWorks, setLoading }) => { const classes = useStyles() const [year, setYear] = useState<number>() const [season, setSeason] = useState<number>() return ( <Grid className={classes.gridContainer} container justifyContent="center"> <FormControl className={classes.formControl}> <Select instanceId="year-select" placeholder="年" options={years} onChange={(e) => { setYear(e?.value) }} /> </FormControl> <FormControl className={classes.formControl}> <Select instanceId="season-select" placeholder="シーズン" options={seasons} onChange={(e) => { setSeason(e?.value) }} /> </FormControl> <IconButton type="submit" className={classes.iconButton} size="medium" color="default" disabled={!year || !season} onClick={() => { setLoading(true) handleGetWorks(year, season) }} > <SearchIcon /> </IconButton> </Grid> ) } export default SelectBox ./src/components/work/WorkDetail.tsx import { createStyles, Theme, withStyles, WithStyles } from "@material-ui/core/styles" import Button from "@material-ui/core/Button" import Dialog from "@material-ui/core/Dialog" import MuiDialogTitle from "@material-ui/core/DialogTitle" import MuiDialogContent from "@material-ui/core/DialogContent" import MuiDialogActions from "@material-ui/core/DialogActions" import Typography from "@material-ui/core/Typography" import IconButton from "@material-ui/core/IconButton" import CloseIcon from "@material-ui/icons/Close" import { Work } from "interfaces/index" import React from "react" const styles = (theme: Theme) => createStyles({ root: { margin: 0, padding: theme.spacing(2), }, closeButton: { position: "absolute", right: theme.spacing(1), top: theme.spacing(1), color: theme.palette.grey[500] } }) export interface DialogTitleProps extends WithStyles<typeof styles> { id: string children: React.ReactNode onClose: () => void } const DialogTitle = withStyles(styles)((props: DialogTitleProps) => { const { children, classes, onClose, ...other } = props return ( <MuiDialogTitle disableTypography className={classes.root} {...other}> <Typography variant="h6">{children}</Typography> { onClose ? ( <IconButton aria-label="close" className={classes.closeButton} onClick={onClose}> <CloseIcon /> </IconButton> ) : null } </MuiDialogTitle> ) }) const DialogContent = withStyles((theme) => ({ root: { padding: theme.spacing(2), } }))(MuiDialogContent) const DialogActions = withStyles((theme) => ({ root: { margin: 0, padding: theme.spacing(1), } }))(MuiDialogActions) interface WorkDetailsProps { work: Work open: boolean handleClose: () => void } const WorkDetail: React.FC<WorkDetailsProps> = ({ work, open, handleClose }) => { return ( work.staffs != undefined && work.casts != undefined ? ( <Dialog onClose={handleClose} open={open} fullWidth> <DialogTitle id="customized-dialog-title" onClose={handleClose}> {work.title} </DialogTitle> <DialogContent dividers> <Typography variant="h4" gutterBottom> Staffs </Typography> { work.staffs.length > 1 ? work.staffs.map((staff, index: number) => ( <Typography key={index} variant="body2" gutterBottom> {staff.role}: {staff.name} </Typography> )) : <Typography variant="body2" gutterBottom>情報を取得できませんでした。</Typography> } <Typography variant="h4" gutterBottom style={{ marginTop: "1rem" }}> Casts </Typography> { work.casts.length > 1 ? work.casts.map((cast, index: number) => ( <Typography key={index} variant="body2" gutterBottom> {cast.character}: {cast.name} </Typography> )) : <Typography variant="body2" gutterBottom>情報を取得できませんでした。</Typography> } </DialogContent> <DialogActions> <Button autoFocus onClick={handleClose} color="primary"> 閉じる </Button> </DialogActions> </Dialog> ) : null ) } export default WorkDetail ./src/components/work/Works.tsx import React, { useState } from "react" import { makeStyles } from "@material-ui/core/styles" import Grid from "@material-ui/core/Grid" import Card from "@material-ui/core/Card" import CardMedia from "@material-ui/core/CardMedia" import CardContent from "@material-ui/core/CardContent" import CardActions from "@material-ui/core/CardActions" import Chip from "@material-ui/core/Chip" import CircularProgress from "@material-ui/core/CircularProgress" import Typography from "@material-ui/core/Typography" import WorkDetail from "components/work/WorkDetail" import { Work } from "interfaces/index" const useStyles = makeStyles(() => ({ circularProgress: { position: "absolute", top: "50%", left: "50%" }, card: { height: "100%", width: "100%", marginBottom: "0.5rem", transition: "all 0.3s", "&:hover": { boxShadow: "1px 0px 20px -1px rgba(0,0,0,0.2), 0px 0px 20px 5px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12)", transform: "translateY(-3px)", } }, cardMedia: { aspectRatio: "16/9", cursor: "pointer" }, cardActions: { marginTop: "0.5rem" } })) interface WorksProps { loading: boolean works: Work[] } const initialWorkState: Work = { id: 0, title: "", year: 0, season: 0, image: "string", staffs: [], casts: [], twitterUsername: "", officialSiteUrl: "", mediaText: "", seasonNameText: "", } const Works: React.FC<WorksProps> = ({ loading, works}) => { const classes = useStyles() const [open, setOpen] = useState(false) const [work, setWork] = useState<Work>(initialWorkState) const handleOpen = () => { setOpen(true) } const handleClose = () => { setOpen(false) } return ( <React.Fragment> <Grid container spacing={4}> <WorkDetail work={work} open={open} handleClose={handleClose} /> { loading ? <CircularProgress className={classes.circularProgress}/> : works != null && works.length >= 1 && works.map((work) => ( <Grid item key={work.id} xs={12} sm={6} md={4}> <Card className={classes.card}> <CardMedia component="img" className={classes.cardMedia} // 画像がなかった場合は「NO IMAGE」を表示(各自用意してpublicディレクトリ以下に配置) src={work.image ? work.image : "/no_image.png"} onError={(e: any) => { e.target.src = "/no_image.png" }} onClick={() => { handleOpen() setWork(work) }} /> <CardActions className={classes.cardActions}> { work.seasonNameText != null && ( <Chip label={work.seasonNameText} variant="outlined" /> ) } { work.mediaText != null && ( <Chip label={work.mediaText} variant="outlined" /> ) } { work.officialSiteUrl != null && ( <Chip label="公式サイト" component="a" rel="noopener noreferrer" href={work.officialSiteUrl} target="_blank" clickable color="secondary" variant="outlined" /> ) } { work.twitterUsername != null && ( <Chip label="Twitter" component="a" rel="noopener noreferrer" href={`https://twitter.com/${work.twitterUsername}`} target="_blank" clickable color="primary" variant="outlined" /> ) } </CardActions> <CardContent> <Typography variant="h3" gutterBottom> {work.title} </Typography> </CardContent> </Card> </Grid> )) } </Grid> </React.Fragment> ) } export default Works ./src/App.tsx import React, { useEffect, useState } from "react" import { makeStyles, ThemeProvider } from "@material-ui/core/styles" import Container from "@material-ui/core/Container" import Header from "components/layouts/Header" import Works from "components/work/Works" import SelectBox from "components/work/SelectBox" import theme from "components/utils/theme" import { getWorks } from "lib/api/works" import { Work } from "interfaces/index" const useStyles = makeStyles(() => ({ container: { marginTop: "2rem" } })) const years: Array<{ value: number label: string }> = [] // 現在の年を取得 const currentYear: number = new Date().getFullYear() for (var y = currentYear; y >= 1970; y--) { years.push({ value: y, label: `${y}` }) } const seasons: Array<{ value: number label: string }> = [ { value: 1, label: "春" }, { value: 2, label: "夏" }, { value: 3, label: "秋" }, { value: 4, label: "冬" } ] // 現在の季節を取得 const currentSeason: number = seasons[(Math.ceil((new Date().getMonth() +1 ) / 3)) - 2].value const App: React.FC = () => { const classes = useStyles() const [loading, setLoading] = useState<boolean>(true) const [works, setWorks] = useState<Work[]>([]) const handleGetWorks = async (year?: number, season?: number, title?: string): Promise<void> => { const res = await getWorks(year, season, title) if (res.status === 200) { setWorks(res.data.works) } setLoading(false) } // デフォルトでは現在の年・季節の作品を取得 useEffect(() => { handleGetWorks(currentYear, currentSeason) }, []) return ( <React.Fragment> <ThemeProvider theme={theme}> <Header handleGetWorks={handleGetWorks} setLoading={setLoading}/> <Container className={classes.container} maxWidth="lg"> <SelectBox years={years} seasons={seasons} handleGetWorks={handleGetWorks} setLoading={setLoading} /> <Works works={works} loading={loading}/> </Container> </ThemeProvider> </React.Fragment> ) } export default App 動作確認 最終的にこんな感じになっていれば完成です。 番外編(データベースの定期更新) ここから先は番外編なので興味の無い人は読み飛ばしてOKです。 もし今回作成したアプリを本格的に使い続けたい場合、定期的に情報を更新するバッチ処理などを実装する必要があるでしょう。(アニメ作品はこれからも続々と追加されていくため) そこで一応、データベースの定期更新について自分なりの手順を記しておきます。 sidekiqをインストール 今回はRailsアプリに定期実行を組み込む際に定番の sidekiq というgemを使っていきたいと思います。 ./Gemfile gem 'sidekiq' gem 'sidekiq-cron' Gemfileを更新したので再度ビルド。 $ docker-compose build Workerクラスを作成 定期実行用のWorkerクラスを作成します。 $ docker-compose run api rails g sidekiq:worker Test $ docker-compose run api rails g sidekiq:worker WorkImport $ docker-compose run api rails g sidekiq:worker WorkDetailImport ./app/workers/test_worker.rb class TestWorker include Sidekiq::Worker # 動作確認用 def perform puts "Hello World!" end end ./app/workers/work_import_worker.rb class WorkImportWorker include Sidekiq::Worker # Annictから情報を取得 def perform Work.new.import_from_annict end end ./app/workers/work_detail_import_worker.rb class WorkDetailImportWorker include Sidekiq::Worker # しょぼいカレンダーから情報を取得 def perform WorkDetail.new.import_from_syobocal end end 各種設定 $ touch config/initializers/sidekiq.rb config/sidekiq.yml config/schedule.yml ./config/initializers/sidekiq.rb # Redisの設定 Sidekiq.configure_server do |config| config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379") } end Sidekiq.configure_client do |config| config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379")} end # どのタイミングで定期実行を行うかを記述したファイルを読み込む schedule_file = "config/schedule.yml" if File.exist?(schedule_file) && Sidekiq.server? Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file) end ./config/sidekiq.yml :verbose: false :pidfile: ./tmp/pids/sidekiq.pid :concurrency: 25 :queues: - default ./config/schedule.yml test: cron: "*/5 * * * *" # 5分おきに実行 class: "TestWorker" queue: default work_import: cron: "0 0 * * 1" # 毎週月曜日の午前0時に実行 class: "WorkImportWorker" queue: default work_detail_import: cron: "0 0 * * 1" # 毎週月曜日の午前0時に実行 class: "WorkDetailImportWorker" queue: default ./config/application.rb # 以下3行を適当な場所に追記(sidekiqのダッシュボードを見るために必要) # https://edgeguides.rubyonrails.org/api_app.html#using-session-middlewares config.session_store :cookie_store, key: '_interslice_session' config.middleware.use ActionDispatch::Cookies config.middleware.use config.session_store, config.session_options ./config/routes.rb require "sidekiq/web" require "sidekiq/cron/web" Rails.application.routes.draw do mount Sidekiq::Web, at: "/sidekiq" # ダッシュボードへのルーティング namespace :api do namespace :v1 do resources :test, only: %i[index] resources :works, only: %i[index] end end end ./docker-compose.yml version: "3" services: db: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: password volumes: - mysql-data:/var/lib/mysql - /tmp/dockerdir:/etc/mysql/conf.d/ ports: - 4306:3306 api: build: context: . dockerfile: Dockerfile command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: - .:/myapp - ./vendor/bundle:/myapp/vendor/bundle environment: TZ: Asia/Tokyo RAILS_ENV: development REDIS_URL: redis://redis:6379 # 追記 ports: - "3001:3000" depends_on: - db redis: # 追記 image: redis:6.0-alpine volumes: - redis:/data command: redis-server --appendonly yes worker: # 追記 build: . environment: RAILS_ENV: development REDIS_URL: redis://redis:6379 volumes: - .:/myapp depends_on: - redis command: bundle exec sidekiq -C config/sidekiq.yml volumes: mysql-data: redis: # 追記 動作確認 設定の変更を反映させるためにコンテナを再起動させます。 $ docker-compose down $ docker-compose up -d http://localhost:3001/sidekiq にアクセスして良い感じのダッシュボードが表示されればOK。 「cron」タブを開いてみると、先ほど作成した定期実行がスケジューリングされています。 $ docker-compose logs -f worker worker_1 | 2021-07-23T20:37:18.682Z pid=1 tid=go7s74505 INFO: Cron Jobs - add job with name: work_detail_import worker_1 | 2021-07-23T20:37:20.715Z pid=1 tid=go7s74505 INFO: Booted Rails 6.1.4 application in development environment worker_1 | 2021-07-23T20:37:20.715Z pid=1 tid=go7s74505 INFO: Running in ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux] worker_1 | 2021-07-23T20:37:20.715Z pid=1 tid=go7s74505 INFO: See LICENSE and the LGPL-3.0 for licensing details. worker_1 | 2021-07-23T20:37:20.715Z pid=1 tid=go7s74505 INFO: Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org worker_1 | 2021-07-23T20:40:07.738Z pid=1 tid=go7ssy92d class=TestWorker jid=be181a712bacc7ece7d0d7c7 INFO: start worker_1 | Hello World! worker_1 | 2021-07-23T20:40:10.093Z pid=1 tid=go7ssy92d class=TestWorker jid=be181a712bacc7ece7d0d7c7 elapsed=2.353 INFO: done worker_1 | 2021-07-23T20:45:18.099Z pid=1 tid=go7sopr55 class=TestWorker jid=82dccac35bf6b3b56cd2d701 INFO: start worker_1 | Hello World! worker_1 | 2021-07-23T20:45:18.103Z pid=1 tid=go7sopr55 class=TestWorker jid=82dccac35bf6b3b56cd2d701 elapsed=0.004 INFO: done 「docker-compose logs」コマンドでログを確認し、5分おきに「Hello World!」と出力されれば無事動いていると考えて大丈夫です。その他も時が来ればしっかりと実行されるはず。 あとがき 以上、Annict様としょぼいカレンダー様の力を借りて俺流アニメデータベースを作ってみました。 やはり自分で作ったアプリというのは愛着が湧くものなので、今後視聴するアニメを選ぶ際などに利用したいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】アプリの作成方法

Rubyのアプリの作成方法について記載します。 この記事でわかること ・Rubyのアプリの作成方法 手順 ①ターミナルを開く ②% 「cd」を記入する % cd アプリを作成したい場所を指定する。 僕はcdでホームディレクトリに戻って、作成したい場所としてカレントディレクトリを選択しちゃいます。(わかりやすいから) ③「rails new アプリ名 -d mysql」を記入する % rails new アプリ名 -d mysql アプリ名は自分で決めたやつでOK ③まで終わればアプリケーションが作成できているはずです。 おまけ 僕のTwitter:https://twitter.com/honaki_days
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む