- 投稿日:2019-07-08T23:32:28+09:00
カスタムバリデーションで他のモデルのカラムを使用する
About
カスタムバリデーションで他のモデルのカラムを使いたい場合の記載方法について記載しています。
Environment
この記事ではmacbook(unix)にインストールしたruby 2.5.1p57, Rails 5.2.3を使用しています。
Validationとは
「バリデーション」とは、「検証、実証、認可、妥当性」を意味する英単語
「質的な良し悪し」を判断するのではなく、「システム的な適合不適合」を判断するための言葉小難しく書きましたが、意図しないデータをDBに登録できない様、バリデーションをかけることが一般的であるそうです。
カスタムバリデーション
通常、バリデーションは以下の様な形式で設定しますが、複雑なバリデーションを設定したい場合、自作のバリデーション(= カスタムバリデーション)を作成します。
通常class User < ApplicationRecord validates :user_id, presence: true validates :email, presence: true end基本的には、以下の様に定義したメソッド名をvalidate に続けて記載することで、設定可能です。
カスタムバリデーションclass User < ApplicationRecord validates :user_id, presence: true validates :email, presence: true validate :if_user_does_not_have_nickname def if_user_does_not_have_nickname return if nickname.present? errors.add(:nickname, "Nickname is absent") end end他のモデルのカラムも組み合わせたい場合
validationを考えていると、他のモデルのカラムを条件として加えたい時があるかと思います。
その様な場合、モデル間でアソシエーションを組むことで他のモデルのカラムを使用することができます。(例)睡眠時間を記録するアプリ
・日付がデータとして渡されている
・もしユーザの睡眠記録が一つもなければ、睡眠記録の新規作成が可能
・睡眠記録がすでにある場合は、渡されたデータが既存の睡眠記録の最終日付の翌日にマッチしているかどうか調べる
・ミスマッチの場合、渡されたデータを新規登録しないsleep.rubyclass Sleep < ApplicationRecord # アソシエーションの設定。読み込み順の関係で、カスタムバリデーションより必ず上に記載してください。 belongs_to :user validates :slept_time, presence: true validates :wakeup_time, presence: true # カスタムバリデーションの呼び出し(コントローラでnew, createメソッドの場合のみ) validate :dates_cannnot_be_registered_if_there_is_no_yesterdays_date, on: [:new, :create] # カスタムバリデーションの作成 def dates_cannnot_be_registered_if_there_is_no_yesterdays_date # もし日付があり、ユーザの睡眠記録がない場合は処理を抜ける(= そのまま新規登録する。) return if date.present? && user.sleeps.blank? # もし日付がユーザの最新の睡眠記録の日付の翌日でない場合は、データを新規登録しない。 if date != user.sleeps.last.date.tomorrow errors.add(:date, "You can't register that there is no data about before days.") end end end最後に
いかがでしょうか。アソシエーションを組めていれば、意外と簡単に実装できてしまいます。
私はbelongs toの記述位置がカスタムバリデーションの下になっていたため、エラーと何時間も格闘する羽目になりましたが...。
参考になれば幸いです!筆者について
TECH::EXPERTにて4月よりruby, railsを学習している未経験エンジニアです。
記載内容に不備・不足があればご指摘いただけると幸いです。
至らぬ点ばかりですので、改善点がありましたらどんどんご指摘下さい!
- 投稿日:2019-07-08T22:57:32+09:00
deviseでユーザー認証をしよう
deviseでカラムを追加して、ログイン機能を実装するには・・・
userモデルを作成するところまではこちら
https://qiita.com/mokku/private/aa4191817369edb269fb
最低限のログインページ実装は上で完了しています。deviseのビューを生成する
$ rails g devise:views Running via Spring preloader in process 5927 invoke Devise::Generators::SharedViewsGenerator create app/views/devise/shared create app/views/devise/shared/_error_messages.html.erb create app/views/devise/shared/_links.html.erb invoke form_for create app/views/devise/confirmations create app/views/devise/confirmations/new.html.erb create app/views/devise/passwords create app/views/devise/passwords/edit.html.erb create app/views/devise/passwords/new.html.erb create app/views/devise/registrations create app/views/devise/registrations/edit.html.erb create app/views/devise/registrations/new.html.erb create app/views/devise/sessions create app/views/devise/sessions/new.html.erb create app/views/devise/unlocks create app/views/devise/unlocks/new.html.erb invoke erb create app/views/devise/mailer create app/views/devise/mailer/confirmation_instructions.html.erb create app/views/devise/mailer/email_changed.html.erb create app/views/devise/mailer/password_change.html.erb create app/views/devise/mailer/reset_password_instructions.html.erb create app/views/devise/mailer/unlock_instructions.html.erbビューファイルの編集を行う
今回は、追加でニックネームを入力してもらいたいのでニックネームの入力欄を追加します。
※一旦、仮で反映させておきます。new.html.erb<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> <%= render "devise/shared/error_messages", resource: resource %> <div class="field"> <%= f.label :nickname %><br /> <%= f.text_field :nickname, autofocus: true, autocomplete: "email" %> </div>ニックネーム入力欄ができました。
コントローラを作成しよう
ターミナルで下記コマンドを入力してコントローラを作成する
$ rails g controller [コントローラ名] $ rails g controller users参考
deviseのビューファイル
https://qiita.com/yasuno0327/items/ff17ddb6a4167fc6b471
- 投稿日:2019-07-08T22:35:11+09:00
amcharts 4 Demos を使ってグラフを作成(piechart編)
About
amcharts関連の日本語文献のあまりの少なさから、誰かのお役に立てればと思い記載しています!
Environment
この記事ではmacbook(unix)にインストールしたruby 2.5.1p57, Rails 5.2.3を使っています。
amchartsとは
前回の記事をご覧ください!
Piechartとは
円グラフです。今回はこちらを作成します。
導入方法
こちらのサイトのページ内を下にスクロールすると、Demo sourceが置いてありますので、コピペで使用可能です。
基本的にはそのままコードを触らずとも、実装可能です。Demo_source<!-- Styles --> <style> #chartdiv { width: 100%; height: 500px; } </style> <!-- Resources --> <script src="https://www.amcharts.com/lib/4/core.js"></script> <script src="https://www.amcharts.com/lib/4/charts.js"></script> <script src="https://www.amcharts.com/lib/4/themes/animated.js"></script> <!-- Chart code --> <script> am4core.ready(function() { // Themes begin am4core.useTheme(am4themes_animated); // Themes end // Create chart instance var chart = am4core.create("chartdiv", am4charts.PieChart); // Add data chart.data = [ { "country": "Lithuania", "litres": 501.9 }, { "country": "Czech Republic", "litres": 301.9 }, { "country": "Ireland", "litres": 201.1 }, { "country": "Germany", "litres": 165.8 }, { "country": "Australia", "litres": 139.9 }, { "country": "Austria", "litres": 128.3 }, { "country": "UK", "litres": 99 }, { "country": "Belgium", "litres": 60 }, { "country": "The Netherlands", "litres": 50 } ]; // Add and configure Series var pieSeries = chart.series.push(new am4charts.PieSeries()); pieSeries.dataFields.value = "litres"; pieSeries.dataFields.category = "country"; pieSeries.slices.template.stroke = am4core.color("#fff"); pieSeries.slices.template.strokeWidth = 2; pieSeries.slices.template.strokeOpacity = 1; // This creates initial animation pieSeries.hiddenState.properties.opacity = 1; pieSeries.hiddenState.properties.endAngle = -90; pieSeries.hiddenState.properties.startAngle = -90; }); // end am4core.ready() </script> <!-- HTML --> <div id="chartdiv"></div>編集方法
style, htmlについては割愛し、scriptについて筆者がわかる範囲で記載していきます。
index.html.erb<!-- Chart code --> <script> am4core.ready(function() { // Themes begin // テーマです。 am4core.useTheme(am4themes_animated); // Themes end // Create chart instance // 始めにインスタンスを作成します。Piechart形式であることをここで指定しています。 var chart = am4core.create("chartdiv", am4charts.PieChart); // Add data // ここにデータを入力していきます。 chart.data = [ { "country": "Lithuania", "litres": 501.9 }, { "country": "Czech Republic", "litres": 301.9 }, { "country": "Ireland", "litres": 201.1 }, { "country": "Germany", "litres": 165.8 }, { "country": "Australia", "litres": 139.9 }, { "country": "Austria", "litres": 128.3 }, { "country": "UK", "litres": 99 }, { "country": "Belgium", "litres": 60 }, { "country": "The Netherlands", "litres": 50 } ]; // Add and configure Series // 作成したインスタンスに設定を追加していきます。 var pieSeries = chart.series.push(new am4charts.PieSeries()); // データはlitres(リットル) pieSeries.dataFields.value = "litres"; // 単位はcountryです pieSeries.dataFields.category = "country"; pieSeries.slices.template.stroke = am4core.color("#fff"); pieSeries.slices.template.strokeWidth = 2; pieSeries.slices.template.strokeOpacity = 1; // This creates initial animation pieSeries.hiddenState.properties.opacity = 1; pieSeries.hiddenState.properties.endAngle = -90; pieSeries.hiddenState.properties.startAngle = -90; }); // end am4core.ready() </script>実装例
最後に、私が作成した"sommeil"というアプリケーションのコードの内、一部のamcharts部分を参考までに記載します。
_piechart.html.erb<!-- Styles --> <style> #chart2div { width: 100%; height: 300px; } </style> <!-- Resources --> <script src="https://www.amcharts.com/lib/4/core.js"></script> <script src="https://www.amcharts.com/lib/4/charts.js"></script> <script src="https://www.amcharts.com/lib/4/themes/animated.js"></script> <!-- Chart code --> <script> am4core.ready(function () { // Themes begin am4core.useTheme(am4themes_animated); // Themes end // Create chart instance var chart2 = am4core.create("chart2div", am4charts.PieChart); // Add data var sleeping_time = '<%= @sleeping_time %>'; chart2.data = [{ "time": "Sleep", "amount": sleeping_time }, { "time": "Awake", "amount": (24 - sleeping_time) }]; // Add and configure Series var pieSeries = chart2.series.push(new am4charts.PieSeries()); pieSeries.labels.template.disabled = true; pieSeries.dataFields.value = "amount"; pieSeries.dataFields.category = "time"; pieSeries.slices.template.stroke = am4core.color("#fff"); pieSeries.slices.template.strokeWidth = 2; pieSeries.slices.template.strokeOpacity = 1; // This creates initial animation pieSeries.hiddenState.properties.opacity = 1; pieSeries.hiddenState.properties.endAngle = -90; pieSeries.hiddenState.properties.startAngle = -90; }); // end am4core.ready() </script> <!-- HTML --> <div id="chart2div"></div>最後に
注釈や解説できない部分がまだまだありますので、今後検証して追記していきます。
ご覧いただき、ありがとうございました。筆者について
TECH::EXPERTにて4月よりruby, railsを学習している未経験エンジニアです。
記載内容に不備・不足があればご指摘いただけると幸いです。
至らぬ点ばかりですので、改善点がありましたらどんどんご指摘下さい!
- 投稿日:2019-07-08T22:33:10+09:00
railsの基礎知識 ※自分メモ用
Rails学習コース
modelとApplication Record
Postモデルrails g modelによってPostモデルが定義された「post.rb」app/modelsに作成
◎ターミナル◎
Post=モデル名
content=カラム名
:text=データ名rails g model Post content:text
そして、このコマンドによって、以下の2つのファイルが作成される。
・app/modelsフォルダにモデルが定義されたファイル
・db/migrateフォルダにマイグレーションファイル◎ターミナル◎
rails cosole
ex)text = "Hello"
テーブルに投稿データの保存
posts テーブルにデータを追加するには下の図のように、
① new メソッドで Post モデルのインスタンスを作成
② posts テーブルに保存
これら2つの手順を行っていく。◎具体的なコマンド↓↓↓
post1 = Post.new(content: "ライオン") 新しいの生成 post1.save セーブする1、テーブルからデータを取り出す方法
①テーブルからデータを1つ取り出す
②contentカラムの値を取り出す
→「Post.first」で取得したデータから投稿内容を取得することができる①post = Post.first ②post.content2、テーブルからデータを取り出す方法
↓↓テーブルからすべてのデータを取り出す方法↓↓
posts = Post.all※「Post.all」では、テーブルにある全てのデータが配列で取得できる
↓↓投稿の配列から1つのデータを取り出す方法↓↓
Post.all [0]※Post.all [0] のように、インデックス番号で1つの要素を取得できる
↓↓配列のデータから投稿内容を取り出す方法↓↓
Post.all[0].contentrails consoleまとめ
データの作成(new,save)
データ取得(Post.all,post.content..etc)
- 投稿日:2019-07-08T22:04:31+09:00
終了~ヘルスケアwebサービスを自分で作る医者の日記~
12章
12.18テストの途中まで終了
テストコードの仕組み
get してからassertなど、順次上から処理していっているのか?
まだわからん
- 投稿日:2019-07-08T21:21:04+09:00
TECH ~Day14~
学習を始めてから早くも2週間が経ち、中間試験までの日にちが近づいてきました。今までの学習はほとんど知識を入れる学習ばかりだったのでいざ問題を解くという場面になった時にどれだけ今までの学習が活かせるのか、とても不安です。
最近は特に忙しくなってきたので、学習を効率よく行うにはどうすればいいのか常に考えています。学習内容
・部分テンプレート
・render
・render partial オプション
・git, git clone, git hub
・bundlerによるgem管理
・bundle exec コマンド・部分テンプレート
ツイッターなどでツイート画面などがHTML構造的に同じもの。
同じHTML構造の部分を共通化していくことによって、無駄なくビューファイルを作ることができる。この共通化された部分を「部分テンプレート」という。・render
部分テンプレートを呼び出す際に利用するメソッド。・render partial オプション
renderに「:partial」というオプションをつけることで、明示的に部分テンプレート名を指定し、部分テンプレートを表示することができます。!部分テンプレートのファイル名はアンダーバー「_」から始まる。
・git, git clone, git hub
git
ソースコードの変更履歴、バージョン等を記録するツールで、開発する際には、ひっすと言えるツールです。facebookやgoogleも利用している。git hub
チーム開発やソースコードの共有にとても便利なサービス。
自分のソースコードを保管できるため、エンジニアの履歴書のような役割も果たす。git clone リポジトリURL ディレクトリ名
git cloneコマンドは、外部のサーバーにあるgitで管理されたソースコードを自分のパソコン(ローカル環境)にダウンロードするコマンド。
実際に使用する際には「git clone リポジトリURL ディレクトリ名」という形でターミナルで実行して行う。・bundlerによるgem管理
bundlerを利用してインストールしたgemは、railsの各プロジェクトではなく、rubyのバージョンごとにある保存場所にインストールされています。「bundle show[gemの名前]」というコマンドを入力するとbundlerでインストールしたgemがどこに保存されているか確認できる。
・bundle exec コマンド
bundler execは二つ以上のrailsアプリケーションを同じバージョンのrubyを利用して作成している際に必要となるコマンド。「bundle exec rake db:create」
↑データベースの作成コマンド「bundle exec rails s」
↑サーバーの起動
- 投稿日:2019-07-08T21:14:45+09:00
Strong Parameters~ヘルスケアwebサービスを自分で作る医者の日記~
12章で
def user_params params.require(:user).permit(:password, :password_confirmation) end登場
7章を見返すと
Strong Parametersか!
読み直してやっと理解できた、、、リスト 7.18のコメントと、上の再録コメントでも重ねて指摘しているように、この実装は最終形ではありません。その理由は、paramsハッシュ全体を初期化するという行為はセキュリティ上、極めて危険だからです。これは、ユーザーが送信したデータをまるごとUser.newに渡していることになります。ここで、Userモデルにadmin属性というものがあるとしましょう。この属性は、Webサイトの管理者であるかどうかを示します (この属性を実装するのは10.4.1になってからです)。admin=’1’という値をparams[:user]の一部に紛れ込ませて渡してしまえば、この属性をtrueにすることができます。これはcurlなどのコマンドを使えば簡単に実現できます。paramsハッシュがまるごとUser.newに渡されてしまうと、どのユーザーでもadmin=’1’をWebリクエストに紛れ込ませるだけでWebサイトの管理者権限を奪い取ることができてしまいます。
以前のバージョンのRailsでは、モデル層でattr_accessibleメソッドを使うことで上のような危険を防止していましたが、Rails 4.0ではコントローラ層でStrong Parametersというテクニックを使うことが推奨されています。Strong Parametersを使うことで、必須のパラメータと許可されたパラメータを指定することができます。さらに、上のようにparamsハッシュをまるごと渡すとエラーが発生するので、Railsはデフォルトでマスアサインメントの脆弱性から守られるようになりました。
この場合、paramsハッシュでは:user属性を必須とし、名前、メールアドレス、パスワード、パスワードの確認の属性をそれぞれ許可し、それ以外を許可しないようにしたいと考えています。これを行うには、次のように記述します。
params.require(:user).permit(:name, :email, :password, :password_confirmation)
このコードの戻り値は、許可された属性のみが含まれたparamsのハッシュです (:user属性がない場合はエラーになります)。これらのパラメータを使いやすくするために、user_paramsという外部メソッドを使うのが慣習になっています。このメソッドは適切に初期化したハッシュを返し、params[:user]の代わりとして使われます
- 投稿日:2019-07-08T21:11:29+09:00
埋め込み~ヘルスケアwebサービスを自分で作る医者の日記~
edit アクションでページ内に埋め込み
updateで更新するとな、、、<% provide(:title, 'Reset password') %> <h1>Reset password</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %> <%= render 'shared/error_messages' %> <%= hidden_field_tag :email, @user.email %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Update password", class: "btn btn-primary" %> <% end %> </div> </div>
- 投稿日:2019-07-08T20:50:29+09:00
MaterializecssのCarouselを使用して、3秒ごとに画像が自動で切り替わるページを作る
概要
TECH::EXPERTのカリキュラムでオリジナルのミニアプリを作成する機会があり、
その一部のページでMaterializecssのCarouselを使用し、3秒ごとに画像が切り替わるページを作成したので紹介します。MaterializecssのCarouselとは
画像をくるくると回せる機能です。
https://materializecss.com/carousel.html自分が作成したページ紹介
作成する前提
MaterializecssがCDNで読み込めている
編集するファイル
・ビューファイル
・CSSファイル
・jsファイルビューファイル
about.html.erb<section class="about-main" > <div class="carousel carousel-slider" data-indicators="true" id="big3" > <div class="carousel-fixed-item"> <div class="container"> <h1 class="white-text">Work Hard See Result</h1> <% if user_signed_in? %> <a class="btn waves-effect white black-text darken-text-2" href="/" target="_blank">HOME</a> <%else%> <a class="btn waves-effect white black-text darken-text-2" href="/users/sign_in" target="_blank">Log in</a> <%end%> </div> </div> <div class="carousel-item big3pic" id="benchpress"> <div class="container"> <h3 class="white-text">Bench Press</h3> <p class="white-text">chest</p> </div> </div> <div class="carousel-item big3pic" id="deadlift"> <div class="container"> <h3 class="white-text">Dead Lift</h3> <p class="white-text">back</p> </div> </div> <div class="carousel-item big3pic" id="squat"> <div class="container"> <h3 class="white-text">Squat</h3> <p class="white-text">legs</p> </div> </div> </div> </section>"carousel carousel-slider" を用いました。
"carousel-fixed-item"は固定して表示したいものを書きます。
"carousel carousel-item"の部分がそれぞれの画像のクラスです。CSSファイル
style.css#big3{ height: 100vh; } #benchpress{ background-image: url('benchpress.jpg'); background-size: 100%; } #deadlift{ background-image: url('deadlift.jpg'); background-size: 100%; } #squat{ background-image: url('squat.jpg'); background-size: 100%; }それぞれidを付与してるので、idによって画像を変更してください。
jsファイル
about.js$(document).ready(function(){ $('#big3').carousel( { dist: 0, padding: 0, fullWidth: true, indicators: true, duration: 100, } ); autoplay() function autoplay() { $('#big3').carousel('next'); autoplay: true, setTimeout(autoplay, 3000); } });autoplayの関数を定義してます。
id=big3に対して3000ミリ秒で次のcarousel-itemを
表示するように設定してます。オプションは下記を参照しました。
- [1] dist: 0, => 遠近ズーム0
- [2] padding: 0, => 中央以外の項目の余白0
- [3] fullWidth: true, => carouselを全幅のスライダーへ
- [4] indicators: true, => インジケータを表示
- [5] duration: 100, => 次のスライドへ移動しきるまでの時間100ミリ秒
最後に
この記事を書いた目的
・自分なりに工夫した点をアウトプットして、理解を深める。
・あわよくば有識者にフィードバックをもらいたい。
・私と同じ初学者からも奇譚のない意見をもらいたい。(自分だったらどうこうする的な)筆者について
TECH::EXPERTにて4月27日より52期夜間・休日コースでruby/railsを学習している未経験エンジニアです。
ご不備等ありましたら、ご指摘ください。ちなみに本記事が初投稿になります。
言わずもがなかもしれませんが、趣味はボディメイク・筋トレでございます。
余談ですが、120kg⇨66kgまで減量して大会出場した経験があり
ダイエットについての質問はなんでも答えられるかと思います
- 投稿日:2019-07-08T20:47:10+09:00
letter_opener でメール内容がブラウザで表示されない
letter_opener でメール内容がブラウザで表示されない
表題のとおりなのですが、
letter_openerの設定を正しく行っても、うまくブラウザに表示されない場合....Railsであれば、
ApplicationMailer
のfrom
を確認しましょう。
from
が設定されていないと、ブラウザで表示されません・・・・これ調べるので1日使ってしまった・・・・orz
自分用メモではあるのですが、
他の同じハマリをしている方達への教訓となれば幸いです。
- 投稿日:2019-07-08T20:30:31+09:00
TECH ~Day13~
学習内容
・アソシエーション
・n + 1問題
・includesメソッド
・後置if
・updateメソッド・アソシエーション
モデル間の関連付けを管理する機能のこと。
定義しておくことでモデルをまたいだデータの呼び出しをより簡単に行うことができる。・n + 1問題
データを呼び出す際に大量のSQLが発行されてしまう問題。・includesメソッド
これはn+1問題を解消することができる。
指定された関連モデルをまとめて取得することで、SQLの発行回数を減らしてくれる。
書き方
includes(:モデル名)・後置if
最後のendを省略してif文を処理の後方に配置する書き方。
elsif、elseに当たる条件分岐がなく、かつ処理が一行で完結する場合に用いる。・updateメソッド
Active Recordのうちの一つ。
updateメソッドはモデルのインスタンスに対して使用することで、引数内の情報にレコードを更新することができる。コメントを投稿するためのルーティングの記述方法
・resourcesメソッド
・ルーティングのネスト・resourcesメソッド
Railsの基本となるコントローラの7つのアクションで記載した7つのアクション名に対してのルーティングを自動で生成するメソッド。
7つのアクションを簡単に定義するメソッド。・ルーティングのネスト
ネストとは入れ子構造とも呼ばれ、ある記述の中に入れ子構造で別の記述をする方法である。
ルーティングでいうと、あるコントーラへのルーティングの記述の中に、別のコントローラへのルーティングを記述するということを示す。
- 投稿日:2019-07-08T20:17:39+09:00
rails スラッシュを複数挟んでもnot foundにならない
同僚から
Rails のルーティングは連続するスラッシュは単一扱いされるらしい。
localhost:3000/utilities///////hoge/39
の様にスラッシュを複数挟んでもnot foundにならないのはなんでなん?という質問を受けたので、備忘録として書いておきます。
僕が調べた感じ、railsの名前解決の流れは
https://qiita.com/kkyouhei/items/1203f5aa521c065a7097
らしい。そこで通る
Journey::Router::Utils.normalize_path(path)
にてdef self.normalize_path(path) path = "/#{path}" path.squeeze!('/') path.sub!(%r{/+\Z}, '') path = '/' if path == '' path endhttps://github.com/rails/journey/blob/master/lib/journey/router/utils.rb#L14
引数に含まれる文字が複数並んでいたら 1 文字にまとめる
.squeeze!('/')
をしているからhttps://docs.ruby-lang.org/ja/latest/method/String/i/squeeze.html
履歴にも残してはいけないものを書いていたので上げ直しです。
ごめんなさい
- 投稿日:2019-07-08T18:48:47+09:00
[Rails]refile + fields_for の書き方
やりたいこと
・refileを使用して画像アップロードの実装
・複数モデルに一度で保存したいため、fields_forを使用この2つの組み合わせの書き方が調べてもほとんど出てこなかったので書いておく。
環境
Ruby 2.5.1
Rails 5.2.3導入、基本的な使い方参考
refileの基本と複数画像のアップロード
導入方法や基本的な使い方は上記サイトを参考にしてください。ただ一点、Gemfileのインストールは上記サイトの通りだとエラーが出たので、
下記コードで実行できました。Gemfilegem "refile", require: "refile/rails", github: 'manfe/refile' gem "refile-mini_magick", github: 'refile/refile-mini_magick'参考サイト様だと
github: 'refile/refile'
となってる部分、
bundle install時にエラーが出たので、エラーメッセージに従ってgithub: 'manfe/refile'
としたら成功しました。fields_forを使用する場合の書き方
前提:
・親モデルと子モデルは1対1のアソシエーション
・1画像のみアップロードするものとする入力フォーム
ビューファイル.html.haml= form_with model: @user do |f| # 親モデルをuserと仮定 # 省略 = f.fields_for :image do |i| # 子モデルをimageと仮定 = i.attachment_field :image # 「:image」 はimage_idカラムの_idを抜いたもの
attachment_field
を使用する。
これでfile_field
と同じファイル選択フォームが作成される。モデルのアソシエーション
モデル.rb# userモデル has_one :image, dependent: :destroy # 「dependent: :destroy」でuserを削除すると関連するimageも削除される accepts_nested_attributes_for :image # fields_forに必要な記述 accepts_attachments_for :image, attachment: :image # refileに必要な記述 # imageモデル attachment :image # 「:image」 はimage_idカラムのidを抜いたもの belongs_to :event
attachment :image
の:image
はモデル名ではなく、カラム名なので間違えないように。
カラムは「image_id」で登録していても、ここでは「_id」は抜かして書く。コントローラーの記述
users_controller.rbdef new @user = User.new @user.build_image # fields_forのために親子モデルを関連付け end def create User.create(user_params) end def index @users = User.all.includes(:image) # この呼び出し方が必須というわけではないです end def user_params params.require(:user).permit( :name, :email, image_attributes: [ :id, :image, # 「image_id」カラムでも「_id」は入れなくて良い :image_cache_id # これを記述しておくとキャッシュファイルも保存される(らしい) ] ) end最低限の記述のみ書いてます。
ちなみに、
.includes(:image)
で子モデルが2つ以上ある場合は、
.includes([:image, :address])
のように、引数内で[]
で囲む必要があるので注意。ビューファイルでの保存した画像の表示
ビューファイル.html.haml= attachment_image_tag @user.image, :image, :fill, 200, 120, format: "jpg" # 「@user.image」 は子モデル名。今回はfields_forでuserが親モデルとして紐づいているためこのように指定 # 「:image」 はimage_idカラムのidを抜いたもの # 「200, 120,」 は表示する画像のサイズ。 「横, 縦,」
attachment_image_tag
を使用して保存したデータを呼び出す。今回、1画像のみのアップロード前提で書いたが、複数画像アップロードだとかなり変わってくる。
複数画像アップロードの場合
入力フォーム
ビューファイル.html.haml= form_with model: @user do |f| # 省略 = f.fields_for :images do |i| # :imagesと複数形にする = i.attachment_field :image, multiple: true, name: "images_attributes[0][image][]" # 「multiple: true」 で複数ファイル選択を可能にする # 「name: "images_attributes[0][image][]"」 でname属性をparamsに合わせる
= f.fields_for :images do |i|
のモデル指定を:images
と複数形にする。
その下の= i.attachment_field :image
はimage_idカラムのidを抜いたものなので単数形のままでOK。
multiple: true
を指定することにより、fields_forもあるためparamsの形がかなり変わり、そのままではparamsに値が入らない。
値がちゃんとparamsに入るよう、name: "images_attributes[0][image][]"
によりname属性を変わった後のparamsと合わせた形に指定し、値がparamsに入るようにする。モデルのアソシエーション
モデル.rb# userモデル has_many :images, dependent: :destroy # has_manyに変更、複数形に変更 accepts_nested_attributes_for :images # 複数形に変更 accepts_attachments_for :images, attachment: :images # 複数形に変更 # imageモデル attachment :image # カラムなので単数形のままでOK belongs_to :event
attachment :image
も複数形に変えないよう注意コントローラーの記述
users_controller.rbdef new @user = User.new @user.images.build # 複数形になり記述が少し変更 end def create # 先にuserインスタンスにparamsを入れる User.new(user_params) # 複数画像は配列で持っているため、1つ1つインスタンスに入れる params[:images_attributes][:"0"][:image].each do |image| @user.images.build(image) end User.save end def index @users = User.all.includes(:images) # 複数形に変更。この呼び出し方が必須というわけではないです end # image_attributes:をストロングパラメーターから削除 def user_params params.require(:user).permit( :name, :email ) endコントローラーのcreateアクションとストロングパラメーターについては、もっと良い書き方があるはず。
正直、params[:images_attributes][:"0"][:image]
のストロングパラメーターでの取得の方法がわからなかったため、苦肉の策でストロングパラメーターからimageを消し、createアクションで直接paramsを渡している。いろいろな書き方を試したが、ストロングパラメーターでは取得できなかった。
できる方法があれば教えていただけると助かります。ビューファイルでの保存した画像の表示
ビューファイル.html.haml# 複数画像は配列で保存されているため、1つ1つ取り出して表示 @user.images.each do |image| # 「@user.images」と複数形に変更 = attachment_image_tag image, :image, :fill, 200, 120, format: "jpg" # 「attachment_image_tag image」 の「image」はeach文で指定した変数複数画像の場合については以上。
これで複数画像のDB保存と表示ができるはず。複数保存の問題点
上記のコードで一応複数画像についても、DB保存、表示が可能になった。
しかし問題がある。元々
fields_for
と.attachment_field
は1つだけ書いてファイル選択フォームは1つしか無かったものが、例えば3画像保存し、編集しようとするとあら不思議、フォームが3つに増えています!| fields_forは紐づくcaptured_images全てに対して展開されるため、 |
| editアクションのviewにてfields_forが呼ばれるたびに、 |
| 選択された画像それぞれに対していちいち展開されてしまう点でした。 |上記のリンク先でも記述されているが、複数保存したものをeditアクションなどで編集しようとすると、保存した画像の数だけファイル選択フォームが増えるという現象が起きる。
上記リンク先によるとどうもfields_for
の仕様で、紐づくレコードが複数あるとその分だけフォームが呼び出されるらしい。editアクションで表示するビューをnewアクションのビューと同じにしていると確実にフォームが増えてしまうので、編集の仕方をどうにか工夫しなければならない。
何か良い方法があったら教えていただけると助かります。
- 投稿日:2019-07-08T18:16:27+09:00
RailsでStripeを使って決済機能を実装する(仮払い機能付き)
サービスを開発していると決済機能をサービス内に実装したいことがよくあるかと思います。
今までちょくちょくstripeを利用してきたので、その経験を元に仮払い(オーソリ)込みの決済の実装方法を記述していこうと思います。Stripeとは
Stripeとはインターネットビジネス内に組み込むことができる決済サービスの1つです。Stripeを使えば、自分が開発しているサービスにクレジットカードを使った決済機能を組み込むことができます。決済方法が銀行振込のみのサービスにはぜひ導入を検討してみてください。
stripeの手数料
stripeの手数料は決済毎に3.6%かかるのみです。導入費用や月ごとの利用料等は全て無料です。つまり、使った分だけ手数料がかかるという仕組みです。固定で発生する費用がないため、個人開発で売り上げがなかなか見込めない小さなサービスにもぴったりだと思います。
Railsプロジェクトにstripeを導入する
それではstripeをRailsに導入してみましょう。
stripeはgemを用意してくれているので、導入はただgemをbundle installするだけです。Gemfilegem 'stripe'stripeのgemを入れると、Rails内でstripeの様々なモデルを利用することができます。
単発の決済に利用するのは以下の2つのモデルです。Stripe::Customer
決済の際にまずstripeを利用する顧客情報を登録します。
emailやsourceなどのパラメータを渡します。Stripe::Charge
単発決済を行う際の決済情報を登録します。
決済金額(amount)や決済通貨(currency)などのパラメータを渡します。Stripe::Refund
決済のキャンセルを登録します。
仮払いの状態であれば全額返金可能。本決済完了後であればstripeの手数料(3.6%)が引かれた96.4%の金額が返金されます。Stripe Checkoutを実装する
それでは今回はAirbnbのような自分の部屋を貸すサービスを例にしてコードを書いていきたいと思います。
部屋の管理はRoomモデル、支払いの管理はPaymentモデルで行なっているとします。(1対1の関係)まずはStripeのAPIキーをstripe.rbに入れておきます。
stripe.rb#開発環境とテスト環境にはstripeのテストモードのAPIキーを使用する if Rails.env.development? || Rails.env.test? Rails.configuration.stripe = { :publishable_key => ENV['STRIPE_PUBLISHABLE_KEY'], :secret_key => ENV['STRIPE_SECRET_KEY'] } end #本番環境にはstripeの本番環境用APIキーを使用する if Rails.env.production? Rails.configuration.stripe = { :publishable_key => ENV['STRIPE_PUBLISHABLE_KEY_PRODUCTON'], :secret_key => ENV['STRIPE_SECRET_KEY_PRODUCTION'] } end #それぞれの環境に適したstripeAPIキーをセットしておく。 Stripe.api_key = Rails.configuration.stripe[:secret_key]次にroom_controllerというコントローラーにstripeを実装していきます。
room_controller.rbdef create @room = Room.find(params[:room_id]) begin ActiveRecord::Base.transaction do @room.reserve = "仮予約" @room.save! #二重決済を防ぐためにすでにpaymentテーブルがユニークか調べる raise "Already paid" if @room.payment.present? #後でPaymentをnewする際に、万が一エラーが起こらないように、transactionの中でpayment_paramsの値を確認しておく raise "prameter error" unless params[:amount].present? && params[:stripeEmail].present? && params[:stripeToken].present? end ##############Stripe(決済)######################################### customer = Stripe::Customer.create( email: params[:stripeEmail], source: params[:stripeToken] ) charge = Stripe::Charge.create( customer: customer.id, amount: params[:amount], description: "「#{@room.name}」の決済", currency: 'jpy', receipt_email: params[:stripeEmail], metadata: {'仮払い' => "1回目"}, capture: false #capture:falseにすると仮払いで処理してくれる。 ) ##################################################################### ###############決済記録を作成################################################### # stripeのcheckoutフォームから送られてきたパラメーターでpaymentのインスタンスを作成 payment = Payment.new(payment_params) payment.email = customer.email # 支払った人がstripeのcheckoutフォームに入力したemail(支払い完了後、stripeからこのメールアドレスに支払い完了メールが送られる) payment.description = charge.description #決済の概要 payment.currency = charge.currency #通貨 payment.customer_id = customer.id # stripeのcustomerインスタンスのID payment.payment_date = Time.current # payment_date(支払いを行った時間)は現在時間を入れる payment.payment_status = "仮払い" # payment_status(この支払い)は仮払い状態(stripeのcaptureをfalseにしている) payment.uuid = SecureRandom.uuid # 請求書の番号としてuuidを用意する payment.charge_id = charge.id # 返金(refund)の時に使うchargeのIDをpaymentに保存しておく payment.stripe_commission = (charge.amount * 0.036).round # stripeの手数料(3.6%)分の金額 payment.amount_after_subtract_commission = charge.amount - payment.stripe_commission # stripeの手数料(3.6%)分を引いた金額(依頼者が払った96.4%の金額) payment.receipt_url = charge.receipt_url # この決済に対するstripeが用意してくれる領収書のURL payment.save! ############################################################################# redirect_to temporary_complete_path #仮払い完了画面へ # stripe関連でエラーが起こった場合 rescue Stripe::CardError => e flash[:error] = "#決済(stripe)でエラーが発生しました。{e.message}" render :new # Invalid parameters were supplied to Stripe's API rescue Stripe::InvalidRequestError => e flash.now[:error] = "決済(stripe)でエラーが発生しました(InvalidRequestError)#{e.message}" render :new # Authentication with Stripe's API failed(maybe you changed API keys recently) rescue Stripe::AuthenticationError => e flash.now[:error] = "決済(stripe)でエラーが発生しました(AuthenticationError)#{e.message}" render :new # Network communication with Stripe failed rescue Stripe::APIConnectionError => e flash.now[:error] = "決済(stripe)でエラーが発生しました(APIConnectionError)#{e.message}" render :new # Display a very generic error to the user, and maybe send yourself an email rescue Stripe::StripeError => e flash.now[:error] = "決済(stripe)でエラーが発生しました(StripeError)#{e.message}" render :new # stripe関連以外でエラーが起こった場合 rescue => e flash.now[:error] = "エラーが発生しました#{e.message}" render :new end endPaymentは領収書の役割を果たすクラスです。
gemのprownやwicked_pdfを使えば、簡単に領収書を作成できるかと思います。
ただ、stripeでは自動的に領収書が作成され、receipt_urlを使えば、作成された領収書のURLを引っ張ってくることもできます。次にStripe Checkoutのフォームを作ります。
new.html.erb<div> <%= form_tag room_path, id:"payForm" do %> <script src="https://checkout.stripe.com/checkout.js"></script> <%= hidden_field_tag 'stripeToken' %> <%= hidden_field_tag 'amount', @room.price %> <%= hidden_field_tag 'stripeEmail' %> <!-- 仮払いしたユーザーのID --> <%= hidden_field_tag 'user_id', current_user.id %> <!-- この仮払いのroomインスタンスを特定 --> <%= hidden_field_tag "room_id", @room.id %> <!-- 支払い済みであれば決済させない --> <% if @room.payment.present? %> <p class="btn btn-error">支払い済み</p> <% else %> <button id="btn-pay" type="button" class="btn btn-primary">仮払いする</button> <% end %> <script> // #ボタンを押した際のcheckoutのフォームはStripeCheckout.configureで設定する var handler = StripeCheckout.configure({ //StripeのAPIキーを引っ張ってくる key: '<%= Rails.configuration.stripe[:publishable_key] %>', locale: 'auto', //言語の設定(autoの場合、ユーザのブラウザ規定言語が呼び出される) currency: 'jpy', // image: "image/directory", もしstripe checkoutのフォーム上部に画像を入れたい場合はここで指定する panelLabel: "{{amount}}のお支払い", //checkoutボタンの表示文字、{{amount}}の中に金額が入る allowRememberMe: false, //RememberMeを使いたい場合はここをtrueにする token: function(token,arg) { //ここでstripeTokenとstripeEmailに値を入れてsubmitする document.getElementById('stripeToken').value = token.id; document.getElementById('stripeEmail').value = token.email; document.getElementById('payForm').submit(); } }); //Stripe Checkoutのフォームに表示される情報をここで指定する document.getElementById('btn-pay').addEventListener('click', function(e){ handler.open({ name: '<%= @room.name %>', description: '<%= @room.price %>円', amount: document.getElementById("amount").value }); e.preventDefault(); }) </script> <% end %> </div>これで仮払い機能の実装はできました。
あとはお部屋の貸し出しを大家さんがOKしてくれたら、本決済してあげるだけです。
つまり、capturedパラメータをtrueに変更してあげるだけです。(captureメソッドを使う)room_controller.rbdef pay_complete room = current_user.rooms.find(params[:room_id]) begin ActiveRecord::Base.transaction do room.reserve = "予約完了" #予約状況 room.save! room.payment.payment_status = "完了" #支払い状況 room.payment.save! #仮払いをcaputureして決済を完了させる charge_id = room.payment.charge_id charge = Stripe::Charge.retrieve(charge_id) #retrieveメソッドで仮払いしたchargeインスタンスを引っ張ってくることができる charge.capture #captureメソッドを使うと、本決済となり、実際にクレジットカードから決済が行われる。 end redirect_to complete_path #本決済完了画面へ #stripe関連でエラーが起こった場合 rescue Stripe::CardError => e flash.now[:error] = "決済でエラーが発生しました。#{e.message}" render :new # Invalid parameters were supplied to Stripe's API rescue Stripe::InvalidRequestError => e flash.now[:error] = "決済(stripe)でエラーが発生しました(InvalidRequestError)#{e.message}" render :new # Authentication with Stripe's API failed(maybe you changed API keys recently) rescue Stripe::AuthenticationError => e flash.now[:error] = "決済(stripe)でエラーが発生しました(AuthenticationError)#{e.message}" render :new # Network communication with Stripe failed rescue Stripe::APIConnectionError => e flash.now[:error] = "決済(stripe)でエラーが発生しました(APIConnectionError)#{e.message}" render :new # Display a very generic error to the user, and maybe send yourself an email rescue Stripe::StripeError => e flash.now[:error] = "決済(stripe)でエラーが発生しました(StripeError)#{e.message}" render :new ######################################################################################## # 下のレスキューはstripe関係以外でのエラーが起こった時に発動する rescue => e flash.now[:error] = "エラーが発生しました#{e.message}" render :new endstripeの場合、仮払いの状態が7日間続いたものは自動的にキャンセルされるようになっています。
7日間以上仮払いの状態を保ちたい場合は、rakeタスクにて一旦Stripe::Refundクラスを使って既存の仮払いをキャンセルし、再度Stripe::Chargeから仮払いを行うと良いかと思います。さいごに
Stripeを使えば、自分のサービスに決済機能がつけられるので、サービス開発がさらに楽しくなりますね!
- 投稿日:2019-07-08T16:00:31+09:00
Railsからスプレッドシートを定時で書き換える
はじめに
サービスのKPIなどをスプレッドシートで管理することが多いとは思いますが、毎回データ取ってスプレッドシートに書き込んで、、をやってるとまあまあ時間の無駄なのでRailsアプリから直接スプレッドシートシートに書き込むようにしました。
加えてKPIの計測なので毎朝データを更新できるようにバッチも組むようにしました。今回には以下を使っています。
- Rails 5.2
- gem 'google_drive'
実装
作るもの
Railsのデータベースから当月に会員登録したユーザーの数を日別で取得するようにします。
ユーザーは User モデルとします。認証情報の取得
API経由でスプレッドシートを編集するために認証情報が必要なので Google Developers Console で情報を取得します。
プロジェクトの作成
APIの有効化
続いてスプレッドシートのAPIの利用を有効化するためにライブラリからスプレッドシートを検索して有効化します。
認証情報の作成
まずはOAuth同意画面で操作します。アプリケーション等を埋めて保存します。
作成したクライアントのリストから名前をクリックするとクライアントIDとクライアントシークレットを確認できます。
リフレッシュトークンの取得
つづいて先程作成したクライアントの情報をもとにリフレッシュトークンを取得します。
書くの面倒くさくなってしまったのでこちらのやり方はこの記事を参考にしてみてください。Rails側の実装
今回はもろもろの処理をジョブの中に書いていきます。
Google API を今回の処理以外でも使う可能性を加味して
ApplicationJob
に API の認証の処理を書き、他のジョブでも呼び出せるようにしました。(使う可能性を加味してというか、私の場合は複数使っているため)app/jobs/application_job.rbclass ApplicationJob < ActiveJob::Base def google_drive credentials = Google::Auth::UserRefreshCredentials.new( client_id: ENV['KPI_SHEET_CLIENT_ID'], client_secret: ENV['KPI_SHEET_CLIENT_SECRET'], scope: %w(https://www.googleapis.com/auth/drive https://spreadsheets.google.com/feeds/), redirect_uri: 'http://storys.jp' ) credentials.refresh_token = ENV['KPI_SHEET_REFRESH_TOKEN'] credentials.fetch_access_token! GoogleDrive::Session.from_credentials(credentials) end end実際にスプレッドシートに書き込む処理は以下です。
ws[1, 1]
はシートの1行目の1列目、ws[1, 2]
は1行目の2列目です。
同じようにws[2, 1]
は2行目の1列目になります。app/jobs/export_kpi_job.rbclass ExportKpiJob < ApplicationJob def perform spreadsheet = google_drive.spreadsheet_by_key('スプレッドシートのキー') ws = spreadsheet.worksheet_by_title('該当のシートの名前') ws[1, 1] = '' ws[1, 2] = '新規登録ユーザー数' current_date = Date.current beginning_day_of_month = current_date.beginning_of_month end_day_of_month = current_date.end_of_month (beginning_day_of_month..end_day_of_month).each.with_index(2) do |date| ws[index, 1] = date ws[index, 2] = Users.where(created_at: date.beginning_of_day..date.end_of_month) end ws.save end end定時処理の設定
定時処理には
gem 'whenever'
を使います。
whenever自体の使い方はこちらを参考にしてください。まずはタスクの作成
lib/tasks/rails_sample.rakenamespace :rails_sample do desc 'KPIをスプレッドシートに吐き出す' task :export_kpi => :environment do ExportKpiJob.perform_now end end今回は毎朝4時にタスクが走るようにします。
config/schedule.rbevery 1.day, at: '4:00 am', role: [:app] do rake 'rails_sample:export_kpi' endまとめ
以上でRailsからスプレッドシートを定時で書き換えるようにできました。
こういう小さな自動化を含めて、自分はもちろん他の人の仕事の効率を上げてあげることがエンジニアのできる貢献の1つかと思うので今後も続けたい所存。
- 投稿日:2019-07-08T15:11:03+09:00
[ruby]バッチ処理について
バッチ処理とは?
バッチ処理とは“一定量の(あるいは一定期間の)データを集め、一括処理するための処理方法”
私は、大量のデータを処理すること全般をイメージしてる。
引用元
https://www.imkk.jp/blog/what-is-batch-processing.htmlバッチ処理のフロー
私の業務では、DBに一括登録するデータをスプレッドシートから取り組むバッチ処理でした。
以下では、データベースに商品を一括にスプレッドシートから取り込んで追加するという場合。フローは以下の通り
まずrakeファイルにそのバッチ処理がどういったものなのかをdescにかく。
そのあと走らせる。
以下がそのコードimport_new_products_information.rakenamespace :batch do desc '新規商品の追加' task import_new_products_information: :environment do require 'batches/import_new_products_information' Batches::ImportNewProductsInformation.run end end走らせるファイル。
一応シート内に商品名と商品コードというカラムがあることが前提。import_new_products_informations.rbrequire 'batch/base' require 'google_spreadsheet' module Batches class ImportrProductsInformations < Batch::Base SPREADSHEET_KEY = '追加する商品のスプレッドシートキー'.freeze SHEET_NAME = '追加する商品一覧があるシートの名前 例)取り込み用など'.freeze def execute(_args) gs = GoogleSpreadsheet.new(SPREADSHEET_KEY) gs.spreadsheet.worksheet_by_title(SHEET_NAME).tap do |sheet| sheet.list.each do |row| products_informations.create!( product_name: row['商品名'], product_number: row['商品コード'] ) end end end end endgoogle_spredsheet.rbdef worksheet_by_title(sheet_name) spreadsheet.worksheet_by_title(sheet_name) end
- 投稿日:2019-07-08T14:49:09+09:00
Thorのコマンドの中でrailsのActiveRecordを使う
以前railsのタスクの書式という記事を書きましたが、railsのタスクが引数がややこしくて、オプションもスマートに使えず余り好きではありません。
Thorでコマンドを書く方が好きなのですが、Thorの中でActiveRecordを使ってDBの操作が出来たのでメモしておきます。
以下はrailsアプリケーションの直下に
thor
というディレクトリを作りthor/foobar
にコマンドがある想定のコードです。railsのautoloadも効くのでthorをGemfileに追加します。
gem 'thor', '~> 0.20'* @scivola さんにコメントいただきましたが
railties
がthorに依存しているのでGemfileには書かなくても読み込めます。ただし私の環境で試したところrequire 'thor'
を書かないとuninitialized constant Thor
で動きませんでした。#!/usr/bin/env ruby require_relative '../config/application' Rails.application.initialize! class Main < Thor desc "exec", "foobar" def exec binding.pry end end Main.start(ARGV)これで
thor/foobar execで実行可能です。なんてことは無くて
config/environment.rb
のコードを丸っと持ってきただけです。なのでRAILS_ENV=production thor/foobar execで実行すると
production
環境で実行可能です。spring経由で起動してないので起動が重いのが玉に傷です。やり方をちょっと調べてみて、ちなみにspring経由で起動するのにこんなタイムリーな記事があったのですが、残念ながら記事内のリンク先のgithubのリンクが死んでいてコードを見ることが出来ませんでした。spring-commands-rspecも参考になると思ってみてみましたが、私がspringの仕組み自体全く理解してないのでイマイチでよく分かりませんでした。その内調べて分かったらまた共有します。何かご存じの方いらっしゃいまたらヒントでも結構ですのでコメントお願いします!
- 投稿日:2019-07-08T13:13:39+09:00
複数のherokuアカウント上のRailsアプリを管理・デプロイするための情報まとめ
一台のデバイスで複数のherokuアカウントを管理・デプロイするための情報
余裕があるときに随時加筆修正します。
heroku で複数アカウントを管理するCLIのプラグインを追加する
$ heroku plugins:install heroku-accounts$ heroku accounts:add any_account_name $ heroku accounts:set added_account_nameheroku のssh鍵を追加する
heroku keys:add # 自動で公開健がherokuにupされるheroku にRailsアプリケーションをデプロイする
herokuにアプリケーションを作成する
heroku create app_nameapp_nameは省略可能
アプリ名を省略した場合、heroku側で自動に生成されるこの時自動でremoteのリポジトリ名がherokuに設定される。
もし、違う名前にしたい場合は、heroku git:remote -a app_name -r <any_repository_name>以降、pushするときは
git push <any_repository_name> masterRails アプリケーションの設定
Railsアプリケーションのデータベースをsqlite3からpostgreSQLに変更する。
TIPS
herokuアプリケーションを削除する
herokuのデプロイに失敗したとき、作成したアプリケーションをいったん削除したい場合がある。
その場合は
heroku apps:destroy --app app_nameを実行すればいい。
herokuにRailsアプリケーションをデプロイしたとき、rake taskに失敗するときの対処法
考慮した可能性
- assets precompile関係
- bundler関係 〇
- gem関係
- その他bundler 2.0.1を利用したrailsアプリケーションがherokuにデプロイできなくなったので原因を探ってみたらbundler 2.0.2対応が原因らしい。
つい最近まで、bundler のバージョンが2.0.1でも問題なく動作したが、おそらく2019年6月13日にリリースされたbundler 2.0.2に対応した影響のためか、2.0.1だと
could not find bundler(2.0.1) required by your /tmp/build_xxxxxx....xxxxx/Gemfile.lockとエラーが出る。
この場合、まずローカル環境にあるGemfile.lockを一度削除したのちに、以下のコマンドを打ち
gem install bundler -v 2.0.2bundler(2.0.2)をインストールし、bundle install (--path vendor/bundle)、bundle updateする。
- 投稿日:2019-07-08T12:27:48+09:00
Rails6 のちょい足しな新機能を試す48(Duration編)
はじめに
Rails 6 に追加されそうな新機能を試す第48段。 今回は、
Duration
編です。
Rails 6 では、Duration
の計算で小数点以下が丸められることが無くなりました。Ruby 2.6.3, Rails 6.0.0.rc1, Rails 5.2.3 で確認しました。Rails 6.0.0.rc1 は
gem install rails --prerelease
でインストールできます。$ rails --version Rails 6.0.0.rc1簡単なスクリプトを作る
今回は、動作確認用の簡単なスクリプトを書いて確認します。
bin/duration.rbduration = 0.4.seconds d = DateTime.parse('2019-01-01') d += duration d += duration d += duration d += duration d += duration p d > DateTime.parse('2019-01-01') p d == DateTime.parse('2019-01-01')Rails 5 では
0.4.seconds
を足すとき 整数に丸められて 0.seconds として加算されてしまいます。結果、
0.4.seconds
を5回足す前も後も変化がありません。$ bin/rails runner bin/duration.rb false trueRails 6 では
整数に丸められることが無くなったので、より正確な値が得られるようになります。
$ bin/rails runner bin/duration.rb true false試したソース
試したソースは以下にあります。
https://github.com/suketa/rails6_0_0rc1/tree/try048_duration参考情報
- 投稿日:2019-07-08T11:46:51+09:00
ブロックとスコープを使いこなしたい【メタプログラミングRuby】
はじめに
この記事では
ブロックって何?
からinstance_evalってやつすげー!
までやります。
ブロックとスコープの知識をスッキリさせたい方
にはぴったりだと思っています。ブロックとは
Rubyでは、
do ~ end
、または{ ~ }
の処理の塊のことをブロックと言います。一行で記述されるブロックは
{ ~ }
を使い、複数行のブロックにはdo ~ end
を使う慣習があります。ブロックを定義できるのは、メソッドを呼び出す時だけです。
ブロックはメソッドに渡され、メソッド内でyield
キーワードを使ってブロックをコールバックできます。block.rbdef my_method(a, b) a + yield(a, b) end p my_method(1, 2){ |x, y| x + y } # => 4ブロックの基本的な説明はここまでにして、早速内容に入っていきましょう。
内容
ブロックと束縛
ブロックを呼び出すためには、ローカル変数、インスタンス変数、selfといった
環境
が必要になります。
このオブジェクトに紐づけられている環境の事を束縛
とも言います。つまり、ブロックとは
コードと束縛両方の集まり
という事になります。ブロックにおいて、コードは
ブロック内にあるコード
を使用し、束縛はブロックを定義した場所にある束縛
を使用します。そして、ブロックをメソッドに渡した時は、その束縛も一緒に運ばれていきます。
メソッドにある束縛はブロックからは見る事が出来ません。blocks.rbdef my_method my_var = "my_methodの変数" yield end my_var = "トップレベルの変数" p my_method { my_var } # => "トップレベルの変数"ブロックと束縛を理解するためには、スコープに関する知識を深める必要があります。
スコープゲート
スコープゲートとは、
スコープが変化する境界線
の事です。(スコープに関しての基本的な説明は省きます)
Rubyでは、下記の3つがスコープゲートになります。
- クラス定義
- モジュール定義
- メソッド
実際に下記のコードを見ながら確認していきましょう。
scope_gate.rbtop_level_var = "トップレベルの変数" local_variables # => [:top_level_var, :obj] class MyClass #クラス定義 my_class_var = "マイクラスの変数" local_variables # => [:my_class_var] def my_method # メソッド定義 my_method_var = "マイメソッドの変数" local_variables # => [:my_method_var] end local_variables # => [:my_class_var] end local_variables # => [:top_level_var, :obj]このような感じですね。
それぞれのスコープ内で束縛が変わっている事が確認できると思います。
思わぬ所で、予期せぬ変数が参照されるエラーが発生し難くなり便利ですね。しかし、Rubyを使っていると、スコープゲートを超えて束縛を渡したい局面に遭遇する事があります。
そういう時は
フラットスコープ
を使っていきましょう。スコープゲートを超えて束縛を渡す(フラットスコープ)
クラス定義ではなく、メソッド呼び出しを使用する
事でスコープゲートを超えることが可能になります。
(正確には、スコープゲートを超えているわけではなく、スコープゲートを使用していないだけです)先程の
scope_gate.rb
を書き換えていきましょう。
もしMyClass内でtop_level_var
を使いたい時はこのように書きます。scope_gate.rbtop_level_var = "トップレベルの変数" local_variables # => [:top_level_var, :obj] MyClass = Class.new do #クラス定義 my_class_var = "マイクラスの変数" local_variables # => [:my_class_var, :top_level_var, :obj] def my_method # メソッド定義 my_method_var = "マイメソッドの変数" local_variables # => [:my_method_var] end local_variables # => [:my_class_var, :top_level_var, :obj] end local_variables # => [:top_level_var, :obj]my_method内でmy_class_varを使いたい場合も、先程と同じように、メソッド呼び出しに書き換えます。
scope_gate.rbtop_level_var = "トップレベルの変数" class MyClass #クラス定義 my_class_var = "マイクラスの変数" local_variables # => [:my_class_var] define_method :my_method do # メソッド定義 my_method_var = "マイメソッドの変数" local_variables # => [:my_method_var, :my_class_var] end local_variables # => [:my_class_var] end local_variables # => [:top_level_var, :obj]技術的には、この方法の事を
入れ子構造のレキシカルスコープ
と呼びます。
しかしRubyに限ってはフラットスコープ
と呼ばれる事が多いようです。フラットスコープを応用すれば、自由自在にスコープを組み合わせる事ができます。
共有スコープ
という方法もありますが、上記の2つ(スコープゲート、フラットスコープ)を理解していれば自然と使えるようになる方法なので省きます。(気になる方は調べてみてください)コードと束縛を好きなように組み合わせる(instance_eval)
instance_evalというメソッドがあります。
instance_evalに渡したブロックは、オブジェクトのコンテキストで評価されます。
つまり、レシーバがselfにしてから評価されるので、レシーバのprivateメソッドやインスタンス変数などにもアクセスが可能になるという事です。また、
他のブロックと同じように、instance_evalを定義した時の束縛も見ることが出来ます。
それでは、コードを見ていきましょう。
instance_eval.rbtop_level_var = "トップレベルの変数" class MyClass def initialize @my_instance_var = "マイクラスのインスタンス変数" end end my_class = MyClass.new my_class.instance_eval do self # => #<MyClass:0x00007fc4b08bdcc0 @my_instance_var="マイクラスのインスタンス変数"> top_level_var # => "トップレベルの変数" @my_instance_var # => "マイクラスのインスタンス変数" endこのような感じです。
instance_evalに渡したブロックのことは、
インスタンス探査機
と呼ばれます。
オブジェクトの内部を探査して、そこで実行するコードだからです。まとめ
ブロック
を上手く扱うためには、束縛
を理解する必要がありました。
そして、束縛の住処であるスコープ
も学習しました。
instance_eval
はおまけのような感じです。気になる所や、間違っている所等ありましたらコメントください!!
- 投稿日:2019-07-08T11:46:04+09:00
ActiveRelationでdefault値を設定する方法
前提知識
RailsではActiveRecordがORマピイングシステムでDBとやりとりをしている。ORマッピング
railsで決められている方法で記述するだけで、データベースの種類に関係なく、データを扱える仕組み。
利点
・DBの種類毎に変わる記述方法を気にしなくて済むmigration file
DBに反映させたい情報を記載したfilerails db:migrateを実行すると
timeスタンプの順番に沿って各fileの内容が実行される。schema
DBへ反映させるための情報が記載されたfile
migration fileの情報を反映させたのがschema
schemaがDBと直接やりとりしている。使い方。
ターミナルで以下のコマンドを実行する。
rails generate model モデル名 カラム名:その型モデル名は単数形で先頭は大文字
カラム名:その型、は複数指定できる(書かなくても良い)カラム名:その型を1つも指定しない場合
ex. ) rails generate model User次に
rails db:migrate
を実行する。schemaに以下の様に記述される。カラム名:その型1以上指定した場合
ex. ) rails generate model Player name:string age:integer height:integer作成されるmigrationfile
shemaが更新されます。
名前がplayersのテーブルが作られ、カラムには以下の三つが存在する。
名前にstring型、年齢にinteger型、高さにinteger型を持つカラムさて、本題に入りましょう
大前提として、ターミナルで打つコマンドからはdefault値を指定できません。じゃあ、どうするの?
migration fileをいじくり、それをmigarateすることでdefault値を設定します。
default値を設定したいタイミングは以下の通りが考えられるのでそれに沿って説明します。今回はadminにdefaul値を設定します。
・テーブルまたはカラムをまだ作成していない時
・既存のdefault値を変更する
・既に存在するカラムにdefault値を設定する
- テーブルまたはカラムをまだ作成していない時 rails generate migration User name:string age:integer height:integer admin:string を実行すると以下のmigration fileが作成されます。
adminの後にdefault: faulsを設定してあげます。
後はmigrateを実行すれば完成です。
rails db:migrate
schemaにdefaultが作られていることを確認する。
rollbackが可能(この場合はtableを作る作業の逆、つまり削除が実行される。)
2,・既存のdefault値を変更する
現状は以下の様になっている。
scheme
ここから、trueをfalseに変更したい。
rails generate migration change_column_cats
を実行する。そうすると、以下のmigration fileが作成される。rails db:migrateを実行する。
adminのdefault値がfalseからtrueになってるのが確認できる。ただし、この書き方だとrollbackすることができません。
試しにしてみると、、、
この様に怒られます。
rollbackは元の状態がわかる必要があります。そのため、以下の様に記述を変えてあげるとかぎゃくせいを持たせる事ができます。
以下は時間がある時に追加します。
3.既に存在するカラムにdefault値を設定する1、2の組み合わせで解決できます。
rollbackを気にしない時rollbackを気にする時
SQLでのカラムのdefault値はnullになっています。なので、何も指定しない状態だとfrom:〜〜 to:falseの〜〜部分はnilに設定してあげればOKです。
- 投稿日:2019-07-08T02:19:05+09:00
railsのcredentials.yml.encとRAILS_MASTER_KEY
RAILS_MASTER_KEYってなんだ?
credentials.yml.encをつかう際に、デコードするためのキー
credentials.yml.encってなんだ?
rails5.2より前のバージョンでは、secrets.ymlにて平文で情報を扱っていた。(secretsなのに。)
環境変数を暗号化して入れられるということですね。これまでsecrets.ymlは環境ごとに分割して使われていましたが
credentials.yml.encは本番環境のみで使われる想定のものだそうです。使い方
Rails.application.credentials.hogehogeで情報を取得する事ができます。
- 投稿日:2019-07-08T02:18:25+09:00
amcharts 4 Demos を使ってグラフを作成
About
amcharts関連の日本語文献のあまりの少なさから、誰かのお役に立てればと思い記載しています!
Environment
この記事ではmacbook(unix)にインストールしたruby 2.5.1p57, Rails 5.2.3を使っています。
amchartsとは
amcharts
javascriptでチャートを描画するためのフレームワークです。 棒、エリア、列、バー、パイ、XY、散布、ローソク足のようなチャートを描画することが出来ます。(参考文献)
http://mikawatan.hatenablog.com/entry/2016/10/09/171130Demosとは
amchartsが提供しているサンプルの一覧です。
実際にご覧いただいた方が早いと思います。下記よりご確認ください。
https://www.amcharts.com/demos/使用方法
HTML内でResources(CDN)を記載することで利用できます。
商用利用の場合も、チャート内のamchartsアイコン(amchartsへのリンク)を削除しなければそのまま使用可能です。(参考文献)
https://www.amcharts.com/online-store/
Free license
"Use anywhere you want as long as you don't mind a small amCharts attribution on charts"Rails環境ですが、Demoをわかりやすく、そのまま使用するために、
今回は各viewファイル内に直接記載していきます。index.html.erb<!-- Resources --> <script src="https://www.amcharts.com/lib/4/core.js"></script> <script src="https://www.amcharts.com/lib/4/charts.js"></script> <script src="https://www.amcharts.com/lib/4/themes/animated.js"></script>これで準備完了です。
導入方法
今回は、こちらのチャートを扱います。
ページ内で下にスクロールすると、Demo sourceが置いてありますので、コピーします。Demo_source<!-- Styles --> <style> #chartdiv { width: 100%; height: 500px; } </style> <!-- Resources --> <script src="https://www.amcharts.com/lib/4/core.js"></script> <script src="https://www.amcharts.com/lib/4/charts.js"></script> <script src="https://www.amcharts.com/lib/4/themes/animated.js"></script> <!-- Chart code --> <script> am4core.ready(function() { // Themes begin am4core.useTheme(am4themes_animated); // Themes end // Create chart instance var chart = am4core.create("chartdiv", am4charts.XYChart); chart.scrollbarX = new am4core.Scrollbar(); // Add data chart.data = [{ "country": "USA", "visits": 3025 }, { "country": "China", "visits": 1882 }, { "country": "Japan", "visits": 1809 }, { "country": "Germany", "visits": 1322 }, { "country": "UK", "visits": 1122 }, { "country": "France", "visits": 1114 }, { "country": "India", "visits": 984 }, { "country": "Spain", "visits": 711 }, { "country": "Netherlands", "visits": 665 }, { "country": "Russia", "visits": 580 }, { "country": "South Korea", "visits": 443 }, { "country": "Canada", "visits": 441 }]; // Create axes var categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis()); categoryAxis.dataFields.category = "country"; categoryAxis.renderer.grid.template.location = 0; categoryAxis.renderer.minGridDistance = 30; categoryAxis.renderer.labels.template.horizontalCenter = "right"; categoryAxis.renderer.labels.template.verticalCenter = "middle"; categoryAxis.renderer.labels.template.rotation = 270; categoryAxis.tooltip.disabled = true; categoryAxis.renderer.minHeight = 110; var valueAxis = chart.yAxes.push(new am4charts.ValueAxis()); valueAxis.renderer.minWidth = 50; // Create series var series = chart.series.push(new am4charts.ColumnSeries()); series.sequencedInterpolation = true; series.dataFields.valueY = "visits"; series.dataFields.categoryX = "country"; series.tooltipText = "[{categoryX}: bold]{valueY}[/]"; series.columns.template.strokeWidth = 0; series.tooltip.pointerOrientation = "vertical"; series.columns.template.column.cornerRadiusTopLeft = 10; series.columns.template.column.cornerRadiusTopRight = 10; series.columns.template.column.fillOpacity = 0.8; // on hover, make corner radiuses bigger var hoverState = series.columns.template.column.states.create("hover"); hoverState.properties.cornerRadiusTopLeft = 0; hoverState.properties.cornerRadiusTopRight = 0; hoverState.properties.fillOpacity = 1; series.columns.template.adapter.add("fill", function(fill, target) { return chart.colors.getIndex(target.dataItem.index); }); // Cursor chart.cursor = new am4charts.XYCursor(); }); // end am4core.ready() </script> <!-- HTML --> <div id="chartdiv"></div>お気づきでしょうか?既に使用方法欄で記載した内容は、こちらのDemo sourceに必ず記載されているものです。なので、コピーをそのまま利用される方は、CDNを記載する必要はありません。
それでは、コピーした内容をそのままhtmlにペーストします。
index.html.erb<!--コピー内容をそのままペースト-->
編集方法
このままでは当然ですが、サンプルデータそのままになってしまいます。
各機能について(理解している部分)を記載していきます。Styles
cssです。フォントのサイズ等を指定できますが、指定できる項目には限りがあります。
index.html.erb<!-- Styles --> <style> #chartdiv { width: 100%; height: 500px; font-size: 20px; } </style>chart code
本体です。以下、コメントアウトにて詳細を記載します。
index.html.erb<!-- Chart code --> <script> am4core.ready(function() { // Themes begin // テーマです。 am4core.useTheme(am4themes_animated); // Themes end // Create chart instance //始めにインスタンスを作成しています。 var chart = am4core.create("chartdiv", am4charts.XYChart); //X軸方向のスクロールバーです。私はいらなかったのでコメントアウトしました。 chart.scrollbarX = new am4core.Scrollbar(); // Add data // ここにデータを入力していきます。 chart.data = [{ "country": "USA", "visits": 3025 }, { "country": "China", "visits": 1882 }, { "country": "Japan", "visits": 1809 }, { "country": "Germany", "visits": 1322 }, { "country": "UK", "visits": 1122 }, { "country": "France", "visits": 1114 }, { "country": "India", "visits": 984 }, { "country": "Spain", "visits": 711 }, { "country": "Netherlands", "visits": 665 }, { "country": "Russia", "visits": 580 }, { "country": "South Korea", "visits": 443 }, { "country": "Canada", "visits": 441 }]; // Create axes //ここからはX軸方向について var categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis()); //データフィールドのカテゴリーです。今回はcountryですね。 categoryAxis.dataFields.category = "country"; //この辺りはあまり手を加える必要はないかと思います。 categoryAxis.renderer.grid.template.location = 0; categoryAxis.renderer.minGridDistance = 30; categoryAxis.renderer.labels.template.horizontalCenter = "right"; categoryAxis.renderer.labels.template.verticalCenter = "middle"; //文字の向きです。このままだと270度回転して縦方向の文字列になりますので、 //横向き希望の方は0に設定すると良いです。 categoryAxis.renderer.labels.template.rotation = 270; categoryAxis.tooltip.disabled = true; categoryAxis.renderer.minHeight = 110; //ここからはY軸方向について var valueAxis = chart.yAxes.push(new am4charts.ValueAxis()); valueAxis.renderer.minWidth = 50; // Create series // ようやくチャートの設定です。上記のデータフィールドの項目を使用します。 var series = chart.series.push(new am4charts.ColumnSeries()); series.sequencedInterpolation = true; // Y軸方向はvisits series.dataFields.valueY = "visits"; // X軸方向はcountry series.dataFields.categoryX = "country"; // tooltipはグラフにカーソルを当てると上部に表示される内容です。 series.tooltipText = "[{categoryX}: bold]{valueY}[/]"; series.columns.template.strokeWidth = 0; series.tooltip.pointerOrientation = "vertical"; series.columns.template.column.cornerRadiusTopLeft = 10; series.columns.template.column.cornerRadiusTopRight = 10; series.columns.template.column.fillOpacity = 0.8; // on hover, make corner radiuses bigger // ホバーした際の設定です。 var hoverState = series.columns.template.column.states.create("hover"); hoverState.properties.cornerRadiusTopLeft = 0; hoverState.properties.cornerRadiusTopRight = 0; hoverState.properties.fillOpacity = 1; series.columns.template.adapter.add("fill", function(fill, target) { return chart.colors.getIndex(target.dataItem.index); }); // Cursor // カーソル設定です。非表示にしたい場合は、以下のコメントアウトのように記載します。 chart.cursor = new am4charts.XYCursor(); // chart.cursor.lineX.disabled = true; // chart.cursor.lineY.disabled = true; }); // end am4core.ready() </script>HTML
最後に、HTMLを記載します。
index.html.erb<!-- HTML --> <div id="chartdiv"></div>リンクの貼り方
chartにリンクを貼りたい!という方、必見です。
下記の様な記載で実装できます。index.html.erb<script> // データの項目にidを追加 chart.data = [{ "country": "USA", "visits": 3025, "id": 0 }, { "country": "China", "visits": 1882, "id": 1 }, { "country": "Japan", "visits": 1809, "id": 2 }, { "country": "Germany", "visits": 1322, "id": 3 }, { "country": "UK", "visits": 1122, "id": 4 }]; // script内で下記記載。最後の{}内の記載方法でid毎のページを呼び出せます。 series.columns.template.url = `http://hoge/users/huga/sleeps/{id.urlEncode()}`;実装例
最後に、私が作成した"sommeil"というアプリケーションのコードの内、一部のamcharts部分を参考までに記載します。
こちらのコードでは、オリジナル要素としてチャートのグラフ内から詳細ページに飛べる仕組みを導入しています。
この部分に関して、いくらGoogle先生に聞いても出てこなかったので、記事を執筆しました...。全編はgithubをご覧下さい。
実装イメージについては、グローバルIPで恐縮ですが公開中です。(sommeil)_chart2.html.erb</style> <!-- Resources --> <script src="https://www.amcharts.com/lib/4/core.js"></script> <script src="https://www.amcharts.com/lib/4/charts.js"></script> <script src="https://www.amcharts.com/lib/4/themes/animated.js"></script> <!-- Chart code --> <script> am4core.ready(function () { // Themes begin am4core.useTheme(am4themes_animated); // Themes end // Create chart instance var chart = am4core.create("chartdiv", am4charts.XYChart); //chart.scrollbarX = new am4core.Scrollbar(); // Add data var sleeping_times = '<%= @sleeping_times %>'.replace(/\[/g, "").replace(/\]/g, "").split(','); var dates = '<%= @dates %>'.replace(/\[/g, "").replace(/\]/g, "").replace(/\s/g, "").split(','); var sleep_ids = '<%= @sleep_ids %>'.replace(/\[/g, "").replace(/\]/g, "").replace(/\s/g, "").split( ','); console.log(sleep_ids); chart.data = [{ "day": dates[0], "time": sleeping_times[0], "id": sleep_ids[0] }, { "day": dates[1], "time": sleeping_times[1], "id": sleep_ids[1] }, { "day": dates[2], "time": sleeping_times[2], "id": sleep_ids[2] }, { "day": dates[3], "time": sleeping_times[3], "id": sleep_ids[3] }, { "day": dates[4], "time": sleeping_times[4], "id": sleep_ids[4] }, { "day": dates[5], "time": sleeping_times[5], "id": sleep_ids[5] }, { "day": dates[6], "time": sleeping_times[6], "id": sleep_ids[6] }, { "day": dates[7], "time": sleeping_times[7], "id": sleep_ids[7] }, { "day": dates[8], "time": sleeping_times[8], "id": sleep_ids[8] }, { "day": dates[9], "time": sleeping_times[9], "id": sleep_ids[9] }, { "day": dates[10], "time": sleeping_times[10], "id": sleep_ids[10] }, { "day": dates[11], "time": sleeping_times[11], "id": sleep_ids[11] }, { "day": dates[12], "time": sleeping_times[12], "id": sleep_ids[12] }, { "day": dates[13], "time": sleeping_times[13], "id": sleep_ids[13] }]; // Create axes var categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis()); categoryAxis.dataFields.category = "day"; categoryAxis.renderer.grid.template.location = 0; categoryAxis.renderer.minGridDistance = 30; categoryAxis.renderer.labels.template.horizontalCenter = "middle"; categoryAxis.renderer.labels.template.verticalCenter = "middle"; categoryAxis.renderer.labels.template.rotation = 0; categoryAxis.renderer.labels.template.fill = am4core.color("#fff"); categoryAxis.tooltip.disabled = true; categoryAxis.renderer.minHeight = 110; var valueAxis = chart.yAxes.push(new am4charts.ValueAxis()); valueAxis.renderer.minWidth = 50; valueAxis.renderer.labels.template.fill = am4core.color("#fff"); // Create series var series = chart.series.push(new am4charts.ColumnSeries()); series.sequencedInterpolation = true; series.dataFields.valueY = "time"; series.dataFields.categoryX = "day"; series.tooltipText = "[{categoryX}: bold]{valueY}[/]"; series.columns.template.strokeWidth = 0; series.tooltip.pointerOrientation = "vertical"; //Go to detail page var current_user_id = '<%= @user.id %>' series.columns.template.url = `http://52.194.48.235/users/${current_user_id}/sleeps/{id.urlEncode()}`; series.columns.template.column.cornerRadiusTopLeft = 10; series.columns.template.column.cornerRadiusTopRight = 10; series.columns.template.column.fillOpacity = 0.8; // on hover, make corner radiuses bigger var hoverState = series.columns.template.column.states.create("hover"); //hoverState.properties.cornerRadiusTopLeft = 0; //hoverState.properties.cornerRadiusTopRight = 0; hoverState.properties.fillOpacity = 1; series.columns.template.adapter.add("fill", function (fill, target) { return chart.colors.getIndex(target.dataItem.index); }); // Cursor chart.cursor = new am4charts.XYCursor(); chart.cursor.lineX.disabled = true; }); // end am4core.ready() </script> <!-- HTML --> <div id="chartdiv"></div>最後に
合わせてpiechartの作成方法なども近日公開予定です。
ご覧いただき、ありがとうございました。筆者について
TECH::EXPERTにて4月よりruby, railsを学習している未経験エンジニアです。
記載内容に不備・不足があればご指摘いただけると幸いです。
至らぬ点ばかりですので、改善点がありましたらどんどんご指摘下さい!
- 投稿日:2019-07-08T02:02:21+09:00
capistrano3でpumaの運用ー設定ファイルの管理
railsユーザの多くがつかってるのではないかと思われるcapistranoとpuma。この組み合わせで、どうやってpuma起動時に任意の設定を指定するか、意外とわけわからんのでまとめた。
pumaのデフォルト仕様
なにも指定せずにpuma起動すると、
config/puma/{env}.rb
,config/puma.rb
, の優先順位で設定ファイルが読まれる。
puma/configuration.rb at 0cd28f373a245eca4e717bff1415b97bbc2356d2 · puma/puma · GitHubちなみに、ローカル環境などで
rails server
したときはどうも、puma
,thin
,webrick
の順にrackハンドラを探して見つかったやつで起動するっぽい。最近のrailsではrails new
するとpumaが入るので、特にいじらないかぎりrails server
したときにはpumaが起動すると思っていい。
rack/handler.rb at 2.0.7 · rack/rack · GitHub% rails server => Booting Puma => Rails 5.2.3 application starting in development => Run `rails server -h` for more startup options Puma starting in single mode...つまり、ローカルで起動するpumaの挙動を変更したい場合は、
config/puma/development.rb
をつくるか、config/puma.rb
をつくってrails server
起動すればいい。capistranoをつかう場合
しかし、capistrano3-pumaをデフォルト設定でつかう場合、
config/puma/{env}.rb
,config/puma.rb
は完全無視される。1
代わりに、capistrano3-pumaが持つ設定ファイル生成機能(後述)をもとに新規設定ファイルを作成し、デプロイサーバにアップロードし、それをpuma起動時に読むようになる。ここでcapistrano & pumaの設定ファイルの運用方針が、大きく2つの道にわかれる。
pumaオリジナル
config/puma/{env}.rb
,config/puma.rb
で運用するcapistrano3-pumaが提供する設定ファイル機能をつかわずに、従来のpumaの運用方法でやる方法。設定を緻密に詳細に指定したいときはこっちのほうがたぶんやりやすい。
必要な設定を逐一記述して、リポジトリにpushしておく。2
あとは、deploy.rb
またはdeploy/{env}.rb
に、設定ファイルのパスpuma_conf
を設定すればいい。
Why it builds a new puma.config instead of using config/puma/production.rb? (question) · Issue #244 · seuros/capistrano-puma · GitHub具体的にはこういう設定。
set :deploy_to, '/path/to/app' set :puma_conf, "#{current_path}/config/puma/staging.rb" # `current_path`の値は`deploy_to`によって決まるので、 # `puma_conf`は`deploy_to`よりあとに指定する点に注意!こうすると、capistrano経由でpumaを起動するときに、
-C /path/to/app/current/config/puma/staging.rb
が指定されてくれる。% bundle exec cap staging puma:start 00:00 puma:start using conf file /path/to/app/current/config/puma/staging.rb 01 bundle exec puma -C /path/to/app/current/config/puma/staging.rb --daemon ...capistrano3-pumaの設定ファイル生成機能をつかって運用する
capistrano3-pumaには設定ファイル生成機能
puma:config
がある。pumaの設定をがっちりかけなくてもそれなりの設定で動いてくれる。
cap {env} deploy:check
が実行されるときに、存在しなければ/path/to/app/shared/puma.rb
(またはpuma_conf
に指定されたパス)を自動生成して各サーバにアップロードする。3具体的にはこのとき、cap {env} puma:config
というタスクが実行されている。
cap {env} puma:config
を単体で実行すると、既存ファイルが存在するしないに関係なくcapistrano管理上の設定で上書きされる。ふだんcapistrano運用者がつかうコマンドは
cap {env} deploy
(内部的にdeploy:check
を実行する)のはずなので、設定ファイルははじめの一回だけ自動生成され、その後都度更新されることはない。
だから、サーバ上の設定を手動で直接かきかえる運用も可能。ただし、そういう運用をおこなう場合は、cap {env} puma:config
の実行に注意。手動で加えた設定は消されてしまうので。% bundle exec cap staging puma:config 00:00 puma:config Uploading /path/to/app/shared/puma.rb 100.0% # /path/to/app/shared/puma.rb # を確認すると、設定ファイルが追加されてたり、上書きされてるのがわかる # /path/to/app/shared/puma.rb # ちなみに、このパスを変えたい場合は、前述した要領で`puma_conf`を指定すればいいcapistrano管理上のpuma設定を上書きする
cap {env} puma:config
が実行されたときに生成されるファイルは、デフォルトでは以下に定義された値がコピーされる。capistrano-puma/puma.rb at v3.1.1 · seuros/capistrano-puma · GitHub
これらの設定値はもちろんcapistrano設定で全部上書きできる。
config/deploy.rb
やconfig/deploy/{env}.rb
に以下のように記述する。(すでに紹介したpuma_conf
の上書きと同じ要領)set :puma_env, 'staging' set :puma_threads, [4, 4] set :puma_workers, 0 set :puma_preload_app, false設定可能な設定値はこちらのリストも参考
GitHub - seuros/capistrano-puma: Puma integration for Capistrano念のためもう一度言うと、ここで設定した設定値はpuma起動時に直接読まれるわけではない。
puma:config
が実行されて、shared/puma.rb
(またはpuma_conf
で指定されたパス)ファイルにその値がハードコードされ、それが起動時(cap {env} puma:start
)に読み込まれることによってpumaに渡る。
つまり、puma_threads
,puma_workers
などのcapistrano管理上のpuma設定は、cap {env} puma:config
が実行されてはじめてリリースされる。capistrano管理上のpuma設定をプログラムする
ここまで見ると、設定ファイル生成機能はライトにつかえる。しかしpumaのチューニングに余念がない人々にとっては、これだけでは足りない点がある。
puma設定には、設定の「値」だけではなく、「プログラム」がかけるのである。たとえば、pumaプロセスがフォークしたときのコールバックなどをカスタム設定できる仕組みになっている。
GitHub - puma/puma: A Ruby/Rack web server built for concurrencyこういうかんじで。
on_worker_boot do # configuration here end before_fork do # configuration here endこういうことをやりたくなったら4、はじめのpumaオリジナル設定ファイルの運用に切り替えるのをおすすめするが、一応設定ファイル生成機能をつかってもできる。
cap {env} puma:config
が実行されるときに、生成される設定ファイルのテンプレートがこれ。
capistrano-puma/puma.rb.erb at v3.1.1 · seuros/capistrano-puma · GitHubつまり、このテンプレートを差し替えることができれば、設定だけじゃなくてプログラムも自由にかける。
上記のテンプレートを参考に、config/deploy/templates
またはlib/capistrano/templates
以下にカスタムテンプレートをおけばいい。これらのディレクトリがどこで決まるのかは以下でわかる。
capistrano-puma/puma.rb at v3.1.1 · seuros/capistrano-puma · GitHub
config/deploy/templates/puma.rb
に以下のようなテンプレートを用意して(このテンプレートはbundle exec cap
が実行されるローカルになければいけない点に注意)#!/usr/bin/env puma puts "Hello!! This is custom puma template!!" directory '<%= current_path %>' rackup "<%=fetch(:puma_rackup)%>" environment '<%= fetch(:puma_env) %>' pidfile "<%=fetch(:puma_pid)%>" state_path "<%=fetch(:puma_state)%>"
puma:config
を実行% be cap staging puma:config 00:00 puma:config Uploading /path/to/app/current/config/puma/staging.rb 100.0%
shared/puma.rb
にリリースされてる。#!/usr/bin/env puma puts "Hello!! This is custom puma template!!" directory '/path/to/app/current' rackup "/path/to/app/current/config.ru" environment 'staging' pidfile "/path/to/app/shared/tmp/pids/puma.pid" state_path "/path/to/app/shared/tmp/pids/puma.state"
pumaのふるまいにはぱっと見変更が反映されたのかされてないのかわからない類のものも多いので、これではまる人は多い気がする。そして設定ファイルのパスに環境差分がおきてしまうのも歯がゆい! ↩
もしサーバごとに異なるpuma設定で運用したい場合、
git push
するのはよくない。 ↩
deploy:check
時に自動生成する機能は、たぶんCapfile
にinstall_plugin Capistrano::Puma
と書いていれば有効になっている。 ↩よくおこなわれるカスタム設定はすでにデフォルトのテンプレートに組み込まれてるので、ここを参照。capistrano-puma/puma.rb.erb at v3.1.1 · seuros/capistrano-puma · GitHub ↩
- 投稿日:2019-07-08T02:02:21+09:00
capistrano3でpumaの運用ー設定ファイルの管理どうする?
railsユーザの多くがつかってるのではないかと思われるcapistranoとpuma。この組み合わせで、どうやってpuma起動時に任意の設定を指定するか、意外とわけわからんのでまとめた。
pumaのデフォルト仕様
なにも指定せずにpuma起動すると、
config/puma/{env}.rb
,config/puma.rb
, の優先順位で設定ファイルが読まれる。
puma/configuration.rb at 0cd28f373a245eca4e717bff1415b97bbc2356d2 · puma/puma · GitHubちなみに、ローカル環境などで
rails server
したときはどうも、puma
,thin
,webrick
の順にrackハンドラを探して見つかったやつで起動するっぽい。最近のrailsではrails new
するとpumaが入るので、特にいじらないかぎりrails server
したときにはpumaが起動すると思っていい。
rack/handler.rb at 2.0.7 · rack/rack · GitHub% rails server => Booting Puma => Rails 5.2.3 application starting in development => Run `rails server -h` for more startup options Puma starting in single mode...つまり、ローカルで起動するpumaの挙動を変更したい場合は、
config/puma/development.rb
をつくるか、config/puma.rb
をつくってrails server
起動すればいい。capistranoをつかう場合
しかし、capistrano3-pumaをデフォルト設定でつかう場合、
config/puma/{env}.rb
,config/puma.rb
は完全無視される。1
代わりに、capistrano3-pumaが持つ設定ファイル生成機能(後述)をもとに新規設定ファイルを作成し、デプロイサーバにアップロードし、それをpuma起動時に読むようになる。ここでcapistrano & pumaの設定ファイルの運用方針が、大きく2つの道にわかれる。
pumaオリジナル
config/puma/{env}.rb
,config/puma.rb
で運用するcapistrano3-pumaが提供する設定ファイル機能をつかわずに、従来のpumaの運用方法でやる方法。設定を緻密に詳細に指定したいときはこっちのほうがたぶんやりやすい。
必要な設定を逐一記述して、リポジトリにpushしておく。2
あとは、deploy.rb
またはdeploy/{env}.rb
に、設定ファイルのパスpuma_conf
を設定すればいい。
Why it builds a new puma.config instead of using config/puma/production.rb? (question) · Issue #244 · seuros/capistrano-puma · GitHub具体的にはこういう設定。
set :deploy_to, '/path/to/app' set :puma_conf, "#{current_path}/config/puma/staging.rb" # `current_path`の値は`deploy_to`によって決まるので、 # `puma_conf`は`deploy_to`よりあとに指定する点に注意!こうすると、capistrano経由でpumaを起動するときに、
-C /path/to/app/current/config/puma/staging.rb
が指定されてくれる。% bundle exec cap staging puma:start 00:00 puma:start using conf file /path/to/app/current/config/puma/staging.rb 01 bundle exec puma -C /path/to/app/current/config/puma/staging.rb --daemon ...capistrano3-pumaの設定ファイル生成機能をつかって運用する
capistrano3-pumaには設定ファイル生成機能
puma:config
がある。pumaの設定をがっちりかけなくてもそれなりの設定で動いてくれる。
cap {env} deploy:check
が実行されるときに、存在しなければ/path/to/app/shared/puma.rb
(またはpuma_conf
に指定されたパス)を自動生成して各サーバにアップロードする。3具体的にはこのとき、cap {env} puma:config
というタスクが実行されている。
cap {env} puma:config
を単体で実行すると、既存ファイルが存在するしないに関係なくcapistrano管理上の設定で上書きされる。ふだんcapistrano運用者がつかうコマンドは
cap {env} deploy
(内部的にdeploy:check
を実行する)のはずなので、設定ファイルははじめの一回だけ自動生成され、その後都度更新されることはない。
だから、サーバ上の設定を手動で直接かきかえる運用も可能。ただし、そういう運用をおこなう場合は、cap {env} puma:config
の実行に注意。手動で加えた設定は消されてしまうので。% bundle exec cap staging puma:config 00:00 puma:config Uploading /path/to/app/shared/puma.rb 100.0% # /path/to/app/shared/puma.rb # を確認すると、設定ファイルが追加されてたり、上書きされてるのがわかる # /path/to/app/shared/puma.rb # ちなみに、このパスを変えたい場合は、前述した要領で`puma_conf`を指定すればいいcapistrano管理上のpuma設定を上書きする
cap {env} puma:config
が実行されたときに生成されるファイルは、デフォルトでは以下に定義された値がコピーされる。capistrano-puma/puma.rb at v3.1.1 · seuros/capistrano-puma · GitHub
これらの設定値はもちろんcapistrano設定で全部上書きできる。
config/deploy.rb
やconfig/deploy/{env}.rb
に以下のように記述する。(すでに紹介したpuma_conf
の上書きと同じ要領)set :puma_env, 'staging' set :puma_threads, [4, 4] set :puma_workers, 0 set :puma_preload_app, false設定可能な設定値はこちらのリストも参考
GitHub - seuros/capistrano-puma: Puma integration for Capistrano念のためもう一度言うと、ここで設定した設定値はpuma起動時に直接読まれるわけではない。
puma:config
が実行されて、shared/puma.rb
(またはpuma_conf
で指定されたパス)ファイルにその値がハードコードされ、それが起動時(cap {env} puma:start
)に読み込まれることによってpumaに渡る。
つまり、puma_threads
,puma_workers
などのcapistrano管理上のpuma設定は、cap {env} puma:config
が実行されてはじめてリリースされる。capistrano管理上のpuma設定をプログラムする
ここまで見ると、設定ファイル生成機能はライトにつかえる。しかしpumaのチューニングに余念がない人々にとっては、これだけでは足りない点がある。
puma設定には、設定の「値」だけではなく、「プログラム」がかけるのである。たとえば、pumaプロセスがフォークしたときのコールバックなどをカスタム設定できる仕組みになっている。
GitHub - puma/puma: A Ruby/Rack web server built for concurrencyこういうかんじで。
on_worker_boot do # configuration here end before_fork do # configuration here endこういうことをやりたくなったら4、はじめのpumaオリジナル設定ファイルの運用に切り替えるのをおすすめするが、一応設定ファイル生成機能をつかってもできる。
cap {env} puma:config
が実行されるときに、生成される設定ファイルのテンプレートがこれ。
capistrano-puma/puma.rb.erb at v3.1.1 · seuros/capistrano-puma · GitHubつまり、このテンプレートを差し替えることができれば、設定だけじゃなくてプログラムも自由にかける。
上記のテンプレートを参考に、config/deploy/templates
またはlib/capistrano/templates
以下にカスタムテンプレートをおけばいい。これらのディレクトリがどこで決まるのかは以下でわかる。
capistrano-puma/puma.rb at v3.1.1 · seuros/capistrano-puma · GitHub
config/deploy/templates/puma.rb
に以下のようなテンプレートを用意して(このテンプレートはbundle exec cap
が実行されるローカルになければいけない点に注意)#!/usr/bin/env puma puts "Hello!! This is custom puma template!!" directory '<%= current_path %>' rackup "<%=fetch(:puma_rackup)%>" environment '<%= fetch(:puma_env) %>' pidfile "<%=fetch(:puma_pid)%>" state_path "<%=fetch(:puma_state)%>"
puma:config
を実行% be cap staging puma:config 00:00 puma:config Uploading /path/to/app/current/config/puma/staging.rb 100.0%
shared/puma.rb
にリリースされてる。#!/usr/bin/env puma puts "Hello!! This is custom puma template!!" directory '/path/to/app/current' rackup "/path/to/app/current/config.ru" environment 'staging' pidfile "/path/to/app/shared/tmp/pids/puma.pid" state_path "/path/to/app/shared/tmp/pids/puma.state"
pumaのふるまいにはぱっと見変更が反映されたのかされてないのかわからない類のものも多いので、これではまる人は多い気がする。そして設定ファイルのパスに環境差分がおきてしまうのも歯がゆい! ↩
もしサーバごとに異なるpuma設定で運用したい場合、
git push
するのはよくない。 ↩
deploy:check
時に自動生成する機能は、たぶんCapfile
にinstall_plugin Capistrano::Puma
と書いていれば有効になっている。 ↩よくおこなわれるカスタム設定はすでにデフォルトのテンプレートに組み込まれてるので、ここを参照。capistrano-puma/puma.rb.erb at v3.1.1 · seuros/capistrano-puma · GitHub ↩
- 投稿日:2019-07-08T02:00:47+09:00
[WIP]RedisとSidekiqの説明
SidekiqとRedisを知るために、情報をまとめた記事です。
sidekiqとは
sidekiqとは、非同期処理を行いたい時に使うライブラリである。
Railsが必須というわけではないようですが、基本的にはRailsで使います。sidekiqの使い方
1.まず、sidekiqを追加します。
gem 'sidekiq'2.app/workersにワーカーファイルを作成します。
rails g sidekiq:worker Hard #このコマンドがapp/workers/hard_worker.rbファイルを作成しますclass HardWorker include Sidekiq::Worker def perfome(name, count) # do something end end3.非同期処理が行われるように、ジョブを作成します
HardWorker.perform_async('bob', 5)"perform"はインスタンスメソッドです。
他にも下記のようなインスタンスメソッドがあります。HardWorker.perfome_in(5.minutes, 'bob', 5) HardWorker.perfome_at(5.minutes.from_now, 'bob', 5)メモ:もしsidekiqと使うときにrails consoleで使う場合は、springを止める事で新しいワーカーを使う事ができます。ワーカーの初期化かな?
4.sidekiqを動かすには下記コマンドを打ちましょう
bundle exec sidekiqsidekiqにはpro版とかがある
Pro版とEnterprise版とで機能などが変わってくるそうです。
https://sidekiq.org/Redisとは
Key-Valueストアデータベースです。
オープンソースのインメモリ型です。データ保存方法の特徴
コンピュータの記憶領域として、
- CPUから直接アクセスできるメインメモリ
- ハードディスクなどのディスクストレージ
の2つがあります。
インメモリ・データベースとは、メインメモリ上だけでデータを管理する仕組みです。
メモリは「揮発性」という性質により、コンピューターが停止すると、メモリに載っている情報も全て消えてしまいます。しかしRedisではデータ永続化機能のオプションがあり、メインメモリ上のデータをディスクストレージに記憶させる事ができます。Redisで保存できるデータ
Redisでは
- リスト型
- セット型
- ハッシュ型
のデータも扱う事ができます。Redisの用途
キャッシュ
- DBクエリ結果のキャッシュ
- 永続的なセッションのキャッシュ
- Webページのキャッシュ
- 画像
- ファイル
- メタデータ
チャット
リアルタイムでのコメントストリーム
ソーシャルメディアフィードゲームのリーダーボード
リアルタイムでのランキングリストを作成する
セッションストア
セッション状態のなどを管理する時
リッチメディアストリーミング
(とにかく認証系を素早く大量に処理できる、という事でしょうか・・・)
地理空間
(地理空間データを簡単・高速に処理する事ができるそうです。位置情報などをリアルタイムに更新する際などには良さそうですね。)
Machine Learning
作成されたデータに対して、並行して機械学習・データ処理を行う訳ですが、その処理を行う際に、迅速に処理する事ができるという点ではRedisは有用なようです。
リアルタイム分析
断続的に更新されるデータを素早く処理するという意味で、Redisは有用そうです。
こうして見ると、Redisは、メモリの特徴を踏まえればわかるように、リアルタイム処理などに有効に働くようです。
競合ライブラリ
MemcachedもインメモリのOSSデータストアです。
参照
https://github.com/mperham/sidekiq/wiki/Getting-Started
https://aws.amazon.com/jp/redis/
- 投稿日:2019-07-08T01:06:42+09:00
今日学んだ事
7/7(日) 21:30〜0:20
●タグ
html.erb<span>〜</apan>・span囲った部分をインライン要素としてグループ化することができるタグ。
グループ化する事で、指定した範囲にスタイルシートを適用できたりする。html.erb<i>〜</i>・フォントを「イタリック体」にする
●averageメソッド
・ActiveRecordクラスのメソッド
使用例
カラム=score
テーブル=students
クラス=Student
scoreカラムのaverageを求めるstudents = students.all students.average.(:score) #scoreの平均を小数点ありの状態で返す小数点を四捨五入する場合
students = students.all students.average.(:score) round #roundメソッドを使用すると小数点を四捨五入する●present?メソッド
・配列の中身がなければfalseを返す
・レビューの評価の平均など出すときに、レビューがない作品にfalseを返したい時などに使う
例index.html.erb<% if product.reviews.present? %>このコードでレビューがない場合でもエラーが回避できる
●レシーバ
・インスタンスメソッドを利用するインスタンス自身の事
例products = Product.all #以下の式のレシーバはproducts products.limit(5)1.Productクラスからproductsインスタンスを生成
2.productsインスタンス自身がlimitメソッドを利用している
3.この時productsがレシーバとなる
ち・な・み・に
メソッドの呼び出しを「オブジェクト(インスタンス)にメッセージ(メソッド)を送る」と表現する。
今回の場合
「products(オブジェクト)にlimit(メッセージ)を送る」となる●selfメソッド
・インスタンスメソッド内にselfを記述すると、そのメソッドを利用したインスタンス(レシーバ)が代入された変数のように扱う事ができる
例def review.average self.review.average(:rate).roundうーん、よく分からない。。。。
●コントローラ内に記述されているlayout 'レイアウトファイル名'について
例
1.class MovieController < ApplicationController 2. layout 'movie' 3. 4. def index 5. @movies = Movie.all 6. end 7.end・MovieControllerのindexアクションが呼ばれたときに表示されるレイアウトはmovie.html.erbとなる。
・なにも指定しないとレイアウトファイルはapplication.html.erbとなる以上
- 投稿日:2019-07-08T00:20:17+09:00
has_manyのidsを使って、簡単に中間テーブルの関連付けを行う
概要
チェックボックスを複数選択し、その情報を配列で受け取って、中間テーブルの関連付けを行います。
E-R図
※今回説明で使用するアプリとカラム数があっていませんが、中間テーブルの使い方として掲載します。事前準備
ToDo系アプリ
を想定して進めます。
画像のようなタスクを新規作成する画面を用意しました。
今回の話で使用するのは、Labelの複数のチェックボックスから値を取ってくる
方法です。
view
app/views/_form.html.erb<tr> <th>Label</th> <td> <% Label.all.each do |label| %> <%= form.check_box :label_ids, { multiple: true, checked: @task.labels.find_by(id: label.id).present?, include_hidden: false }, label[:id] %> <label class='badge badge-secondary'><%= label.name %></label> <% end %> </td> </tr>
form.check_box :label_ids
と記述すると、paramsの値として、"label_ids"=>["7", "8", "10"]
このような形でハッシュのキーを持つ値を送れます。
multiple
オプションを使用することで、複数のチェックボックスのパラメータを配列形式で送れます。
checked: @task.labels.find_by(id: label.id).present?
これはeditで使用することを想定しており、編集するタスク
に紐付いているラベル
にチェックを付けるということをしています。
include_hidden: false
チェックしていない項目については、パラメータを送らないオプションです。model
app/models/task.rbclass Task < ApplicationRecord has_many :labelings, dependent: :destroy has_many :labels, through: :labelings endapp/models/labeling.rbclass Labeling < ApplicationRecord belongs_to :task belongs_to :label endapp/models/label.rbclass Label < ApplicationRecord has_many :labelings, dependent: :destroy endcontroller
app/controllers/tasks_controller.rbdef create @task = Task.new(task_params) if @task.save redirect_to root_path, notice: '新しいタスクを作成しました' else render :new end end #省略 private def task_params params.require(:task).permit(:subject, :content, :expired_at, :state, :priority, :user_id, label_ids: []) end
label_ids: []
複数チェックボックスの値を配列で渡すことを許可しています。タスク新規作成時に、ラベルを複数つける処理の動き
もう一度タスク新規投稿の画面を貼ります。
Labelの開発
・DB操作
・次回MTGまで
にチェックを入れた状態で登録ボタンを押します。
この3つのチェックボックスの値を取得します。まず
params
には次のような値が送られますParameters: { "utf8"=>"✓", "authenticity_token"=>"WoRUP+q21xCR760x9L3Y9lyLeQ+THkS9YOGISfGRyDRg0xaI1D2BNnRQLTuaHANpw+NCohg06QtB5U4RxruO5g==", "task"=>{ "subject"=>"タスク新規作成", "content"=>"ラベルを複数登録", "expired_at(1i)"=>"2019", "expired_at(2i)"=>"7", "expired_at(3i)"=>"8", "expired_at(4i)"=>"10", "expired_at(5i)"=>"00", "user_id"=>"87", "state"=>"着手中", "priority"=>"high", "label_ids"=>["7", "8", "10"]}, "commit"=>"登録する" }次に、controllerのcreateアクション時にbinding.pryで値を確認します。
def create @task = Task.new(task_params) binding.pry if @task.save redirect_to root_path, notice: '新しいタスクを作成しました' else render :new end end# idsだけ確認 pry(#<TasksController>)> @task.label_ids => [7, 8, 10] # newメソッドで作られたlabelsを確認 pry(#<TasksController>)> @task.labels => [#<Label:0x00007f92cfe32628 id: 7, name: "開発", created_at: Fri, 05 Jul 2019 15:28:18 JST +09:00, updated_at: Sun, 07 Jul 2019 23:10:17 JST +09:00>, #<Label:0x00007f92cfe32470 id: 8, name: "DB操作", created_at: Fri, 05 Jul 2019 15:28:18 JST +09:00, updated_at: Sun, 07 Jul 2019 23:10:24 JST +09:00>, #<Label:0x00007f92cfe322e0 id: 10, name: "次回MTGまで", created_at: Fri, 05 Jul 2019 15:28:18 JST +09:00, updated_at: Sun, 07 Jul 2019 23:11:06 JST +09:00>] pry(#<TasksController>)> @task.save (0.9ms) BEGIN ↳ (pry):5 CACHE User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 87], ["LIMIT", 1]] ↳ (pry):5 Task Create (33.3ms) INSERT INTO "tasks" ("subject", "content", "created_at", "updated_at", "expired_at", "state", "priority", "user_id") VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING "id" [["subject", "タスク新規作成"], ["content", "ラベルを複数登録"], ["created_at", "2019-07-07 23:59:15.509944"], ["updated_at", "2019-07-07 23:59:15.509944"], ["expired_at", "2019-07-08 10:00:00"], ["state", "着手中"], ["priority", 0], ["user_id", 87]] ↳ (pry):5 Labeling Create (0.9ms) INSERT INTO "labelings" ("task_id", "label_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["task_id", 212449], ["label_id", 7], ["created_at", "2019-07-07 23:59:15.553820"], ["updated_at", "2019-07-07 23:59:15.553820"]] ↳ (pry):5 Labeling Create (0.3ms) INSERT INTO "labelings" ("task_id", "label_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["task_id", 212449], ["label_id", 8], ["created_at", "2019-07-07 23:59:15.556142"], ["updated_at", "2019-07-07 23:59:15.556142"]] ↳ (pry):5 Labeling Create (0.2ms) INSERT INTO "labelings" ("task_id", "label_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["task_id", 212449], ["label_id", 10], ["created_at", "2019-07-07 23:59:15.557177"], ["updated_at", "2019-07-07 23:59:15.557177"]] ↳ (pry):5 (0.8ms) COMMIT ↳ (pry):5 => true pry(#<TasksController>)> @task.id => 212449 pry(#<TasksController>)> Labeling.where(task_id: 212449) Labeling Load (0.2ms) SELECT "labelings".* FROM "labelings" WHERE "labelings"."task_id" = $1 [["task_id", 212449]] ↳ app/controllers/tasks_controller.rb:38 => [#<Labeling:0x00007f92d142e9e0 id: 11, task_id: 212449, label_id: 7, created_at: Sun, 07 Jul 2019 23:59:15 JST +09:00, updated_at: Sun, 07 Jul 2019 23:59:15 JST +09:00>, #<Labeling:0x00007f92d142e8a0 id: 12, task_id: 212449, label_id: 8, created_at: Sun, 07 Jul 2019 23:59:15 JST +09:00, updated_at: Sun, 07 Jul 2019 23:59:15 JST +09:00>, #<Labeling:0x00007f92d142e760 id: 13, task_id: 212449, label_id: 10, created_at: Sun, 07 Jul 2019 23:59:15 JST +09:00, updated_at: Sun, 07 Jul 2019 23:59:15 JST +09:00>]無事1度のcreateアクションで、中間テーブルに3個保存されました!
タスク編集時に、複数のラベルを変更した時の、中間テーブルの動き
ここまで新規作成時の中間テーブルの関連付けの説明をしました。
では、更新する時はどうやるの?
外すラベルのid調べてテーブルからdeleteしてinsertするの?といった疑問が浮かぶと思います。
実はdeleteとかはいい感じに勝手にやってくれます!
実際の動きを確認してみました
新規作成時に開発
・DB操作
・次回MTGまで
にチェックを入れてます。
viewにchecked: @task.labels.find_by(id: label.id).present?
を記載しているので、デフォルトでチェックが入っています
DB操作
・次回MTGまで
のチェックボックスを外し、
新たに検討中
にチェックしました。
更新するボタンを押します。(0.2ms) BEGIN ↳ app/controllers/tasks_controller.rb:52 Label Load (0.3ms) SELECT "labels".* FROM "labels" WHERE "labels"."id" IN ($1, $2) [["id", 7], ["id", 9]] ↳ app/controllers/tasks_controller.rb:52 Labeling Destroy (0.3ms) DELETE FROM "labelings" WHERE "labelings"."task_id" = $1 AND "labelings"."label_id" IN ($2, $3) [["task_id", 212449], ["label_id", 8], ["label_id", 10]] ↳ app/controllers/tasks_controller.rb:52 Labeling Create (0.4ms) INSERT INTO "labelings" ("task_id", "label_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["task_id", 212449], ["label_id", 9], ["created_at", "2019-07-08 00:08:31.021375"], ["updated_at", "2019-07-08 00:08:31.021375"]] ↳ app/controllers/tasks_controller.rb:52 User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 87], ["LIMIT", 1]] ↳ app/controllers/tasks_controller.rb:52 (2.1ms) COMMIT ↳ app/controllers/tasks_controller.rb:52すると、中間テーブルからチェックボックスを外したlabel_idがDestroyされた後にCreateされています。