- 投稿日:2020-12-04T23:31:55+09:00
【Rails】Gemを初公開しました
Gemを初公開しました
ask_year_monthという、
レシーバが何月なのか、
スマートにチェックするメソッドを生やしただけのGemを公開しました。
例えば、
Time.current.month == 12 #=> trueよりも、
Time.current.december? #=> trueのが見やすいよなーという所から。
ご興味がある方は良かったら使ってみてください。
RubyGems
https://rubygems.org/gems/ask_year_month
GitHub
https://github.com/mah666hhh/ask_year_month
参考記事
RailsプラグインGemの作成方法、RSpecテストまで含めたrails pluginの作り方
- 投稿日:2020-12-04T23:31:55+09:00
【Rails】Gemを初公開しました。何月なのかを月名で確認するメソッドを生成
Gemを初公開しました
ask_year_monthという、
レシーバが何月なのか、
月名でスマートにチェックできるメソッドを生やすGemを公開しました。
月名で確認できなくて地味にモヤッとしたんですよね。
例えば、
Time.current.month == 12 #=> trueよりも、
Time.current.december? #=> trueのが見やすいよなーという。
ご興味がある方は良かったら使ってみてください。
RubyGems
https://rubygems.org/gems/ask_year_month
GitHub
https://github.com/mah666hhh/ask_year_month
参考記事
RailsプラグインGemの作成方法、RSpecテストまで含めたrails pluginの作り方
- 投稿日:2020-12-04T22:58:42+09:00
DockerでRails6(MySQL, Webpacker)を動かす。
Docker練習第二弾。Rails5は結構やっていたのでRails6を動かしてみようと思います。
環境
- Docker 19.03.13
- docker-compose 1.27.4
- Windows10 Pro
ディレクトリ構成
rails_test/ ┝ Dockerfile ┝ docker-compose.yml ┝ Gemfile ┝ Gemfile.lock ┝ environments/ └ db.envDockerfile
DockerfileFROM ruby:2.6 RUN apt-get update -qq && \ apt-get install -y build-essential \ nodejs RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ apt-get update && apt-get install yarn RUN mkdir /rails_test WORKDIR /rails_test ADD Gemfile /rails_test/Gemfile ADD Gemfile.lock /rails_test/Gemfile.lock RUN bundle install ADD . /rails_test
- FROM Ruby2.6をプルします。
- RUN Railsに必要なNode.jsとyarnをインストール。 作業ディレクトリの作成。
- WORKDIR 作業ディレクトリの指定をします。
- ADD ローカルのGemfileとGemfile.lockをコンテナにコピーします。
- RUN バンドルインストールをします。
- ADD Gemfileをローカルに反映します。
今回webpackerを使うためにyarnをインストールするのポイントでした。
docker-compose.yml
今回のメインのdocker-compose.ymlの設定です。
docker-compose.ymlversion: '3' services: app: build: . volumes: - .:/rails_test command: bash -c "rm -f tmp/pids/server.pid && rails s -b 0.0.0.0" ports: - 3000:3000 environment: WEBPACKER_DEV_SERVER_HOST: webpacker env_file: - ./environments/db.env depends_on: - db webpacker: build: . environment: NODE_ENV: development RAILS_ENV: development WEBPACKER_DEV_SERVER_HOST: 0.0.0.0 volumes: - .:/rails_test command: ./bin/webpack-dev-server ports: - 3035:3035 db: image: mysql:5.7 volumes: - rails-db:/var/lib/mysql env_file: - ./environments/db.env volumes: rails-db:各コンテナの設定を説明します。
appapp: build: . volumes: - .:/rails_test command: bash -c "rm -f tmp/pids/server.pid && rails s -b 0.0.0.0" ports: - 3000:3000 environment: WEBPACKER_DEV_SERVER_HOST: webpacker env_file: - ./environments/db.env depends_on: - dbRailsの設定です。
- volumes ローカルのディレクトリとマウントします。これによりコンテナ作成時にローカルでの変更点が反映されます。
- command サーバーを立ち上げています。ポイントとしてdocker-compose downでコンテナを削除した際にserver.pidがローカルに残るため再度コンテナを作成した際にサーバーが立ち上げられなくなるため最初にserver.pidを削除します。
- depends_on MySQLのコンテナとの起動順序を定義します。Railsが先に立ち上がるとDBと接続できないと怒られます。
- env_file ここではDBのユーザーネームなど定義します。今回はrootユーザーで行うためルートユーザーのパスワードだけ設定しておきます。
- environment webpackerの設定です。
webpackerwebpacker: build: . environment: NODE_ENV: development RAILS_ENV: development WEBPACKER_DEV_SERVER_HOST: 0.0.0.0 volumes: - .:/rails_test command: ./bin/webpack-dev-server ports: - 3035:3035Webpackerの設定です。ほとんど公式に書いてある通り(Github)にしただけなので、特に言うこともありませんがここでもvolumesをRailsと合わせておかないとwebpack-dev-serverが見つけられなくなります。
dbdb: image: mysql:5.7 volumes: - rails-db:/var/lib/mysql env_file: - ./environments/db.env volumes: rails-db:MySQLの設定です。rails-dbという名前付きボリュームにてDBのデータを永続化しています。また設定は別に用意(db.env)しています。もしrootユーザーでなく新しいユーザーを作りたい場合にはdb.envに書き加えます。
/environments/db.envMYSQL_ROOT_PASSWORD=passwordとりあえずこれだけ書いておきます。
Gemfile
この二行だけ書き加えます。Gemfile.lockは何も書きません。
Gemfilesource 'https://rubygems.org' gem 'rails', '6.0'アプリ作成
まずはrails newをしてアプリを作ります。
$ docker-compose run --rm app rails new . --force -d mysqlディレクトリを新たに作らずにDBはMySQLを使い、Gemfileのオーバーライドをするコマンドを入力しています。ここで自分はyarnを入れていなかったためにwebpackerが途中でインストールが止まりました。
database.ymlの設定
ローカルにできたdatabase.ymlファイルにdocker-compose.ymlで設定したものに書き換えます。
database.ymldefault: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: <%= ENV.fetch('MYSQL_USER') { 'root' } %> password: <%= ENV.fetch('MYSQL_PASSWORD') { 'password' } %> host: db development: <<: *default database: rails_test_development test: <<: *default database: rails_test_test production: <<: *default database: rails_test_production username: rails_test password: <%= ENV['RAILS_TEST_DATABASE_PASSWORD'] %>defaultのところを書き換えています。今回MYSQL_USERとMYSQL_PASSWORDは設定していないのでrootとpasswordになりますが、db.envにて設定できるようにしています。
コンテナ起動
これでRailsがMySQLに接続できるようになりました。コンテナを全て起動しましょう。
$ docker-compose up -dコンテナが3つ(app, db, webpacker)停止していなければ成功かと思われます。
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ab56f7caf110 rails_test_app "bash -c 'rm -f tmp/…" 2 minutes ago Up 2 minutes 0.0.0.0:3000->3000/tcp rails_test_app_1 eb345957801c mysql:5.7 "docker-entrypoint.s…" 3 minutes ago Up 2 minutes 3306/tcp, 33060/tcp rails_test_db_1 a64c29f979c1 rails_test_webpacker "./bin/webpack-dev-s…" 13 minutes ago Up 2 minutes 0.0.0.0:3035->3035/tcp rails_test_webpacker_1DB作成
最後にdb:createして http://localhost:3000 にてデフォルト画面が見れたら成功です。
$ docker-compose exec app rails db:create所感
Qiitaの記事を見ながらコピペでやってできた気になっていましたが、改めて自分で書いてみるとMySQLに接続できなかったりWebpackerが起動できなかったりとトラブル続出でした。
また、node_modulesをコンテナの中に置いておくのってセンスが無い気もしました。何かいいアイデアがありそう。とはいえひと月前までコピペでなんとなくやっていたところも大分自分の言葉で説明できるようになりました。
次は本番環境想定してnginxとの連携とかですかね...。
- 投稿日:2020-12-04T22:53:10+09:00
PDF出力してみたいんじゃ! 【Thinreports + Rails】
どうも。とある六本木一丁目にある会社のエンジニアです。
というわけで SmartHR Advent Calendar 2020 の5日目だぜ。目的
弊社プロダクトのPDF出力に Thinreports を使用しているのでお友達になりたかった。
使うもの
Thinreports + Rails でやっていきます
ゴール
タイトルと本文がある書類をPDFで出力できるようにする!
簡単!やっていこう
とりあえず rails new
rails new thinreports_demo --skip-testgem 追加
gem 'thinreports'bundle installサンプルPDF出力する準備
ThinreportsでのPDF出力には
tlf
というフォーマットのデータが必要なので後々用意する必要があるので先にrails側の準備を整えるコントローラーを用意
rails g controller pdfs index sample_docsample_doc用にroutesを修正
routes.rbRails.application.routes.draw do root 'pdfs#index' resources :pdfs, only: :index do get :sample_doc, on: :collection end endviews を修正
views/pdfs/index.html.erb<h1>PDFにしてくれる君</h1> <p> <%= link_to 'サンプル書類がみたい', sample_doc_pdfs_path %> </p>リンクにアクセスするとPDFを見れるようにしていく
Thinreports Editor で TLFファイル 作る
公式を見て Thinreports Editorのインストール(必要なら Generatorの方も)
Thinreports インストールガイド(公式)何はともあれ
Thinreports Editor
を起動新規作成から新しくA4の書類を作ります
後々のことを考えて
- タイトル
- 本文
- 著者
の3つを
テキストブロックツール
を使って作り出しますapp/pdfs 以下に保存(ファイル名は任意で)
PDFとして出力する設定をしていく
pdfsコントローラーにPDFを出力するための設定をする
詳しくはここら辺のクイックスタート を参考にするpdfs_controller.rbclass PdfsController < ApplicationController : : def sample_doc report = Thinreports::Report.new(layout: "#{Rails.root}/app/pdfs/sample_doc.tlf") report.start_new_page # さっき作った title に value つっこんでる report.page.item(:title).value('PDFやで') # title 以外はあとでやるのでここではスルーします file = report.generate send_data( file, filename: 'filename_sample.pdf', type: 'application/pdf', disposition: 'inline' ) end end先ほどのリンクからアクセスすると PDF出てくる
やったね✌️
書類らしきものを作る
ここら辺は scaffold しましょう
- Doc
- title
- content
- author
rails g scaffold doc title content:text authorrails db:migrate新しく TLFファイル を複製
内容は
sample_doc.tlf
と同じでよいので もう一つ TLF作る(特に意味はないのでsample_doc.tlfを使い回しでいいと思います)書類らしきものごとにPDF出力できるようにする
routes の修正
routes.rbRails.application.routes.draw do : : resources :docs do get :show_pdf end : : endコントローラー
docs_controller.rbclass DocsController < ApplicationController : : def show_pdf @doc = Doc.find(params[:doc_id]) report = Thinreports::Report.new(layout: "#{Rails.root}/app/pdfs/doc.tlf") report.start_new_page # 以下で各カラムごとのデータを入れる report.page.item(:title).value(@doc.title) report.page.item(:content).value(@doc.content) report.page.item(:author).value(@doc.author) file = report.generate send_data( file, filename: "doc_#{@doc.id}.pdf", type: 'application/pdf', disposition: 'inline' ) end : : endリンクも忘れず
views/docs/index.html.erb<p id="notice"><%= notice %></p> <h1>Docs</h1> <table> : <tbody> <% @docs.each do |doc| %> <tr> : <td><%= link_to 'ShowPDF', doc_show_pdf_path(doc) %></td> # ☝️追加 : </tr> <% end %> </tbody> </table> <br> <%= link_to 'New Doc', new_doc_path %>データを入れる
ダミーテキストジェネレータ とかを使ってデータ入れる
http://localhost:3000/docs/new から適当に
http://localhost:3000/docs はこんな感じ
完成!!(見た目は整える時間なかった!)
やったね✌️まとめ
お友達になれました
![]()
- 投稿日:2020-12-04T21:55:42+09:00
[Rails]Stripe Checkoutを利用して定期決済機能を実装する
はじめに
この記事では、Stripeを用いた定期決済機能を実装します。
決済には、Ruby on RailsとCheckout Sessionを利用します。Stripeとは
決済サービスです。
WebサイトやWebアプリケーション、ネイティブアプリに決済システムを導入できます。似たようなサービスには、PayPal, Pay.JP, Omiseなどがあります。
Stripeは機能がとにかく豊富で、多彩な決済システムを構築できるのが特徴です。
用語整理
Checkout
Stripeの決済ページには、Checkout と Elements の2つのタイプが用意されています。Checkout:Stipeが用意した決済専用ページ。
Elements:決済フォームとシステムだけ用意され、決済ページはカスタマイズできる。サクッと決済ページを作成したいときはCheckoutを利用するといいですね。定期決済、都度決済どちらにも対応しています。
Session ID
今回はセッションを用いた決済システムを構築します。こちらの決済システムでは、レスポンスにSession_id
が返ってきます。
このSession_id
にはcusntomer_id(顧客ID)
やsubscription_id(定期決済ID)
など、さまざまな決済情報が入っています。注意点
Stripeドキュメントに書かれているRubyコードの例は、主にSinatraで書かれているのでRailsではありません。
Sinatraとは、Rubyのフレームワークです。RubyのフレームワークはRailsだけではなくて、Sinatraもあります。以前StripeはRailsを使って例を出していましたが、、、現在ではSinatraに鞍替えしています。
RailsにSinatraを導入することはできないので、今回の実装では一部JavaScriptを利用しています。
この記事でできること
- チェックアウトを用いた決済ページの作成
- 決済システムの構築
- session_idを活用
準備
商品を作成するためにStripe Dashboardに入る必要があります。Stripeアカウントを持っていない方は、ここで作成しましょう。
本番環境ではなく、テスト環境で行うだけでしたらアカウントを有効にさせる必要はありません。今回作成する商品や、APIキーはテスト用のものですので、今回の実装も本番環境を使用することはありません。
すでに本番環境を有効にしている方
もしすでに有効にしている場合は、注意点があります。ダッシュボードの左側に View test data もしくは Viewing test data と表示されている切り替えボタンがあると思います。View test dataとなっている時は本番モード、Viewing test dataとなっている時はテストモードです。
テストモードで実装を行いたい場合は、商品の作成からテストモードで行わなければなりません。本番モードの時に作成した商品は本番環境でしか、テストモードの時に作成した商品はテスト環境でしか使用できません。こちらはご注意ください。
商品の作成
商品の作成にはダッシュボード左側のProductsを選択します。
Add productを選択します。
最低限、名前と金額さえ選択できていれば、商品は作成できます。
One timeは一回限りの都度決済、Recurringは定期決済となります。
今回は毎月の定期決済を実装するので、Recurringを選択し、Billing perio(期間)はMonthlyと設定します。
設定が完了したら、右上のSave productをクリックします。
そうすると、PricingのところにAPI IDという蘭があり、そちらには
price_id
が発行されています。こちらが発行されていれば準備は完了です。※補足
2020年8月27日以前のStripe APIのバージョンでは、定期決済の実装には
plan_id
を用いていました。現在のバージョンではprice_id
を用いるので、ご注意ください。実装
Gemをインストール
Gemfilegem 'stripe'APIのシークレットキーを設定
StripeダッシュボードのHomeに行くと、Get your test API keysという欄があります。そちらにあるSecret keyをRailsのディレクトリに記述します。
シークレットキーを記述するのには
credentials.yml
を利用してもいいですが、今回はenv
ファイルを使って行います。.envSTRIPE_TEST_SECRET_KEY = sk_test_xxxxxxまた、
config/initializers
の中にstripr.rb
というファイルを作成し、その中にシークレットキーを設定します。stripe.rbStripe.api_key = ENV['STRIPE_TEST_SECRET_KEY']Paymentsコントローラーを作成する
名前はなんでもいいですが、今回はPaymentsと命名します。
メソッドはnew_subscription
とcreate_subscription
の2つを作成することにします。payments_controller.rbdef new_subscription @session = Stripe::Checkout::Session.create({ payment_method_types: ['card'], line_items: [{ price: 'price_XXXX', quantity: 1, }], mode: 'subscription', success_url: request.base_url + '/payments/create_subscription?session_id={CHECKOUT_SESSION_ID}', cancel_url: request.base_url + '/payments/subscription1', }) end def create_subscription endドキュメントにはsessionに@はついていませんが、viewにsession_idを渡す必要があるので、@を付けます。
priceの欄に、先ほどダッシュボード上で商品を作成したときに発行されたprice_idを入力します。今は
create_subscription
メソッドに何も書かなくていいです。ただ、今の設定ですと決済完了した時にはこのメソッドに遷移するようになっているので、viewファイルにcreate_subscription.html.erb
を作成し、決済完了したことがわかるように何か書くといいかなと思います。create_subscription.html.erb<p>できたよ</p>決済画面を実装
new_subscription.html.erb<script src="https://js.stripe.com/v3"></script> <script> var stripe = Stripe('pk_test_XXXX'); stripe.redirectToCheckout({ sessionId: '<%= @session.id %>' }).then(function (result) { }); </script>
Stripe('pk_test_XXXX')
のカッコ内には、公開可能APIキーを入れてください。シークレットキーとは別のものなので、注意してください。先ほどシークレットキーを取得したStripeダッシュボードからPublishable keyをコピーし、貼り付けましょう。ドキュメントでは決済画面に遷移する前にボタンが実装されていますが、ボタンが不要だと思ったので今回は省いています。ボタンが必要な方はドキュメントを参考にしてください。
試しに決済
これで決済画面ができたので、遷移してみましょう。
こちらの画面に、テストカードを利用して決済してみましょう。
テストカードには、カード番号に4242 4242 4242 4242と入力すれば、後の記入欄はなんでもいいです。決済完了し、この画面になるとおそらく決済できたと思います。Stripeダッシュボードでも確認できます。
create.subscription.html.erbの画面↓
session_id
レスポンスにはsession_idが返ってきています。これは、決済ごとに発行されるIDです。
こちらのsession_idを取り出すと、色々な情報が出てきます。取り出し方は以下です。
session_idを取り出すStripe::Checkout::Session.retrieve( 'cs_test_XXXX', )レスポンス{ "id": "cs_test_XXXX", "object": "checkout.session", "allow_promotion_codes": null, "amount_subtotal": null, "amount_total": null, "billing_address_collection": null, "cancel_url": "https://example.com/cancel", "client_reference_id": null, "currency": null, "customer": null, "customer_email": null, "livemode": false, "locale": null, "metadata": {}, "mode": "payment", "payment_intent": "pi_XXXX", "payment_method_types": [ "card" ], "payment_status": "unpaid", "setup_intent": null, "shipping": null, "shipping_address_collection": null, "submit_type": null, "subscription": null, "success_url": "https://example.com/success", "total_details": null }この情報のどれかをDBに保存したい場合は、こちらから保存するといいかなと思います。例えば、customer_idを保存したい場合は以下のようになると思います。
payments_controller.rbdef create_subscription session = Stripe::Checkout::Session.retrieve(params[:session_id]) Payment.create(customer_id: session.customer) endまとめ
StripeのAPIバージョンは2020年8月末で更新されています(おそらく)。このバージョンから仕組みが大きく変わっているところがあり、この日以前のQiitaの記事があまり参考にならない場合があります。例えば、price_idの登場だとか、ですね。
このバージョンの日本語記事が少なく、Stripeのドキュメントも英語で、かつドキュメントにあるRubyコードのサンプルはSinatra用となっているので、、Railsで実装をしようとすると難しい部分がありますね。
これからStripe系の記事もいくつか投稿していこうと思います。
参考資料
Stripeドキュメント: (Dynamic subscriptions の After 部分)
Stripe APIリファレンス: セッションを取得する
Qiita: Rails5.2でStripeを使う(v3)
- 投稿日:2020-12-04T20:41:06+09:00
もしも・・・な Rails Girls があったら
はじめに
この記事は Rails Girls Japan Advent Calendar 2020 の4日目の記事です。
企画・運営に携わられている皆様お疲れ様です。
こういう企画があったら楽しいのではということで投稿いたしました。もしも・・・SPA な Rails Girls があったら
元ネタは、去年のAdvent Calendarの Rails Girlsのカリキュラムをこうしていきたいみたいな提案 - Qiita です。
CRUD
が基礎となるのはそうですし、日本的な基礎から入って応用へ行くメリットも分かりますが、最先端も面白い。
まあ、SPA
って認識があれば初心者ではないと思いますが。もしも・・・COBOL な Rails Girls があったら
女性IT技術者の大先輩と言えば、グレース・ホッパー - Wikipedia さん。
グレースさんが開発したCOBOL
はいまだ現役で稼働しています。
勿論Rails
も凄いのですが、COBOLも凄いので、イタコさんに憑依していただきグレースさんの開発苦労話など伺いたいものです。
イタコさんだったら日本語で語ってくれますよね?もしも・・・パパ活 な Rails Girls があったら
Rubyのパパ
と言えば、まつもとゆきひろ - Wikipedia さん。
そのパパからプログラミングについて、直々カツ!をいただける、大変ありがたい話ではないですか。
勿論、いいプログラミングについては、ヨシ!を頂けます。まとめ
いずれも、
個人的に喜び勇んで参加したくなる企画ですね。
- 投稿日:2020-12-04T20:38:36+09:00
RailsアプリのCI/CDを"GitHub Actions"で作ってみた
記事のきっかけ
初学者ながら自身のRailsアプリにCI/CDを入れるぞ❗️と考えました。
最近話題になっているGitHub Actionsってなんだろう❓
GitHubでCI/CDも管理できるらしい。凄い❗️
その気持ちから、他のCI/CDサービスと自分なりに比較選択をし、
GitHub Actionsを選択。実装に突き進みました。
本記事はその時のコード記録と実施内容の忘備録を兼ねて記事にしました。アプリ開発環境
- Rails v6.0.3.4
- Ruby v2.6.6
- PostgreSQL v12.4
- Docker-compose v1.26.2
- Docker v19.03.12
GitHub Actionsとは
言わずも知れた開発管理必須ツールであるGitHub。
そのGitHubが提供するCI/CDサービス。
GitHubと高度に連携されており、設定によりGitHub上のコードを
自動でビルド・テスト・デプロイが可能。なぜGitHub Actionsなのか?
私なりにCIを選ぶにあたって他のサービスと検討しました。
他のサービスもありますが、私が比較したのは下記の2つのサービスです。
Circle CI
SaaS型のCI/CDサービス。FacebookやCyberAgentでの活用事例がありCI/CDサービス大手
QiitaでもCI/CDの記事が豊富にあり、アプリ作成事例での活用も多いイメージがあります。Code Pipeline
AWSのCI/CDサービス。各種AWSとの連携ができるのが強み私が選択した理由
①:GitHub Actionsの圧倒的なメリット点
GitHubのリポジトリの場所でCI/CDが実行・確認できる事です。
アプリ開発はGitHubは開発では必然的に使うのでこれは非常に便利です。
またAWSのリソースを触る場合においてアクセスキーを登録する必要がありますが
リポジトリ管理環境とAWSのアクセスキーを一度に管理できるのは、
AWSを使用したデプロイを想定している中で
リポジトリと外部SaaSでCI/CDの管理を分けるより、
進捗及び危機管理しやすい点があると考えました。②:他のCI/CDとのサービス比較
上記でも触れましたが、Circle CIは外部SaaSへの権限移譲の問題などの下記資料も検証し
*GitHub Actions の self-hosted runner と Amazon EKS を使った Docker の Build Pipeline
(freee株式会社)
今後の実務の中でもこの事は問題になる可能性があるなら、CI/CDとして最初に使うのは
GitHub Actionsでもいいのではないか?と考えました。Codepipelineとの比較では今回はAPI側の変更作業が多くなる事が予想されたので
GitHub上でCI/CDを随時実行し確認できた方が作業効率がいいと思われました。以上を総合判断し、最終的にGitHub Actionsを使用する事としました。
ただこれは初心者が考えた事ですので、間違いやそれぞれのCI/CD側の言い分があるかもしれません。
Circle CIはGithub Actionsより優れていると宣伝してますし(そんな堂々と・・。)
CodepipelineもAWSとの連携をする中では大きなメリットを享受出来る事と思います。GitHub Actions 作成手順・コード
前置きが長くなりました。早速GitHubActionsでのCI/CDを作成して行きます。
公式サイトは下記のリンク先となります。
GitHub Actionsのドキュメント1. 作成リスト
今回RailsでのアプリでCI/CDを組み立てます。
- .github/workflows ディレクトリ配下(新規作成)
- linter.yml
- rspec_security.yml
- main.yml
- database.yml (修正)
2.CIセッティング
今回のCIは2種類でセッティングしました。
1. Linter (トリガー:GitHubにpush
した時)
2. RSpec, security (トリガー:GitHubにpush
した時 及びpull_request
時)linter.ymlname: Linter on: [push] jobs: linters: name: Linters runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 - name: Ruby Bundler uses: actions/cache@v2 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | ${{ runner.os }}-gems- - name: Bundle install run: | bundle config path vendor/bundle bundle install --jobs 4 --retry 3 - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - name: yarn install run: yarn install - uses: actions/cache@v2 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: Run linters run: | bundle exec rubocop --parallelrspec_security.ymlname: Rails RSpec and security on: [push, pull_request] env: RAILS_ENV: test CI_HOST: localhost jobs: build: runs-on: ubuntu-latest services: postgres: image: postgres:12 ports: - 5432:5432 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 chrome: image: selenium/standalone-chrome ports: - "4444:4444" volumes: - /dev/shm:/dev/shm steps: - name: Checkout code uses: actions/checkout@v2 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 - name: Ruby Bundler uses: actions/cache@v2 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | ${{ runner.os }}-gems- - name: Bundle install run: | bundle config path vendor/bundle bundle install --jobs 4 --retry 3 - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - name: yarn install run: yarn install - uses: actions/cache@v2 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: Setup test database env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres run: | bundle exec rake db:create db:schema:load --trace - name: run rspec run: bundle exec rspec env: SELENIUM_REMOTE_URL: http://localhost:4444/wd/hub - name: Archive rspec result screenshots if: failure() uses: actions/upload-artifact@v2 with: name: rspec result screenshots path: tmp/screenshots/ - name: security check run: | bundle exec bundle-audit check --update bundle exec brakeman -q -w2CD (ECR自動デプロイ)
CDは
ECR
へのコンテナbuild&push
を行うセッティングです。
今回の設定でのトリガーはtag push
のversion(v)
で実行しております。main.ymlname: Build and Push on: push: tags: - v* jobs: build-and-push: runs-on: ubuntu-18.04 timeout-minutes: 300 steps: - uses: actions/checkout@v1 - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-1 - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 - name: Build, tag, and push image to Amazon ECR env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPO_NAME }} run: | IMAGE_TAG=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g") docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAGdatabase.yml
環境変数のセッティングが必要となります。
テスト部にENV.fetch
で第一引数で指定した環境変数を確認し、
なければブロックで定義したデフォルト値が帰るようにしています。database.ymldefault: &default adapter: postgresql encoding: utf8 min_messages: WARNING host: db port: 5432 username: postgres password: postgres pool: 5 timeout: 5000 stats_execution_limit: 10 development: <<: *default database: development test: <<: *default database: test host: <%= ENV.fetch('CI_HOST') { 'db' } %> production: <<: *default database: myworkdb host: <%= ENV['DB_URL'] %> username: <%= ENV['DB_USERNAME'] %> password: <%= ENV['DB_PASSWORD'] %>CI/CD内容
CI
Linter.yml
コード内容としてはrubocop
でのLinterチェックとしました。
ローカル開発環境ではrubocop
prettier
beautify
の3種を併用しておりますが、
push
前のLinter整形忘れチェックとしてpush
時にチェックする対応で対応しました。
*ローカル環境下でのLinterセッティングについては下記記事にまとめております。
よろしければご参照下さい。
"Rails"でのフォーマット環境を整える(VScode)
RSpec, security
- テストはRailsの定番RSpecを走らせました。
- セキュリティは Gemの脆弱性診断
bundle-audit
と総合セキュリティbrakeman
を活用しました。
*Railsのセキュリティに関しては別記事でまとめております。
よろしければご参照下さい。
Rails6のセキュリティチェック環境を整えるCD
セッティングコードはClassmethodさんの下記の記事を参考にしました。
この記事のおかげで以後の開発が凄く捗りました。ありがとうございます。
- 参考元リンク:GitHub ActionでDockerコンテナをビルドしてAmazon ECRに保存する
(Developers.io Classmethod株式会社)最後に
最後まで読了頂きありがとうございます。
少しでも資料がどなたかの役に立てれば幸いです。
また今回GitHub Actionsの詳細な設定内容(コード)については触れておりませんので
別記事でGitHub Actionsの設定について個人的にまとめたいと思っております。
- 投稿日:2020-12-04T20:15:22+09:00
Mysql2::Error: Field '****' doesn't have a default valueに二回もハマった話
Mysql2::Error: Field 'tag_name' doesn't have a default value
とは、NOT NULL制約をかけているにも関わらず値が入っていないよーといった感じのエラーですclass CreateTags < ActiveRecord::Migration[6.0] def change create_table :tags do |t| t.string :name,null:false t.timestamps end end endとかでカラムに,null: falseで必ず値が入るように設定していたと思います
なので、rails側でも
validates :name,presence: trueと、バリデーションをかけていたと思います。
というわけで、まず考えられる原因が
マイグレーションファイルでnull: falseとNOT NULL制約を設けていたにも関わらず、Rails側でバリデーション をかけていなくて、form_withとか何とかで、値を保存しようとした!!
といったことが考えられます。
しかし自分は違いました、バリデーション もかけている
[しくじった原因1: ストロングパラメーターの記述が間違ってる]
{"authenticity_token"=>"T8/aOv7fmGDk6UO/GHiYkODYvpMTH/3tFP6sCW0QPVVaAVh6ZHcAd2xaTQzcuioVXdOOWYAFNHO8u3S0OmnG6Q==", "drink_tag"=> {"tag_name"=>"酸味", "image"=> #<ActionDispatch::Http::UploadedFile:0x00007f8a24b3d318 @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"drink_tag[image]\"; filename=\"ethiopia.jpg\"\r\n" + "Content-Type: image/jpeg\r\n", @original_filename="ethiopia.jpg", @tempfile=#<File:/var/folders/34/pbcy_n7j1q79hnpm7sbcgl0m0000gn/T/RackMultipart20201204-1452-13ocgpk.jpg>>, "name"=>"エチオピア", "explain"=>"酸味があってアイスもおすすめ!!", "price"=>"1000"}, "commit"=>"投稿する"}と、パラメーターがとんでいて
private def drink_params params.require(:drink_tag).permit(:name,:price,:explain,:image,:tag_name).merge(user_id: current_user.id) endとストロングパラメーターを書くのが正しいですが
自分の場合、ストロングパラメータくらい分かったつもりになって教材をコピペしたせいで,
private def drink_params params.require(:tweet_tag).permit(:name,:price,:explain,:image,:tag_name).merge(user_id: current_user.id) endと、
params.require(:drink_tag)を
params.require(:tweet_tag)にしていました、、、。
ストロングパラメーターが間違っていたら
正しい値を許容できていないのでdef create @drink = DrinkTag.new(drink_params) if @drink.save redirect_to drinks_path else render 'new' end endの@drinkは空,nilなので、
nilな物を保存すんな!って怒られてしまいます、、、。
form_withの使い方はあってたので、しっかりとしたパラメーターが作られて
railsのバリデーションの網はかいくぐったようですが、MySQLちゃんがしっかりと値の保存を防いでくれました、、、。[しくじった原因2: Formオブジェクトを用いた値の保存で、一度に保存するためのmodelの記述が間違ってる!]
Formオブジェクトとは、一つのビューで複数の値を保存したいときに用いられるテクニック?的なものです。
( 詳しくは検索、検索♪)class UserDonation include ActiveModel::Model attr_accessor :name, :name_reading, :nickname, :postal_code, :prefecture, :city, :house_number, :building_name, :price with_options presence: true do validates :name, format: { with: /\A[ぁ-んァ-ン一-龥]/, message: "は全角で入力してください。"} validates :name_reading, format: { with: /\A[ァ-ヶー-]+\z/, message: "は全角カタカナで入力して下さい。"} validates :nickname, format: { with: /\A[a-z0-9]+\z/i, message: "は半角英数で入力してください。"} end def save user = User.create(name: name, name_reading: name_reading, nickname: nickname) Address.create(postal_code: postal_code, prefecture: prefecture, city: city, house_number: house_number, building_name: building_name, user_id: user.id) Donation.create(price: price, user_id: user.id) end endと、一つのビューで複数のテーブルに値を保存したい時に、こういったモデルをよく作りますが
saveメソッドでのキー、バリューの書き忘れ!!!
で自分はつまずきました、、、。
この定義したsaveメソッドはあとでコントローラーで
def create @donation = UserDonation.new(donation_params) if @donation.valid? @donation.save # バリデーションをクリアした時 return redirect_to root_path else render "new" # バリデーションに弾かれ時 end endとsaveメソッドを用いますが、
例えば、
def save user = User.create(name: name, name_reading: name_reading, nickname: nickname) Address.create(postal_code: postal_code, prefecture: prefecture, city: city, house_number: house_number, building_name: building_name, user_id: user.id) Donation.create(price: price, user_id: user.id) endで、
Address.create(postal_code: postal_code, prefecture: prefecture, city: city, house_number: house_number,
building_name: building_name, user_id: user.id)
の
postal_code: postal_code
が抜け落ちてたら、このエラーが起きます、、、。
なので、しっかりsaveメソッドが定義できてるかみましょう!!これもフォームで入力して、しっかりパラメーターが作られるので、railsのバリデーション の
網を抜けたのでしょうが、データベース側でも「nilはだめ!!」としていたので、助かりました、。まとめ
バリデーション をかけているにもかかわらず、このエラーが起きるということは、モデルかコントローラーに
何かしらの問題がある。もっというと、コントローラーは、createで値を保存するので、
ストロングパラメーターに問題があったり、formオブジェクトはmodelにsaveメソッドを定義してるので、saveメソッドに問題が
ないか確かめましょう、、、!!!他にもこのエラーに対する考えられる原因と、それの対処法があればコメントお願いします!!
- 投稿日:2020-12-04T18:56:40+09:00
【Rails】ActionMailer のテストで Mail::Matchers を使う
はじめに
CBcloud Advent Calendar 2020 の2日目の記事です。
本記事では、メール送信の単体テストの際、ActionMailer が依存している Mail に含まれている
Mail::Matchers
を RSpec のマッチャとして利用する方法を紹介します。また、比較対象として、以下の2つも同時に記載します。
- Rails Guides に記載されているテスト方法
- RSpec のドキュメントに記載されているテスト方法
最後に、
Mail::Matchers
を使った例の紹介と、実際に動くサンプルコードを添付します。動作確認環境
- ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-darwin19]
- actionmailer (6.0.3.4)
- rspec (3.10.0)
テストコード
Rails Guides に記載されているテスト方法
https://guides.rubyonrails.org/testing.html#testing-your-mailers
require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "invite" do # Create the email and store it for further assertions email = UserMailer.create_invite('me@example.com', 'friend@example.com', Time.now) # Send the email, then test that it got queued assert_emails 1 do email.deliver_now end # Test the body of the sent email contains what we expect it to assert_equal ['me@example.com'], email.from assert_equal ['friend@example.com'], email.to assert_equal 'You have been invited by me@example.com', email.subject assert_equal read_fixture('invite').join, email.body.to_s end endRSpec のドキュメントに記載されているテスト方法
https://relishapp.com/rspec/rspec-rails/v/3-9/docs/mailer-specs/mailer-spec
require "rails_helper" RSpec.describe NotificationsMailer, :type => :mailer do describe "notify" do let(:mail) { NotificationsMailer.signup } it "renders the headers" do expect(mail.subject).to eq("Signup") expect(mail.to).to eq(["to@example.org"]) expect(mail.from).to eq(["from@example.com"]) end it "renders the body" do expect(mail.body.encoded).to match("Hi") end end endMail::Matchers を使ったテスト方法
ActionMailer の依存で Mail gem は既にインストール済みなので、導入手順は簡単です。
- RSpec の設定で、
Mail::Matchers
を include する- ActionMailer のテストで
Mail::Matchers
が提供するマッチャを使う以下が実際に
Mail::Matchers
を使って書いたコードです。
比較のため、最初のコンテキストでは先述した RSpec のドキュメントに記載されたサンプルを踏襲したコードを実装しています。RSpec.configure do |config| config.include Mail::Matchers, type: :mailer end RSpec.describe NotificationsMailer, type: :mailer do before do ActionMailer::Base.deliveries.clear end describe '#signup' do # @see: https://relishapp.com/rspec/rspec-rails/v/3-9/docs/mailer-specs/mailer-spec context 'when using RSpec mailer examples' do subject(:mail) { described_class.signup } it 'renders the headers' do expect(mail.subject).to eq('Signup') expect(mail.to).to eq(['to@example.org']) expect(mail.from).to eq(['from@example.com']) end it 'renders the body' do expect(mail.body.encoded).to match('Hi') end it 'sends the mail' do expect { mail.deliver_now }.to change { ActionMailer::Base.deliveries.count } end end # @see: https://github.com/mikel/mail#using-mail-with-testing-or-specing-libraries context 'when using Mail::Matchers' do subject(:mail) { described_class.signup.deliver_now } it { is_expected.to have_sent_email } it { is_expected.to have_sent_email.from('from@example.com') } it { is_expected.to have_sent_email.to('to@example.org') } it { is_expected.to have_sent_email.with_subject('Signup') } it { is_expected.to have_sent_email.with_body('Hi') } end end endメール送信後のマッチャが全て
have_sent_email
に集約されているのがわかりますね。
ここで紹介したもの以外にも、添付ファイルに対するマッチャなど、メールのテストに必要なものが一通り揃っているので、ぜひドキュメントを確認して使ってみてください。
https://github.com/mikel/mail#using-mail-with-testing-or-specing-libraries最後に
ここまで読んで、実際に自分で試してみたいと思われた方のために、動くサンプルコードを Gist で公開しています。
https://gist.github.com/tomohiro/dfa7362cd066dd87f25f40bdd6856513余談
個人的には
--format documentation
で出力したときの表現が冗長に感じています。参考資料
- 投稿日:2020-12-04T17:29:08+09:00
Sprockets::DoubleLinkError
初心者です。備忘録のため。残します。
超基本的な内容です。エラー内容
Sprockets::DoubleLinkErrorRails にて erb ファイルを作成して、表示しようとしたら上記発生。ターミナル確認すると以下記述あり。
ActionView::Template::Error (Multiple files with the same output path cannot be linked ("style.css") In "/Users/○○/git/photo_submission/app/assets/config/manifest.js" these files were linked: - /Users/○○/git/photo_submission/app/assets/stylesheets/style.css - /Users/○○/git/photo_submission/app/assets/stylesheets/style.scss ):原因
調べてみたら、Rails は rails new したときに自動で作成される views/layauts/application.html.erb を参照して HTML ファイルを作るようです。
assets ファイル以下のデータ群を一つに圧縮する(アセットパイプライン)。
views/layauts/application.html.erb
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>あとで erb ファイルに変更しようと思って、先に普通の HTML ファイルで作成しており、深く考えずに style.scss を作成していた。VSCode の拡張機能の Live Sass Compiler でstyle.css も自動で作成されており、これがエラーの原因。
assets/stylesheets/application.css に style.scss の記述を写し、stylesheets 直下には application.css だけ残すようにファイルの構造を調整して、再度出力すると問題なく表示できた。
- 投稿日:2020-12-04T16:51:45+09:00
第7回
やったこと
- bundler
- if, case, Array.each
if, case, Array.each
条件分岐を取り扱おうということだ
お題
入力された年数がleap year(うるう年)かどうかを判定する
プログラミングの実習でよくある課題だが、条件分岐が複雑まず4の倍数かどうかを判定する
入力部は
p year = ARGV[0].to_ito_iとはIntegerであるということを示すものらしい
if year%4==0 then p true else p false end最初の条件分岐文がこちら。yearを入力として、それを4で割った余りが0ならtrue、そうじゃなければfalseだぞ、と出力するこれを実行すると
> ruby check_leap.rb 2004 2004 true1997の場合は
> ruby check_leap.rb 1997 1997 falseとなる
特殊な年数の場合をはじいていく
100で割り切れる年はうるう年でなく、400で割り切れる年はうるう年であるという二点を判定できるようにする
こういう例外処理は先に済ませておこうと思う。また、長くなってきたのでmethodにしておくp year = ARGV[0].to_i def leap?(year) if year % 400 ==0 p true elsif year % 100 ==0 p false elsif year % 4 == 0 p true else p false end end leap?(year)これを実行すると
> ruby check_leap.rb 1900 1900 false2000の場合は
> ruby check_leap.rb 2000 2000 trueとなった。100の倍数だが400の倍数でない1900と、100の倍数かつ400の倍数である2000を区別できている
array, eachを使う
大量の値を一気にテストしたいときにはarray(配列)が便利。
def leap?(year) if year % 400 ==0 p true elsif year % 100 ==0 p false elsif year % 4 == 0 p true else p false end end [1900,1997,2000,2004].each do |year| p year leap?(year) end実行すると
> ruby check_leap.rb 1900 false 1997 false 2000 true 2004 true正しく判定されてますね
case
授業中では扱ってなかった気がするけどcaseを使うとさらにコードのrefactoringが可能
caseを使って先ほどのコードを書きなおしてdef leap?(year) case when year % 400 ==0 ; return true when year % 100 ==0 ; return false when year % 4 ==0 ; return true else return false end end [1900, 1997, 2000, 2004].each do |year| p year leap?(year) endで動くかと思ったら
> 1900 > 1997 > 2000 > 2004となり、判定が行われていない???(red)
と思ったけどleap?(year)の結果が出力がされてないじゃん、ということでp leap?(year)として動かすと
> 1900 > false > 1997 > false > 2000 > true > 2004 > trueよかった、プログラムは短くなったしgreen状態にできた
参考記事
- source ~/MasahiroOba/grad_members_20f/members/MasahiroOba/chapter7.org
- 投稿日:2020-12-04T16:48:06+09:00
第7回
やったこと
- bundler
- if, case, Array.each
if, case, Array.each
条件分岐を取り扱おうということだ
お題
入力された年数がleap year(うるう年)かどうかを判定する
プログラミングの実習でよくある課題だが、条件分岐が複雑まず4の倍数かどうかを判定する
入力部は
p year = ARGV[0].to_ito_iとはIntegerであるということを示すものらしい
if year%4==0 then p true else p false end最初の条件分岐文がこちら。yearを入力として、それを4で割った余りが0ならtrue、そうじゃなければfalseだぞ、と出力するこれを実行すると
> ruby check_leap.rb 2004 2004 true1997の場合は
> ruby check_leap.rb 1997 1997 falseとなる
特殊な年数の場合をはじいていく
100で割り切れる年はうるう年でなく、400で割り切れる年はうるう年であるという二点を判定できるようにする
こういう例外処理は先に済ませておこうと思う。また、長くなってきたのでmethodにしておくp year = ARGV[0].to_i def leap?(year) if year % 400 ==0 p true elsif year % 100 ==0 p false elsif year % 4 == 0 p true else p false end end leap?(year)これを実行すると
> ruby check_leap.rb 1900 1900 false2000の場合は
> ruby check_leap.rb 2000 2000 trueとなった。100の倍数だが400の倍数でない1900と、100の倍数かつ400の倍数である2000を区別できている
array, eachを使う
大量の値を一気にテストしたいときにはarray(配列)が便利。
def leap?(year) if year % 400 ==0 p true elsif year % 100 ==0 p false elsif year % 4 == 0 p true else p false end end [1900,1997,2000,2004].each do |year| p year leap?(year) end実行すると
> ruby check_leap.rb 1900 false 1997 false 2000 true 2004 true正しく判定されてますね
case
授業中では扱ってなかった気がするけどcaseを使うとさらにコードのrefactoringが可能
caseを使って先ほどのコードを書きなおしてdef leap?(year) case when year % 400 ==0 ; return true when year % 100 ==0 ; return false when year % 4 ==0 ; return true else return false end end [1900, 1997, 2000, 2004].each do |year| p year leap?(year) endで動くかと思ったら
> 1900 > 1997 > 2000 > 2004となり、判定が行われていない???(red)
と思ったけどleap?(year)の結果が出力がされてないじゃん、ということでp leap?(year)として動かすと
> 1900 > false > 1997 > false > 2000 > true > 2004 > trueよかった、プログラムは短くなったしgreen状態にできた
参考文献
- source ~/MasahiroOba/grad_members_20f/members/MasahiroOba/chapter7.org
- 投稿日:2020-12-04T16:18:11+09:00
rbenvのインストールについて(Checking for rbenv shims in PATH: not found)
結論
シェルの確認
↓
.zshrcファイルの作成
↓
eval "$(rbenv init -)"
export PATH="~/.rbenv/bin:$PATH"
export PATH="~/.rbenv/bin:~/.rbenv/shims:$PATH"
の三行を追加
↓
source ~/.zshrc環境
- Mac
問題点
$ curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-doctor | bashを実行すると、
Checking for `rbenv' in PATH: /usr/local/bin/rbenv Checking for rbenv shims in PATH: not found The directory `/Users/ユーザー名/.rbenv/shims' must be present in PATH for rbenv to work. Please run `rbenv init' and follow the instructions. Checking `rbenv install' support: /usr/local/bin/rbenv-install (ruby-build 20201118) Counting installed Ruby versions: none There aren't any Ruby versions installed under `/Users/ユーザー名/.rbenv/versions'. You can install Ruby versions like so: rbenv install 2.2.4 Checking RubyGems settings: OK Auditing installed plugins: OKとなり、
Checking for rbenv shims in PATH: not foundこの部分のパスが通っていないこと。
解決法
rbenv shimsの環境パスの設定が正しくできていない、とのことなので
.bash_profile内で、export RBENV_ROOT="$HOME/.rbenv" if [ -d "${RBENV_ROOT}" ]; then export PATH="$HOME/.rbenv/bin:$PATH" eval "$(rbenv init -)" fiを記述する。これで再度確認するとOK!になるのだが、ターミナルを再起動するとまた
Checking for rbenv shims in PATH: not foundのエラーになる。
ここからが具体的な解決法。
今現在シェルがzshになっている。その上で、bash_profileファイル内に記述したのだがそれではターミナルを起動するごとにsource ~/.bash_profileをする必要がある。
結論として新たにzshrcファイル(zsh_profileファイルでもいい)を作成し、同じような記述をするとターミナルを再起動してもパスが通っている状態になっている。
- 投稿日:2020-12-04T16:05:30+09:00
RailsでWeb APIを実装しよう!
はじめに
PORT Advent Calendar 2020 の4日目の記事です!
はじめまして! PORT新卒エンジニアのしゅんいち(shxun6934) です!
業務でのとあるタスクにて、Railsを使用してWebAPIを実装することになりました。
今までは、WebAPI(QiitaとかTwitterとかお世話になりました。)を使用する側でしたが、WebAPIを作成する側になったので、いろいろわからないところが多かったです。簡単にですが、そのタスクに臨むにあたって学んだこと、意識していたことを皆さんに共有しておきたいと思います。
そもそも、APIって何?
API(Application Programming Interface) は、アプリケーションとアプリケーション、人、組織をつなげてくれるものです。
APIと言っても種類はいくつかあります。
(例:Twitterから「いいね」の情報を取得するためのもの、iOSアプリでカメラの機能を使用するためのもの、画像の色を反転させるためのもの、などなど。。。)一般的に言われている「API」は、WebAPIを指すことが多いです。
WebAPIは、ネットワークを使用して他のアプリケーションと通信する APIのことです。
RESTful API
RESTの原則に従ったAPI。
ここで思うことは、「RESTって何???」
簡単に言うと、RESTは複数のソフトウェアを連携させるための設計原則の考え方のことです。
RESTの原則は以下の4つ。URIを通して提供できる情報が表現できていること = アドレス可能性
インターフェースが統一されていること = 統一インターフェース
やり取りする情報で全てを完結させること = ステートレス性
情報の内部に別の情報や状態へのリンクを含めることができること = 接続性これらに従って設計すれば、使いやすくAPIになります。
では、実際にWebAPIで意識することを見ていきます。
URI
WebAPIなので、URIは必須になります。
ここでは、REST原則のアドレス可能性が鍵になります。
URIはユーザーが実際に入力するものなので、わかりやすく、どんな機能を持つURIがひと目でわかるようにしたほうがベストです。例として、犬の種別の一覧を取得するAPIを設計します。
GET https://example.com/api/get-dogs-type-all-listこれでは、読みにくく、直感的ではありません。(何の機能なのかはなんとなくわかる気がする。。。)
dogs
とtype
が同列に並んでいるので、階層的にわかりづらいですし、list
とall
では意味がほぼ同じなので、重複してます。
また、WebAPIを使用するユーザーが想定なので、URIにわざわざget
という単語を入れなくてもいいです。(後述のHTTPメソッドより)なので、階層的にわかりやすく、シンプルにしたほうが使いやすそうですね。
以下のように修正します。GET https://example.com/api/dogs/type/list
dogs
のtype
のlist
を取得できると直感的にわかるようになったと思います。(多分)HTTPメソッド
REST原則の統一インターフェースに一番密な要素です。
標準化されたHTTPメソッドの個々の役割を理解して、それらをどのAPIでも適用するものです。基本的なHTTPメソッドは、以下。
HTTPメソッド 役割 GET リソースを取得する、検索する POST リソースを作成する、追加する PUT リソースのデータを更新する DELET リソースを削除する もし、POSTで「リソースのデータを更新する」役割をも持たせたとして、
記事を投稿するAPI
と記事の内容を更新するAPI
があったとします。2つのAPIを以下のように設計します。
POST /api/articles POST /api/articles/:article_id上記の設計だと、同じアプリケーションに「リソースを作成する」
POST
と「リソースのデータを更新する」POST
が存在してしまいます。これでは、
POST
に関して同じ意味をなさなくなるので、インターフェースが統一されていないことになり、使いづらいAPIになってしまいます。この例では、HTTPメソッドの役割に沿って、
記事の内容を更新するAPI
はPUTで行うようにしたほうがいいと思います。HTTPレスポンス
APIを使用して返ってくるものにもしっかりと制約をつけ、インターフェースを統一しましょう。
(HEADは割愛させていただきます。すいません。)ステータスコード
200
とか、404
とか、500
とかのあれです。
ステータスコード単体でも意味を成しているので、HTTPメソッド同様、理解する必要があります。よく目にするステータスコードは以下。
ステータスコード 意味 200 リクエストが成功した 201 リクエストが成功し、リソースが新規作成された 400 リクエストパラメータに不足・不備があった 401 アクセストークンが無効、認証されていない 404 リクエストされたリソースが存在しなかった 500 何らかのエラーがサーバー側で発生している WebAPIの設計において、基本的には想像しうる状態を全て網羅しなくてはいけません。
網羅した上で、必要なステータスコードはなんなのかを考えていきましょう。ボディ
ユーザーが実際に扱うデータのフォーマットや構造を考えて置く必要があります。
最近のWebAPIは、JSONで返すことが多いです。
JSON
のフォーマットにしたがっていけば問題なさそうですね。レスポンスのボディは2種類あると思います。
リクエストが成功した場合
とリクエストが失敗した場合
です。リクエストが成功した場合
ユーザーが欲しい情報を
JSON
のフォーマットに沿って構造化していきます。今回、個人的に意識したことは、階層構造です。
例として、ユーザー一覧を取得するAPIの
JSON
を考えます。
ユーザーを取得する際に、住所の情報も欲しいとします。もし、住所の情報に、
prefecture
とcity
が欲しいとき、
以下のように階層ごとにグループ化することで、わかりやすく、データもまとまります。users.json{ { "id": 1 "first_name": "AAAA", "middle_name": null, "last_name": "BBBB", "sex": "man", // 住所として一つのグループにする。 "address": { "prefecture": "東京都", "city": "新宿区" }, "favorite_tags": ["Ruby", "Rails", "WebAPI"] "url": "https://example.com/users/1" }, { "id": 2 "first_name": "CCCC", "middle_name": "1111", "last_name": "DDDD", "sex": "woman", // 同列だとわかりづらい "prefecture": "東京都" "city": "渋谷区" "favorite_tags": [] "url": "https://example.com/users/2" }, }リクエストが失敗した場合
ユーザーに何のエラーなのか、どうして起きたのかを正しく提供しなくてはいけません。
ユーザー一覧を取得するAPIのパラメーターに、ソートをかけるとします。(例:favorites_tagsで"Ruby"を持っているユーザーの一覧)
ここで間違えて、"Ruby"ではなく、数字を入れてしまったときのレスポンスは、
以下のようにする必要があると思います。https://example.com/api/users?favorites_tags=65847{ "message": "Invalid Request", "errors": [ { "type": "bad_type", "message": "favorites_tags is String, is not Integer" } ] }エラーが複数存在する場合は、グループ化して、わかりやすくするのも一つの手ですね。
Railsでは、、、
Webアプリを作成するために最適な
Rails
でも当然、WebAPIの実装はできます。RailsでのAPI実装
Railsには、APIモードなるものが存在していて、プロジェクトを新規作成する場合は、
$ rails new my_api --apiでAPIモードになります。
また、既存のアプリをAPI専用にしたい場合は、
config/application.rb
にてconfig/application.rbconfig.api_only = trueを設定するとできるみたいです。(超便利!!)
ただ、今回は既存のプロジェクトの一部でAPIが実装されているので、上二つのやり方はできませんでした。
RailsのAPI実装の特徴は、APIモードではない、いつも通りのRailsとあまり変わらずに実装することができることです。
MVC
のView層が使用されないくらいで、あとは同じ感じでコードを書けるので、楽にかけます。
ただ、APIではいつもよりも制限がかかっているので、仕様については理解しないといけないところがあります。今回の実装で詰まった、
ActionController::API
を簡単に説明します。ActionController::API
APIモードにする際に、
app/application.rb
を以下のように変更する必要があります。app/application.rb# ActionController::Base → ActionController::APIに変更する。 class ApplicationController < ActionController::API endこれでAPI専用になるのですが、、、
このActionController::API
は、ActionController::Base
をAPI専用にしたものであることを認識しないといけませんでした。上記でいつも通りに開発できると言いましたが、厳密には少し違います。
このActionController::APIのリファレンスのこの部分。
Keep in mind that templates are not going to be rendered
APIモードでも、
render
メソッドでhtmlやxmlのフォーマットを指定すれば、読み出せます。しかし、template
の読み出しはできないため、jsonをviewとして扱うことが結構難しくなっています。まとめ
APIを実装する上では、ユーザー目線に立つことが何よりも大事だと思いました。
実際に使う人の目線に立って、使いづらくないか、わかりやす構造になっているかを気にしてAPIを組み立てていく必要があると思います。RailsでWebAPIを実装するときは、ドキュメントを読んで仕様を理解する必要があります。
便利な機能が揃っていて、ブラウザ向けアプリケーションの開発とあまり大差ないので楽にかける分、ブラウザ向けアプリケーションでできていたことができないことみたいな制約もあるので、注意が必要です。参考
以下、参考にしたもの。
書籍
サイト
Rails API
API設計
PORTについて
「世界中に、アタリマエとシアワセを。」 を目的に、就活や金融領域などの各産業領域に特化したメディアを展開しています。
- 投稿日:2020-12-04T15:58:46+09:00
$rails s でサーバーが動かない
開発環境
Ruby 2.5.0
Ruby on Rails 6.0.3.4
rbenv 1.1.2
macOS Big Sur v11.0.1エラー内容
rails s
とコマンドを打つと[WARNING] Could not load command "rails/commands/server/server_command". Error: uninitialized constant URI::Generic /Users/user/.rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/active_support.rb:00in 'block in load_missing_constant' (同様の文が何行も出るため中略) /User/user/Desktop/rails_testbin/spring:15:in `<top (required)>' bin/rails:3:in `load' bin/rails:3:in `<main>' (See full trace by running task with --trace)上記のようなメッセージが出て、http://localhost:3000 が起動できない
解決方法
結論から言うと、Rubyのバージョンを2.5.0から2.7.0に変更したら動きました。
※コメントでご指摘をいただいたのですが、変更するバージョンは、バグが少なくなっているという点から、基本的になるべく最新版をインストールしたほうが良いです。(2020/12/05時点では2.7.2が最新)参考記事:
https://info-wcn.com/ruby-changeversion-mac/
https://qiita.com/kamillle/items/5a7befd0ebad47378832
https://qiita.com/chihiro/items/efdf8b88865b7a93971f1.Rubyのバージョン変更
rbenvを最新バージョンへとアップデート
$ brew upgrade rbenv現在のRubyのバージョンを確認
$ ruby -v ruby 2.5.0インストール済みのRubyのバージョンを確認
$ rbenv versions system * 2.5.0 (set by /Users/user/.rbenv/version)これで、現在自分が使っているRubyのバージョンは2.5.0であることが分かりました。
これを2.7.0へとアップデートしていきます。インストールできるRubyのバージョンを確認
$ rbenv install -l バージョンがずらっと表示されるRubyのインストール
$ rbenv install 2.7.0 # インストールするバージョンを2.7.0に指定 $ rbenv rehash $ rbenv versions system 2.5.0 * 2.7.0 (set by /Users/user/.rbenv/version)2.7.0が追加されたことを確認。
Rubyのバージョン切り替え(ローカル)
$ rbenv local 2.7.0 バージョンを指定 $ ls -a .ruby-versionが作成されているか確認 $ less .ruby-version 2.7.0と表示されるバージョンの確認から戻るときは、qを押して戻る
Rubyのバージョン切り替え(グローバル)
$ rbenv global 2.7.0 # バージョンを指定 2.7.0と表示される確認から戻るときは、ローカル同様qを押す
これで、使用するRubyのバージョン切り替えが完了しました。
2.Railsのバージョン変更
1の終了後、
$rails sと打つと(真似しなくてよい)、
rbenv: rails: command not found The `rails' command exists in these Ruby versions: 2.5.0このようになり、上手く動きません。
そこで、https://qiita.com/kamillle/items/5a7befd0ebad47378832 の「対処」を参考にして、新しくインストールしたruby環境にrailsをインストールする
$ gem update --system $ gem install bundler $ gem install rails # 各コマンド間、色々なものがインストールされるため時間がかかるかも $ rails --version # Rails 6.0.3.4と表示された(2020-12-04時点)これでRailsのバージョン変更が完了しました。
3.サーバーが動くか確認
試しに、デスクトップへrails_appという名前のアプリケーションを作成してみます。
$ cd Desktop $ rails new rails_appインストールが完了したら、
$ cd rails_appで移動し、
$ bundle installを実行。
最後に
$ rails sを実行すると
=> Booting Puma => Rails 6.0.3.4 application starting in development => Run `rails server --help` for more startup options Puma starting in single mode... * Version 4.3.7 (ruby 2.7.0-p0), codename: Mysterious Traveller * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://127.0.0.1:3000 * Listening on tcp://[::1]:3000 Use Ctrl-C to stopとなり、無事http://localhost:3000 にアクセスすることが出来ました!!
- 投稿日:2020-12-04T15:50:40+09:00
素数一覧を求めるワンライナー各言語まとめ
お遊び記事です.【追記】お遊びのつもりが,とても秀逸な別解がコメント欄に集まりましたので,そちらもぜひ御参照下さい.とりあえず,J言語すげえ.
アルゴリズムと実装
主に関数型リスト処理を用いて,$x$を$2〜x-1$の各整数で割った余りのリストに0が含まれていなければ$x$を素数と判定しています.このため,任意範囲の整数リスト生成(
range
等),リスト各要素への関数適用(map
等),リストに特定の値が含まれているかの判定(include
等),条件を満たした要素のみをリストから取り出す処理(filter
等)を行う関数があると,より短いワンライナーとなります.各言語での記述例
$1<n<100$の$n$について素数判定を行った結果を表示しています.どうしても長くなりがちなので,字句解析が可能な程度に空白等を詰めています.
Ruby>> ->n{(2...n).reject{|x|(2...x).map{|z|x%z}.include?(0)}}[100] => [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]Haskell> (\n->filter(\x->not$elem 0$(\x->map(\z->rem x z)[2..x-1])x)[2..n])100 [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97]Python>>> (lambda n:[x for x in range(2,n)if not 0 in map(lambda z:x%z,range(2,x))])(100) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]JavaScript> (n=>(r=>r(n).filter(x=>!(i=>r(i).map(z=>i%z).includes(0))(x)))(n=>[...Array(n-2)].map((v,k)=>k+2)))(100) [ 2, 3, //(途中結果は省略) 97 ]Scheme> ((lambda(n)(filter(lambda(x)(not(member 0(map(lambda(z)(modulo x z))(cddr(iota x))))))(cddr(iota n))))100) (2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97)備考
記事に関する補足
JavaScriptに範囲生成関数が標準で用意されていないのがちょっと意外.最近のJavaScriptでは[...Array(n).keys()].slice(s)
あたりが定番の模様(コメントより).更新履歴
- 2020-12-04:冒頭にコメント欄参照の旨追記
- 2020-12-04:初版公開(Ruby,Haskell,Python,JavaScript,Scheme)
- 投稿日:2020-12-04T14:10:02+09:00
prawnで請求書っぽいPDFを作る
本記事はiCARE Advent Calender 2020 の5日目です。
はじめに
本記事ではprawnを用いた請求書っぽいPDFの作成方法を解説します。
RailsでPDF作成する場合だとwicked_pdfも使えますが、wicked_pdfがhtmlライクにPDF作成ができるのに対して、prawnはゴリゴリのDSLなので、学習コストがかかりますが、wicked_pdfよりも柔軟にPDFを作ることができるので、個人的にはprawnの方が好きだったりします。
セットアップ
今回の請求書PDFではテーブル表示を行いたいので、prawn-tableを使います。
# Gemfile gem 'prawn' gem 'prawn-table'でbundle install。
your_app $ bundle install主要なメソッドの紹介
請求書の作成の前に、今回用いる主要なメソッドを紹介したいと思います。
Prawn::Document#generate
https://prawnpdf.org/docs/0.11.1/Prawn/Document.html#method-c-generate
公式の説明文を引用します。
Creates and renders a PDF document.
When using the implicit block form, Prawn will evaluate the block within an instance of Prawn::Document, simplifying your syntax. However, please note that you will not be able to reference variables from the enclosing scope within this block.
PDFドキュメントを作成してレンダリングします。
暗黙的なブロックフォームを使用する場合、PrawnはPrawn :: Documentのインスタンス内のブロックを評価し、構文を簡素化します。 ただし、このブロック内の囲んでいるスコープから変数を参照することはできないことに注意してください。
PDFドキュメントの雛形を作成するメソッドですね。
オプションとして、ページサイズ(A4, B5等)や余白(上下左右)を指定することが出来ます。
# 例 Prawn::Document.generate( 'sample.pdf', page_size: 'A4', top_margin: 35, bottom_margin: 35, left_margin: 35, right_margin: 35 ) do |pdf| # 処理 endPrawn::Document#bounding_box
https://prawnpdf.org/docs/0.11.1/Prawn/Document.html#method-i-bounding_box
こちらも公式を引用
A bounding box serves two important purposes:
・ Provide bounds for flowing text, starting at a given point
・ Translate the origin (0,0) for graphics primitives
A point and :width must be provided. :height is optional. (See stretchyness below)bounding_box には、次の2つの重要な目的があります。
・ 特定のポイントから開始して、流れるテキストの境界を指定します
・ グラフィックスプリミティブの原点(0,0)を変換します
ポイントと:widthを指定する必要があります。 :heightはオプションです。PDF内にボックス要素を作成するのに用いるメソッドですね。
# 例 Prawn::Document.generate( 'sample.pdf', page_size: 'A4', top_margin: 35, bottom_margin: 35, left_margin: 35, right_margin: 35 ) do |pdf| pdf.bounding_box([50, 75], width: 200, height: 300) do # 処理 end end後ほど紹介しますが、テキストを書き込んだり、tableを作成して表示したりできます。
Prawn::Text#text
https://prawnpdf.org/docs/0.11.1/Prawn/Text.html#method-i-text
公式引用
If you want text to flow onto a new page or between columns, this is the method
to use. If, instead, if you want to place bounded text outside of the flow of a ?document (for captions, labels, charts, etc.), use Text::Box or its convenience method text_box.Draws text on the page. Prawn attempts to wrap the text to fit within your current bounding box (or margin_box if no bounding box is being used). Text will flow onto the next page when it reaches the bottom of the bounding box. Text wrap in Prawn does not re-flow linebreaks, so if you want fully automated text wrapping, be sure to remove newlines before attempting to draw your string.
テキストを新しいページまたは列間で流したい場合は、これが使用する方法です。 代わりに、ドキュメントのフローの外側に境界付きテキストを配置する場合(キャプション、ラベル、グラフなど)、Text :: Boxまたはその便利なメソッドtext_boxを使用します。
ページにテキストを描画します。 Prawnは、現在のバウンディングボックス(またはバウンディングボックスが使用されていない場合はmargin_box)内に収まるようにテキストを折り返そうとします。 テキストは、バウンディングボックスの下部に到達すると、次のページに流れます。 Prawnでのテキストの折り返しは改行をリフローしないため、完全に自動化されたテキストの折り返しが必要な場合は、文字列を描画する前に必ず改行を削除してください。
小難しいことが書いてありますが、要は文字をPDF内に記述したい時に使うメソッドになります。
# 例 Prawn::Document.generate( 'sample.pdf', page_size: 'A4', top_margin: 35, bottom_margin: 35, left_margin: 35, right_margin: 35 ) do |pdf| pdf.bounding_box([50, 75], width: 200, height: 300) do pdf.text 'サンプルだよ' pdf.text 'サンプル 左寄りだよ', align: :left pdf.text 'サンプル 右寄りだよ', align: :right pdf.text 'サンプル 文字大きいよ', size: 30 end end例のように、文字の開始位置を決められたり、文字サイズを変更したりすることができます。
Prawn::Document#move_down
引用
https://prawnpdf.org/docs/0.11.1/Prawn/Document.html#method-i-move_down
Moves down the document by n points relative to the current position inside the current bounding box.
現在のbounding_box内の現在の位置を基準にして、ドキュメントをnポイント下に移動します。
ドキュメントを下に移動したい時に用いるメソッドです。
# 例 Prawn::Document.generate( 'sample.pdf', page_size: 'A4', top_margin: 35, bottom_margin: 35, left_margin: 35, right_margin: 35 ) do |pdf| pdf.bounding_box([50, 75], width: 200, height: 300) do pdf.text 'サンプルだよ' pdf.move_down(5) pdf.text 'サンプル 左寄りだよ', align: :left pdf.move_down(10) pdf.text 'サンプル 右寄りだよ', align: :right pdf.move_down(20) pdf.text 'サンプル 文字大きいよ', size: 30 end endテキストとテキストの間を空けたいときなんかに使います。
Prawn::Table#table
Quote from the official Doc
If a block is passed to methods that initialize a table (Prawn::Table.new, Prawn::Document#table, Prawn::Document#make_table), it will be called after cell setup but before layout. This is a very flexible way to specify styling and layout constraints. This code sets up a table where the second through the fourth rows (1-3, indexed from 0) are each one inch (72 pt) wide:
pdf.table(data) do |table| table.rows(1..3).width = 72 end表形式でデータを表示したい場合に使えるメソッドになります。
# 例 Prawn::Document.generate( 'sample.pdf', page_size: 'A4', top_margin: 35, bottom_margin: 35, left_margin: 35, right_margin: 35 ) do |pdf| pdf.table( [['小計', "11000円"], ['消費税', "1000円"], ['合計金額', "11000円"]], column_widths: [50, 100], position: :right ) do |table| table.cells.size = 10 end end請求書を作成する
お待たせしました。今まで紹介したメソッドを用いて、
簡単な請求書めいたPDFを作成したいと思います。invoice_pdf_exporter.rbの作成
app/services下にinvoice_pdf_exporter.rbを作成し、
以下のコードを記述します。require 'prawn' class InvoicePdfExporter FONT_PATH = Rails.root + 'public/fonts/任意のフォントファイル.ttf' def initialize # Prawnドキュメントを生成 # ページサイズやマージンを指定 Prawn::Document.generate( Rails.root + 'invoice.pdf', page_size: 'A4', top_margin: 35, bottom_margin: 35, left_margin: 35, right_margin: 35 ) do |pdf| # フォントを指定しないと Prawn::Errors::IncompatibleStringEncoding 例外が発生する pdf.font FONT_PATH # 本文の生成 self.create_contents pdf end end endfontメソッドは初出なので公式Docから引用します。
Prawn::Document#font
https://prawnpdf.org/docs/0.11.1/Prawn/Document.html#method-i-font
Without arguments, this returns the currently selected font. Otherwise, it sets the current font. When a block is used, the font is applied transactionally and is rolled back when the block exits.
引数がない場合、これは現在選択されているフォントを返します。 それ以外の場合は、現在のフォントを設定します。 ブロックが使用されると、フォントはトランザクションで適用され、ブロックが終了するとロールバックされます。
ドキュメントに記載がありませんが、fontの指定をしないで実行すると、
Prawn::Errors::IncompatibleStringEncoding
の例外が発生します。サンプルコードではpublicディレクトリに配置していますが、app/assets/fontsでもいいと思います。
フォントファイルは、
https://fontfree.me/
に無料フォントがありますので、お好きなものをお使いください。今回はほのか明朝を使います。
create_contentsメソッドを実装する
では具体的な処理を記述します。
コード全晒しです。
def create_contents(doc) # bunding_boxメソッドでボックスを生成 # 引数にはボックス生成位置、横、縦のサイズを指定 doc.bounding_box([50, 750], width: 300, height: 150) do # textメソッドでテキストを挿入。引数には文字サイズとalignを指定できる。:left, :right, :center doc.text "〒123-4567", size: 10, align: :left # move_downメソッドで次のテキストの書き出し位置を下げている doc.move_down 10 doc.text "東京都新宿区新宿1-1-1", size: 10, align: :left doc.move_down 10 doc.text "ご担当者 様", size: 12, align: :left end doc.bounding_box([300, 750], width: 300, height: 150) do doc.text 'HogeHoge株式会社', size: 12, align: :left doc.move_down 5 doc.text '〒103-0021', size: 10, align: :left doc.move_down 5 doc.text '大阪府大阪市中央区中央1-1-1', size: 10, align: :left doc.move_down 5 doc.text '大阪中央ビル4F 経理部経理課', size: 10, align: :left doc.move_down 5 doc.text 'TEL: 03-1234-5678', size: 10, align: :left end doc.text '請求書', size: 20, align: :center doc.bounding_box([0, 550], width: 300, height: 60) do doc.text '下記の通りご請求申し上げます。', size: 10, align: :left doc.move_down 10 doc.text "合計金額 11,000円", size: 16, align: :left end doc.bounding_box([320, 550], width: 300, height: 60) do doc.text "日付: 2020年10月01日", size: 10, align: :left end rows = [['詳細', '数量', '単価', '金額'], ['雑費', '1', '10000', '10000']] # tableメソッドでテーブルを生成する # rowsは多重配列 # 多重配列でない場合 Prawn::Errors::InvalidTableData 例外が発生する doc.table(rows, column_widths: [370, 30, 60, 60], position: :center) do |table| # セルのサイズの指定 table.cells.size = 10 # 1行目のalignを真ん中寄せにしている table.row(0).align = :center end doc.bounding_box([0, 300], width: 300, height: 100) do doc.text "振込期限 202X年10月31日", size: 10, align: :left doc.move_down 10 doc.text '振込先 日本銀行 本店 0000', size: 10, align: :left doc.move_down 10 doc.text ' 普通: 1234567', size: 10, align: :left end doc.bounding_box([373, 300], width: 150, height: 100) do doc.table [['小計', "11000円"], ['消費税', "1000円"], ['合計金額', "11000円"]], column_widths: [50, 100], position: :right do |table| table.cells.size = 10 end end endサンプルコードなので金額や配列の中身をベタ書きにしていますが、
initializeメソッドの引数にデータを渡してやれば、動的なPDFを作成することができます。あとはコンソール上で実行してやりましょう。
your_app $ rails c $ InvoicePdfExporter.new紹介していないけど便利なメソッド
Prawn::Document#start_new_page
Creates and advances to a new page in the document.
Page size, margins, and layout can also be set when generating a
new page. These values will become the new defaults for page creationドキュメント内の新しいページを作成して進みます。
ページサイズ、余白、およびレイアウトは、生成時に設定することもできます。
新しいページ。 これらの値は、ページ作成の新しいデフォルトになります次のページを作成するメソッドです。
Prawn::Document.generate( 'sample.pdf', page_size: 'A4', top_margin: 35, bottom_margin: 35, left_margin: 35, right_margin: 35 ) do |pdf| pdf.table( [['小計', "11000円"], ['消費税', "1000円"], ['合計金額', "11000円"]], column_widths: [50, 100], position: :right ) do |table| table.cells.size = 10 end pdf.start_new_page pdf.text '次のページですよ' endPrawn::Image#image
Add the image at filename to the current page. Currently only
JPG and PNG files are supported. (Note that processing PNG
images with alpha channels can be processor and memory intensive.)ファイル名の画像を現在のページに追加します。
JPGおよびPNGファイルがサポートされています。 (PNGの処理に注意してください
アルファチャネルを備えた画像は、プロセッサとメモリを大量に消費する可能性があります。)PDF内に写真を貼るメソッドです。横縦の幅や寄せる位置などを指定できます。
Prawn::Document.generate("image2.pdf", :page_layout => :landscape) do pigs = "#{Prawn::BASEDIR}/data/images/pigs.jpg" image pigs, :at => [50,450], :width => 450 dice = "#{Prawn::BASEDIR}/data/images/dice.png" image dice, :at => [50, 450], :scale => 0.75 endまとめ
今回紹介したメソッドの他にも様々な機能がありますので、
詳しく知りたい方はドキュメントをご覧ください!https://prawnpdf.org/manual.pdf
iCAREテックブログもよろしくね!!
- 投稿日:2020-12-04T14:01:05+09:00
これから投稿していきます。
Web開発エンジニアを目指すためにいまから頑張ります。
まずは、Ruby、Railsを用いたポートフォリオの作成に挑みます。
事前知識がないので少しアウトプット
ブラウザにプログラムを反映するためにルーティング→コントローラ→ビューの順を踏む
▼ルーティング
コントローラー、ビューがどういった定義で順で実行されていくか
定める役割▼コントローラー
ルーティングを参照しどのビューを実行するか決める役割▼ビュー
具体的なHTMLファイルなど実行されるプログラムを格納する場所
- 投稿日:2020-12-04T13:53:57+09:00
YouPlot - 標準入力からターミナルにグラフを描出するツール
はじめに
こんにちは。昨年のアドベントカレンダーでは、GR.rbというRubyでグラフを描出するツールを紹介いたしました。
今年はYouPlotというツールを作ったので紹介します。
Github: https://github.com/kojix2/youplot
YouPlotは、ターミナルにグラフを描出するソフトです
ターミナルの画面にアスキーアートでグラフを表示したいと思ったことはありませんか?YouPlotはそのためのツールです。グラフを描出するエンジンの部分は、mrknさんの作ったUnicodePlotを利用しています。エンジンの部分を作る記事ではありません。
この記事では、前半でYouPlotの使い方を簡単に紹介し、後半でコマンドラインツールをRubyで作る上で感じたことを書きます。
Uplotのつかい方
インストール
gem install youplotコマンド名はuplotです!1
youplot
でも動きます。uplotヒストグラム
echo -e "from numpy import random;" \ "n = random.randn(10000);" \ "print('\\\n'.join(str(i) for i in n))" \ | python \ | uplot hist --nbins 20散布図
curl -s https://raw.githubusercontent.com/uiuc-cse/data-fa14/gh-pages/data/iris.csv \ | cut -f1-4 -d, \ | uplot scatter -H -d, -t IRIScurl -s https://raw.githubusercontent.com/uiuc-cse/data-fa14/gh-pages/data/iris.csv \ | cut -f1-4 -d, \ | uplot density -H -d, -t IRIS折れ線グラフ
curl -s https://www.mhlw.go.jp/content/pcr_positive_daily.csv \ | cut -f2 -d, \ | uplot line -w 50 -h 15 -t 'PCR positive tests' --xlabel Date --ylabel number箱ひげ図
curl -s https://raw.githubusercontent.com/uiuc-cse/data-fa14/gh-pages/data/iris.csv \ | cut -f1-4 -d, \ | uplot box -H -d, -t IRIS棒グラフ
echo -ne "A:200\nB:300\n" | uplot bar -d: -s ?そのほかの機能
- Kernel.gets が ARGF も取ってきてくれるので、
uplot lines -H -d, hoge.csv
みたいにファイルを指定することもできます。--pass
または-O
オプションをつけると、受け取った標準入力をそのまま標準出力に流し込むことができます。(グラフは標準エラー出力に描出されます)これは、hoge | uplot | fuga
のようにコマンドをパイプラインのチェーンの間に挟んで使用する場合に便利です。このあたりのオプションの仕様は固まっていないのでまた変更になる可能性があります。uplot color
で指定可能な色一覧が表示されます。![]()
uplot count
で要素の数を数えて棒グラフにすることができます。より詳しい使い方は
uplot --help
をご覧ください。uplot bar --help
などとするとより詳しいオプションが表示されます。
UnicodePlotの大半のオプションは指定できるようになっているはずです。2YouPlotを作って感じたこと
バックグラウンド
Rubyではコマンドラインツールが簡単に作れてしまう…
Rubyはコマンドラインツールを作るのが得意です。Macで広く使われているパッケージマネージャーの
homebrew
はRubyで書かれています。YouPlotは
UnicodePlot
に、optparse
を使ってコマンドラインインターフェースをつけたツールです。言い換えると、グラフ描画エンジンUnicodePlotに、データをパイプから読み込む仕組みや、オプションで操作を切り替えられるスイッチを付けて、一つのツールにまとめたものです。技術的に難しいことは何もやっていません。グラフの描出はすべてUnicodePlotにお任せしているのです。でも、便利です。Rubyでお気に入りのツールをラップすれば、簡単にコマンドラインツールを作成することができます。
でもYouPlotと同じようなツールは多くない…
YouPlotを作る前に、私は同じようなツールがないかGoogle検索やGithubで探しました。デファクト・スタンダードになっているような便利なツールがすぐに見つかると思ったのです。けれども、私が探した範囲では、デファクト・スタンダードと呼べるようなツールはありませんでした。(強いて言うならば
gnuplot
がそれに近いかも知れません)それならば、ということで自分で作ることにしました。なぜYouPlotのようなツールはあまり多くないのでしょうか?
その理由はいくつか考えられます。一つは、Unixのコマンドラインツールで作業をしている人の絶対数がそれほど多くないことです。
もう一つの理由は、アスキーアートでグラフを表示するためのライブラリの不足です。自分で描画エンジンを作るのは結構大変です。YouPlotのようなツールを思いついても、作ることができなかった人もたくさんいると思います。
例えば、GOやRustのような言語では、コマンドラインツールが盛んに開発されています。しかし、必ずしもこれらの言語でターミナル上にアスキーアートでグラフを描出するライブラリが特別に豊富というわけではなさそうです。だからそれなりに手間がかかる気がします。
アスキーアートでグラフを描出するライブラリ UnicodePlot
幸いにしてRubyには、ターミナル上にアスキーアートでグラフを表示する便利なライブラリがありました。YouPlotが使っているグラフ描画エンジンは
UnicodePlot
です。これは、Rubyのコミッタで、Julia言語が好きなmrknさんが、JuliaのUnicodePlotをRuby向けに実装し直したものです。Julia言語のUnicodePlotは、おそらくターミナル上にグラフを描出する最も有名なライブラリです。しかし、Julia言語は、REPLやJupyterを通して利用する場合が多く、コマンドラインツールの作成はそれほど活発ではありません。
こんな感じで、いろいろな偶然が重なって、YouPlotもちょっとだけユニークなツールになっているのです。
ツールはみんなユニーク
ツールは、二度と重ならない条件のもとで生まれます。作者、目的、環境、マシン、ユーザー、仲間、同僚、家族、時代… そういったものが交差した座標はどれもユニークです。だから、どのツールも自然とユニークになります。
YouPlotは、もともと生命情報のタブ区切りテキストをすばやく観察したい目的で作られました。なのでリアルタイムに時系列データを表示する機能がありません。一方やはりRubyで実装されているtermplotというツールは、YouPlotより少しあとに開発されましたが、リアルタイムでグラフを描く機能に特化しています。このように作者の目的によってツールは少しずつ異なったものになります。)
さらに、全てのツールは作者に求められて生み出されています。だから、あなたが作っているツールも、おそらくユニークかつ誰かに求められているはずです。私が言いたいことは一つです。みんな、Rubyでツールを作ったら公開しよう。
…
…(さて、今日は十分ポエムを書いたので、もう帰っていいですか)
OptionParserを使ってコマンドラインオプションをつける
コマンドラインツールを作るのにはRuby標準ライブラリのoptparseを使いました。optparseは下記のような構文でコマンドラインオプションを用意してくれます。
require 'optparse' opt = OptionParser.new params = {} opt.on('-a') {|v| params[:a] = v } opt.on('-b') {|v| params[:b] = v } opt.parse!(ARGV)公式リファレンスがまとまっているのでしっかりと目を通します。
optparseの応用は、sonotsさんの下記の記事がとても参考になりました。
(今回YouPlotでは利用していませんが、すこし複雑なツールを作りたい時はPiotr Murachさんが開発しているTTYツールキットが便利です。たとえば、ターミナルにボックスやツリーを表示したり、コマンドを実行して結果を表示する機能などがあります。)
オブジェクト指向のRubyでデータ分析用のツールを作るときに気になること
この項は、主観が強く入っています。私はプログラミングも数学もよくわかりません。コンピュータやプログラミング、数学に造形の深い方らみると間違っている記述もあるかもしれません。一つの意見として聞いてください。
Rubyはデータ処理用の言語としてはそれほど広く使われていません。その理由はいろいろ言われています。ライブラリが足りない、とか。しかし、私の個人的な意見としては、オブジェクト指向とデータ分析相性があまりよくないからだと思います。普及しているデータ処理用の言語、例えばR, Juliaは、どちらもオブジェクト指向らしくはありません。(このような話題では、どうしてもPythonに注目が集まる傾向がありますが、Pythonよりも、RやJuliaとの比較に意味があると思います)
「データ」と「手続き」
オブジェクト指向はむずかしく、私は理解できまている自信はありません。しかし、言いたいことを自分なりに表現するために続けますね。オブジェクト指向とは「データ」と「手続き」を一つのまとまりとして扱うことだと理解しています。しかし、「データ」と「手続き」を一つのカタマリとして扱うこと、これは、データを分析する際には不便な面があると思います。
私たちはデータを分析するとき、「データ」に、さまざまな「手続き」を適応して、うまくいく手続きを探そうとします。あたらしい「手続き」を開発しようとしている人がいます。「データ」にどの「手続き」を適応すると良い結果が出るかを試して、それを仕事にしている人もいます。結果だけほしくて、「手続き」には興味がない人もいます。いずれにせよ、この目的のためには「データ」はデータだけ、「手続き」は手続きだけで管理して、互いに依存しないようにバラバラにしておいた方が安全できれいに見えるでしょう。
一方で、Rubyでは、全てはオブジェクトで構成されており、原則として「手続き」はメソッドとしてすべて作者の意図通りにオブジェクトに所属しています。(単体の手続きは
lambda
やproc
,block
など存在はします)ここでは、オブジェクトにどの「手続き」を適応すればよいのかは、オブジェクト自身が知っています。私たちはオブジェクトがどんなメソッドを持っているのか、オブジェクト自身に問い合わせることができ、自分で「手続き」を管理する必要はありません。そう考えるとRubyでは「データ」と「手続き」の関係性がよく整備された環境で、成果物そのものに注力できる良い環境が整っているとも言えるかもしれません。Rubyのオブジェクトは、インスタンス変数を持っています。なので、メソッドを呼び出すとしばしばオブジェクトの状態が変わってしまいます。これもデータを分析する上では望ましくない性質です。
たとえば、Jupyter Notebookでセルを一つづつ実行していいくことを考えます。セルを実行する順番を変えたり、あとからもう一回実行すると結果が変わってしまうようなワークフローは、データ分析の観点からはあまり望ましくないと思います。そしてJupyter Notebookを使っているような場合には、そのような試行錯誤が頻繁に発生します。このようなケースでは内部の状態が変化してしまうオブジェクトとメソッドは活躍しにくくなります。
このように、オブジェクト指向の持つ「データ」と「手続き」をまとめて管理する傾向は、データ分析ではしばしば不利になるケースがあると私は考えます。
モジュール関数(module function)
そうはいっても、Rubyでも、どうしても「手続き」そのものを扱いたいこともあります。例えば、数学の関数がそれに当たります。RubyではMathモジュールというものが存在し、これらはモジュール関数(module function)として作成されています。モジュール関数は、特異メソッドでありかつ、プライベートメソッドでもあるような関数のことです。
Math.sin(x) # 特異メソッドなので呼べる include Math sin(x) # プライベートメソッドでもあるので呼べる Object.sin(x) # 呼べないMathモジュールは、クラスではなくてモジュールなので、インスタンス変数を持ちません。だから、何回実行しても内部の状態が変わって結果が変わることはありません。
# 馬の耳に念仏モジュール module Horse module_function def hear(sound) "..." end end # Horseはモジュールなので中味は変化しない Horse.hear("念仏") Horse.hear("東風") # 馬耳東風なんだ、Rubyでも関数が作れるんだ、よかった。という話になるでしょうか…。
長々と書きました。YouPlotのような小さなツールを作るためには、こんなことをくどくど考える必要はないかもしれません。けれども、ライブラリを作る前に、一つのオブジェクトをモジュールにしようか、クラスにしようか、Mixinをどうしようかと悩んだりすることは、パッケージを作った方はきっと経験があると思います。
YouPlotでも上記のようなことをあれこれ考えて、部分的にモジュール関数(module function)を使っています。モジュール関数を作るのは簡単で、
module_function
と書くだけです。構造体(struct)
YouPlotでは、パラメータの保持に構造体を使っています。
Qiitan = Struct.new(:name, :age) qiitan = Qiitan.new("キータン", 6) qiitan.age # 6StructはHashと比較して、デフォルトでkeyが固定されています。そのため何かのタイミングで新しくキーが追加されてしまうことを防ぐことができて少し安心です。また、値を取り出す記述がメソッドのように見えるのですこしすっきりします。
モジュール関数や構造体を使うRubyのコードは、どちらかというとあまりRubyらしくない記述法だと思います。けれども、Juliaや他の言語のデータ解析関連のライブラリーはオブジェクトとメソッドという組み合わせよりは、構造体と関数という発想で作られている場合があるのではないかと思います。そういったライブラリをRubyに移植することを考えると、module function や struct のようなものを適材適所で使っていけば、頭を切り替えなくてもすむというメリットがあります。とくにデータ分析のツールを作る上では、活躍する場面があるのではないかと思います。
(一方で、「データ」と、それに適応する「手続き」が一度確立された分野では、Rubyのオブジェクト指向の強みがいかんなく発揮されていくのではないかと思います。)
YouPlotの課題とこれから
リアルタイムにグラフを表示したい
YouPlotはデータをリアルタイムに表示できません。いずれそういったことができるようになるといいなと思っています。
テストとエラーメッセージの整備
勉強して少しずつテストを追加したり、エラーメッセージを整備していきたいと思っています。
前処理・統計処理など
YouPlotのようなツールに簡単な前処理や統計の機能をつけようとするのは自然だと思います。しかし、Rubyのスクリプトでありますのでスピードがでないと思っています。
datamash
などの専用のコマンドと組み合わせる方がUnixらしくて現実的かも知れません。
もしも、YouPlotと同様のツールをGolangやRustで開発したらとても便利なツールになるかも知れません。興味のある方は挑戦してみてください。おわりに
みなさんもRubyのツールを作っていたらぜひ公開してください。
この記事は以上です。
- 投稿日:2020-12-04T13:29:04+09:00
Railsでログイン機能を作る
Railsでログイン機能をつくってみました
今回作るログイン機能はタスク管理アプリで
初回利用時にユーザー登録を行い、その後利用するときは「私は先日に登録したユーザーです」と言う認証を行うやり方。セッションとCookie
ログイン機能を実装する前に前提知識となるセッションとCookieについてはこちらの記事に書きました。
https://qiita.com/ombossk0720/items/5345b8ef267749e550feUserモデルを作る
認証機能を作るにはアプリに新しい概念を加える必要がある。
それはアプリケーションを利用する概念。
そこでユーザーを表すUserモデルを作る。
タスク管理アプリのUserモデルに最低限必要なのはメールアドレスとパスワード。
ただそれだけでは、ユーザーを表すのに常に長ったらしいメールアドレスか、暗号のような数字を
表示しなければいけなくなるので、扱いやすいようにユーザー名を別途保持することにする。generateコマンドを使ってUserモデルをつくってみる
% bin/rails g model user name:string email:string password_digest:string
generateコマンドによって、データベースにuserテーブルを作るためにマイグレーションファイルとUserモデルが作られた。
早速migrationしたいところだがまずマイグレーションファイルを編集。
名前やメールアドレスやパスワードには何かしらの文字列が必ず入るのでこれらの属性にNULLが入ることはない。
また、同じメールアドレスをもつユーザーが複数人いることはない。ありえない値が入ることを防ぐために生成した
マイグレーションファイルを開いて以下のように書き換える。db/migrate/xxxxxxxxxxxxx_create_users.rbclass CreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| t.string :name, null: false t.string :email, null: false t.string :password_digest, null: false t.timestamps t.index :email, unique: true end end end書き換えたらマイグレーションを実行して、データベースにusersテーブルを作成する。
% bin/rails db:migrate
usersテーブルが作られ、generateコマンドでUserモデルも作られている。
ユーザー管理機能一式を追加する
次にアプリケーションの利用者をどうやってUserとしてデータベースに登録するか検討します。よくある形は
1,未登録のユーザーが自らサインアップを行う。
2,管理者がユーザーを登録することで、ユーザーがアプリケーションを利用できるようにする。1は全世界の個人に向けて提供する場合に、2は特定のグループに属する提供する場合に適している。
そしてユーザー管理機能は管理者だけ触れるようにする必要がある。Userモデルにadminフラグを追加する
まず、ユーザーが管理者かどうかを表すフラグを追加する。
以下のコマンドでマイグレーションファイルの雛形を作成する。
% bin/rails g migration add_admin_to_users
ファイルを開き以下のように編集。
db/migrate/xxxxxxxxxxxxx_add_admin_to_users.rbclass AddAdminToUsers < ActiveRecord::Migration[6.0] def change add_column :users, :admin, :boolean, default: false, null: false end endこのマイグレーションはusersテーブルにadminと言うフラグのカラムを追加する。早速適用。
% bin/rails db:migrate
ユーザー管理のためのコントローラを実装する
次にユーザー管理のためのコントローラを作成。
ここでは「管理系」の機能として「ユーザー管理」を行うのでAdmin::UsersControllerと言う名前をつける。
この名前は、Adminと言うモジュールの名前空間の中にUsersControllerと言うクラスを定義するという意味になる。
Railsではモジュール階層を、コードを保存するためのディレクトリ階層に対応させている。app/controllers/admin/users_controller.rbと言うファイルが対応するようになる。
これから「管理系」の機能を足したいとき、Admin::のついたコントローラを追加していけばコードがAdminディレクトリの下にまとまって
わかりやすくなる。まず、Admin::UsersControllerを画面を伴うGETアクションのビューとともに以下のコマンドで作ります。
% bin/rails g controller Admin::Users new edit show index
generateの機能によってnew,edit,show,indexのからのアクションとデフォルトのビューができた。
合わせてroutes.rbにも自動で定義が追加されるがこれは決めている仕様と合わないので、エディタで編集。
admin/users/で始まるURLの書かれた記述を全て削除して次のように内容を編集する。config/routes.rbRails.application.routes.draw do namespace :admin do resorces :users end root to: 'tasks#index' resources :tasks # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html endこれで想定したURLで適切なアクションにリクエストが飛ぶようになる。各機能のURLを得るには、admin_users_pathやadmin_user_pathなど
adminがついたヘルパーメソッドを使う。ここからは各機能を作り込んでいく。
登録の機能
コントローラ(app/controllers/admin/users_controller.rb)とビュー(app/controllers/admin/users/new.html.slim)を以下のように変更。
app/controllers/admin/users_controller.rbclass Admin::UsersController < ApplicationController def index @users = User.all end def show @user = User.find(params[:id]) end def new @user = User.new end def edit @user = User.find(params[:id]) end def create @user = User.new(user_params) if @user.save redirect_to admin_user_url(@user), notice: "ユーザー「#{@user.name}」を登録しました。" else render :new end end def update @user = User.find(params[:id]) if @user.update(user_params) redirect_to admin_user_url, notice: "ユーザー「#{@user.name}」を更新しました。" else render :edit end end def destroy @user = User.find(params[:id]) @user.destroy redirect_to admin_users_url, notice: "ユーザー「#{@user.name}」を削除しました。" end private def user_params params.require(:user).permit(:name, :email, :admin, :password, :password_confirmation) end endここからnew,edit,show,indexと次のようにコードを書いてもいいのですが、面倒なのでパーシャルを使います。
```ruby:app/controllers/admin/users/new.html.slim
h1 ユーザー登録= form_with model: [:admin, @user], local: true do |f|
.form-group
= f.label :name, '名前'
= f.text_field :name, class: 'form-control'
.form-group
= f.label :email, 'メールアドレス'
= f.text_field :email, class: 'form-control'
.form-check
= f.label :admin, class: 'form-check-label' do
= f.check_box :admin, class: 'form-check-input'
| 管理者権限
.form-group
= f.label :password, 'パスワード'
= f.password_field :password, class: 'form-control'
= f.submit '登録する', class: 'btn btn-primary'
```
これを各それぞれのファイルに書き込むのはしんどそうですよね。
ここから先の手順でやれば書き込んだことと同じことになります。パーションファイルはファイル名をから始めます。
今回はform系のことなのでviews/admin/users/index.html.slimと同じ階層に新規でform.html.slimファイルを作成して
以下のようにコードを入力していきます。views/admin/users/_form.html.slim- if user.errors.present? ul#error_explanation - user.errors.full_messages.each do |message| li= message = form_with model: [:admin, user], local: true do |f| .form-group = f.label :name, '名前' = f.text_field :name, class: 'form-control' .form-group = f.label :email, 'メールアドレス' = f.text_field :email, class: 'form-control' .form-check = f.label :admin, class: 'form-check-label' do = f.check_box :admin, class: 'form-check-input' | 管理者権限 .form-group = f.label :password, 'パスワード' = f.password_field :password, class: 'form-control' .form-group = f.label :password_confirmation, 'パスワード(確認)' = f.password_field :password_confirmation, class: 'form-control' = f.submit '登録する', class: 'btn btn-primary'!インデントエラーがよく発生するので注意!
_form.html.slimが作成できたら
index,edit,show,newファイルを以下のように編集していきます。views/admin/users/index.html.slimh1 ユーザー一覧 = link_to '新規登録', new_admin_user_path, class: 'btn btn-primary' .mb-3 table.table.table-hover thed.thed-default tr th= User.human_attribute_name(:name) th= User.human_attribute_name(:email) th= User.human_attribute_name(:admin) th= User.human_attribute_name(:created_at) th= User.human_attribute_name(:updated_at) th tbody - @users.each do |user| tr td= link_to user.name,[:admin, user] td= user.email td= user.admin? ? 'あり' : 'なし' td= user.created_at td= user.updated_at td = link_to '編集', edit_admin_user_path(user), class: 'btn btn-primary mr-3' = link_to '削除', [:admin, user], method: :delete,data: { confirm: "ユーザー「#{user.name}」を削除します。よろしいですか?"}, class: 'btn btn-danger'views/admin/users/new.html.slimh1 ユーザー登録 .nav.justify-content-end = link_to '一覧', admin_users_path, class: 'nav-link' = render partial: 'form', locals: { user: @user }views/admin/users/edit.html.slimh1 ユーザーの編集 .nav.justify-content-end = link_to '一覧', admin_users_path, class: 'nav-link' = render partial: 'form', locals: { user: @user }views/admin/users/show.html.slimh1 ユーザーの詳細 .nav.justify-content-end = link_to '一覧', admin_users_path, class: 'nav-link' table.table.table-hover tbody tr th= User.human_attribute_name(:id) td= @user.id tr th= User.human_attribute_name(:name) td= @user.name tr th= User.human_attribute_name(:email) td= @user.email tr th= User.human_attribute_name(:admin) td= @user.admin? ? 'あり' : 'なし' tr th= User.human_attribute_name(:created_at) td= @user.created_at tr th= User.human_attribute_name(:updated_at) td= @user.updated_at = link_to '編集', edit_admin_user_path, class: 'btn btn-primary mr-3' = link_to '削除', [:admin, @user], method: :delete, data: { confirm: "ユーザー「#{@user.name}」を削除します。よろしいですか?" }, class: 'btn btn-danger'configも追加します
config/locales/ja.ymlmodels: task: タスク attributes: task: id: ID name: 名称 description: 詳しい説明 created_at: 登録日時 updated_at: 更新日時 user: name: 名前 email: メールアドレス admin: 管理者権限 password: パスワード password_confirmation: パスワード(確認) created_at: 登録日時 updated_at: 更新日時以上で、ユーザー管理機能の基礎となる部分は出来上がりで/admin/usersにアクセスすれば利用することができます。
長い道のりでした。
"参考書籍:現場で使える Ruby on Rails5速習実践ガイド"
- 投稿日:2020-12-04T13:11:47+09:00
【個人アプリ作成】情報管理アプリ制作5日目
今日は5日目。Bootstrapの使い方と複数条件指定の機能のキャッチアップをしました。
Bootstrapについて
stratbootstrapからデザインを落としてきて、VScodeで編集するのはできるようになりました。
rubyでいじれるようにするためには、下記の4つのgemのインストールを行うなどが分かりました。
少し、時間がなくてまとめられないので、こちらもまとめていきたいと思います。gem "sass-rails", "~>5.0"
gem "bootstrap-sass", "~>3.3.6"
gem "jquery-rails"
gem "jquery-ui-rails"ただ、BtoBに近い感じなのでBootstrapはバージョンアップが早くてrubyで実装には向かないかもと思い始めました。
そのため、CSSデザインをこだわりすぎるといつまで経っても実装できないので、プログラミングスクールのデザインを流用しつつ、自分で作ろうと思います。複数条件指定の機能
rubyのGemで「ransack」というGemがあり、検索のためのフォームを作成したりコントローラーでの検索メソッドを簡潔にできるようになります。
具体的にはこんなメソッドを組み込むことで検索ができるそうです。eq :条件に合った検索を行う
_lteq :「〜以下」という検索条件
search_form_for :「ransack特有の検索フォームを生成するヘルパーメソッド」、
collectionselectメソッド : DBにある情報をプルダウン形式で表示できるヘルパーメソッドそのほか学べたこととしてはseeds.rbのfiあるには元からあるDBにデータを入れておくことができること、distinctで重複する結果を省くことができることです。検索結果を盛り込み次第、コードも開示できればと思います。
まとめ
こんな感じで毎日書くとなると結構しんどいですが、継続が最大の近道だと思うのでこれからも頑張ります!!
- 投稿日:2020-12-04T12:40:33+09:00
ruby2.7.1, rails 6.0.3でrailsコマンドを叩くと大量の警告が出た話
ruby3以降で
rb_check_safe_obj
が削除されるらしくsqlite3が大量の警告を吐いて邪魔だったので対処した話> rails test Running via Spring preloader in process * **/sqlite3/database.rb:89: warning: rb_check_safe_obj will be removed in Ruby 3.0対応
sqlite3のバージョンを1.4.2以降に更新
# Gemfile gem 'sqlite3', '1.4.2' bundle update対応後
> rails test Running via Spring preloader in process * Started with run options --seed * 5/5: [======] 100% Time: 00:00:01, Time: 00:00:01
- 投稿日:2020-12-04T12:09:15+09:00
フレームワーク各種自作サーバ接続用チートシート
いろいろなフレームワークを、centOS7で接続するときに便利なローカルサーバ接続用のコマンド集です。ローカルホスト接続はよく見るのですが、この条件を毎回調べるのが面倒なので、テスト起動した上で備忘録としてまとめたものです。
※随時修正
PHP編
Laravel
デフォルトポートは8000
#php artisan serve -host 0.0.0.0192.68.11.xx:8000/プロジェクト名
cakePHP
サーバ起動不要
192.168.11.xx/プロジェクト名
CodeIgniter3
サーバ起動不要
192.168.11.xx/プロジェクト名
Symfony
デフォルトポートは8000
#php bin/console server.run 0.0.0.0192.168.11.xx:8000/
Yii2
デフォルトポートは8000
#php yii serve-port=8000192.168.11.xx/yii2/web/
Zend Framework
サーバ起動不要
192.168.11.xx/プロジェクト名/public/
Javascript編
React
記述中
Vue.js
記述中
Ruby編
デフォルトポートは3000
Rails
#bundle exec rails s -b 0.0.0.0※bundle execを省略する方法もあるので、その場合はbundle execは不要。
192.168.11.xx:3000/
Sinatra
デフォルトポートは4567
#bundle exec ruby touch app.rb -o 0.0.0.0192.168.11.xx:4567
Python編
Django
デフォルトポートは8000
①起動設定
#vi /プロジェクト名/setting.py以下のように書き換えて保存
②起動
setting.pyALLOWED_HOSTS = ["*"]#python manage.py runserver 0.0.0.0:8000192.168.11.xx:8000/プロジェクト名
Flask
デフォルトポートは5000
各種pyファイルのサーバ設定に以下のように書き込む。
#サーバ接続設定 if __name__ == "__main__": app.run(host='0.0.0.0')#python pyファイル名192.168.11.xx:5000/
- 投稿日:2020-12-04T12:09:15+09:00
フレームワーク各種ローカルサーバ接続用コマンドチートシート
いろいろなフレームワークを、centOS7で接続するときに便利なローカルサーバ接続用のコマンド集です。ローカルホスト接続はよく見るのですが、この条件を毎回調べるのが面倒なので、今までにテスト起動した上で備忘録としてまとめたものです。
※随時修正
①PHP編
1:Laravel8
デフォルトポートは8000
プロジェクトに遷移し、以下のサーバ起動コマンドを打ち込む
#php artisan serve --host 0.0.0.0192.68.11.xx:8000
2:cakePHP4
サーバ起動不要
192.168.11.xx/プロジェクト名/index.php
3:CodeIgniter4
サーバ起動不要
192.168.11.xx/プロジェクト名/public
4:Symfony4
デフォルトポートは8000
#php bin/console server.run 0.0.0.0192.168.11.xx:8000/
5:Yii2
デフォルトポートは8000
#php yii serve-port=8000192.168.11.xx/yii2/web/
6:Zend Framework2
サーバ起動不要
192.168.11.xx/プロジェクト名/public/
②Javascript編
1:React
デフォルトポートは3000
①webpacker.config.jsに追記する
webpacker.config.jsdevServer:{ host: '0.0.0.0' }②起動
$npm start192.168.11.xx:3000
2:Vue.js
デフォルトポートは8080
①package.jsonに追記
package.json"serve": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js --host 0.0.0.0 --port=xxxx②起動
#npm run serve192.168.11.xx:8080
③Ruby編
1:Rails
デフォルトポートは3000
#bundle exec rails s -b 0.0.0.0※bundle execを省略する方法もあるので、その場合はbundle execは不要。
192.168.11.xx:3000/
2:Sinatra
デフォルトポートは4567
#bundle exec ruby touch 任意のrbファイル -o 0.0.0.0192.168.11.xx:4567
④Python編
1:Django
デフォルトポートは8000
①起動設定
#vi /プロジェクト名/setting.py以下のように書き換えて保存
setting.pyALLOWED_HOSTS = ["*"]②起動
#python manage.py runserver 0.0.0.0:8000192.168.11.xx:8000/プロジェクト名
2:Flask
デフォルトポートは5000
各種pyファイルのサーバ設定に以下のように書き込む。
#サーバ接続設定 if __name__ == "__main__": app.run(host='0.0.0.0')#python 任意のpyファイル192.168.11.xx:5000/
- 投稿日:2020-12-04T11:32:45+09:00
バイナリーサーチについて
バイナリーサーチとは?
ソース済みのリストや配列に入ったデータに対する検索を行うときに用いられる手法。
まずは中央値を確認し、検索したい値との大小関係を用いて検索したい値が中央の値の右にあるか、左にあるかの判断を繰り返し片側には存在しないことを確かめながら検索する手法。記述例
def binary_search(array, number_of_elements, target) left = 0 right = number_of_elements - 1 while left <= right center = (left + right) / 2 if array[center] == target return center elsif array[center] < target left = center + 1 else right = center - 1 end end return -1 end array=[1,3,5,6,9,10,13,20,26,31] puts "検索したい数字を入力してください" target = gets.to_i number_of_elements = array.length result = binary_search(array, number_of_elements, target) if result == -1 puts "#{target}は配列内に存在しません" else puts "#{target}は配列の#{result}番目に存在します " end
- 投稿日:2020-12-04T11:26:48+09:00
RSpecを使った四則演算の構文
はじめに
RubyやRailsでポートフォリオを作成していく上で、避けては通れないテスト実装。
Railsの標準装備はMinitestですが、実際の現場で使うことを考えて、RSpecを採用される方が多いと思います。そこで、
「RSpecとはどういうものなのか?」
ということを、基本的な四則演算の構文を見ながら解説していきます。対象
これからRSpecを学ぼうと思っている人
環境
Ruby 2.6
RSpec 3.9RSpecの構文例
describe
は、国語でいうところの『章』のような意味合いです。
「この章では、いったいどんなテストコードが書かれているのか?」ということを表しています。
it A do
で、「Aという結果をもたらすテストコードである」という意味を表します。
expect X.to eq Y
で、「X = Y」という意味を持ったテストコードになります。
eq
はマッチャと呼ばれ、「=」にあたる意味を持っています。例を見ていきましょう。
Rspec.describe '四則演算' do describe '足し算' do it '1 + 1 は 2 である' do expect(1 + 1).to eq 2 end end describe '引き算' do it '2 - 1 は 1 である' do expect(2 - 1).to eq 1 end end describe '掛け算' do it ' 2 × 3 は 6 である' do expect(2 * 3) to eq 6 end end describe '割り算' do it '6 ÷ 2 は 3 である' do expect(6 / 2) to eq 3 end end end逆に間違っていることを期待する場合には、
not to
を使います。Rspec.describe '四則演算' do describe '足し算' do it '1 + 1 は 3 ではない' do expect(1 + 1).not_to eq 3 end end describe '引き算' do it '2 - 1 は 0 ではない' do expect(2 - 1).not_to eq 0 end end describe '掛け算' do it ' 2 × 3 は 8 ではない' do expect(2 * 3).not_to eq 8 end end describe '割り算' do it '6 ÷ 2 は 2 ではない' do expect(6 / 2).not_to eq 3 end end endおわりに
RSpecは、独自の構文のため、中々とっつきづらいかと思われます。
まずは分かりやすい構文から理解し、RSpecに慣れていきましょう。
- 投稿日:2020-12-04T11:03:33+09:00
RailsのDBをmysqlに変える(メモ)
RailsでデフォルトのDB'SQlite'から'mysql'(ver8.0.22)に変更する際に詰まったので、
mysqlインストールから実際にDB作成をするところまでの手順をメモ。1.mysqlインストーラでmysqlをインストール。
http://dev.mysql.com/downloads/installer/
からインストーラをダウンロード。
2.ダウンロードしたインストーラを起動。
あとは流れに沿ってインストールしていく。(rootのパスワード設定等。)
そして、この流れの中で後にRailsからmysqlを起動しようとするとErrorになってしまう設定項目があった。それがこの設定。
ここで上の認証方式を選択してしまうと、認証方式が'caching_sha2_password'になるが、Railsではこの認証方式に対応していないらしい。(この認証方式はmysql8で使用可能。)
なので、下の設定を選択して認証方式を'mysql_native_password'にする必要があった。3.gemファイルでmysqlをinstall。
Gemfile# Use mysql as the database for Active Record gem "mysql2"bundel install4.railsアプリケーションのDB設定
config/database.ymldefault: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: host: localhost timeout: 5000 development: adapter: mysql2 encoding: utf8 reconnect: false database: <%= ENV['DATABASE_DEV_NAME'] %> pool: 5 username: <%= ENV['DATABASE_DEV_USER'] %> password: <%= ENV['DATABASE_DEV_PASSWORD'] %> host: <%= ENV['DATABASE_DEV_HOST'] %> # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: adapter: mysql2 encoding: utf8 reconnect: false database: アプリケーション名_test pool: 5 username: root password: <%= ENV['DATABASE_TST_PASSWORD'] %> host: localhost production: adapter: mysql2 encoding: utf8 reconnect: false database: アプリケーション名_production pool: 5 username: アプリケーション名 password: <%= ENV['アプリケーション名_DATABASE_PASSWORD'] %> host: localhostのように変更。
5.パスワードを環境変数で管理する。
dotenvというgemをinstall。Gemfilegem "dotenv-rails"bundle installアプリケーションのルートディレクトリに.envファイルを作成し、その中に環境変数を設定する。
.envDATABASE_DEV_NAME = 'アプリケーション名_development' DATABASE_DEV_PASSWORD = 'パスワードを記入' DATABASE_DEV_USER = 'MySQLユーザー名を記入' DATABASE_DEV_HOST = 'localhost' DATABASE_TST_PASSWORD = 'パスワードを記入'.gitignoreファイルに.envを追加する。
.gitignore# Ignore env config /.env※mysqlユーザを作成しておく。
ここで
rails db:create完了。
※mysqlユーザを作成する。
1.mysqlに接続する。
コマンドプロンプトを管理者権限で開き、以下を実行する。net start mysql80mysql -u root -pmysqlインストール時に設定したパスワードを入力。
2.実際にmysqlユーザを作成する。
mysql> create user 'test-user'@'localhost' identified by 'password';3.mysqlユーザに権限を与える。
mysql> grant all on *.* to 'test-user'@'localhost';
- 投稿日:2020-12-04T11:02:24+09:00
RailsのSystem SpecでJavaScriptエラーを検出する方法
はじめに
最近のWebサイトであれば、JavaScriptを利用していないページを探すほうが難しいかと思います。
そんな中で単純にRailsのSystem Specによる検証だけだと、テストを通ったのでリリースしたら実は以下のようなJavaScriptエラーが発生していたみたいな見落としが起こることが時々ありますよね。解決方法としての理想はJavaScriptが影響する全ての箇所を網羅するようなテストを書くことかもしれませんが、現実的にはそこまで書けないというケースが多いかと思います。
また、全てを網羅していたつもりでも実際には漏れがあって本番環境のコンソールにJavaScriptエラーが表示されていたみたいなことは起こりえます。そこで今回の記事ではRailsのSystem SpecでJavaScriptエラーを検出する方法を紹介します。
設定例
spec/support/system_test.rbmodule Capybara module Matchers class JsNoErrorMatcher def matches?(page) errors = page.driver.browser.manage.logs.get(:browser) errors.find_all { |error| error.level == 'WARNING' }.each do |error| STDERR.puts 'WARN: javascript warning' STDERR.puts error.message end @severe_errors = errors.find_all { |error| error.level == 'SEVERE' } @severe_errors.empty? end def description 'have no js errors' end def failure_message @severe_errors.map(&:message).join("\n") end end def have_no_js_errors JsNoErrorMatcher.new end end end RSpec.configure do |config| config.include Capybara::Matchers, type: :system driven_by :selenium, using: :headless_chrome config.after(:each, type: :system) do |example| if example.metadata[:skip_js_error] # ログリセット page.driver.browser.manage.logs.get(:browser) else expect(page).to have_no_js_errors end end end※説明を簡単にする為に1ファイルにまとめて書いていますが実際は分けたほうが綺麗だと思います。
使い方
これまで通りSystem Specを実行することで、JavaScriptエラーが発生した場合には、発生したエラーメッセージが表示されてテストが失敗するようになります。
もしも外部のスクリプトに依存している等の理由により、エラーを無視するしかないみたいなケースでは以下のように書くことで特定のテストだけJavaScriptエラーの検証をスキップすることが出来ます。
it 'ログインできること', skip_js_error: true do visit root_path # ... endまとめ
というわけで、この記事では「System SpecでJavaScriptエラーを検出する方法」を紹介しました。
既存のテストに設定を足すだけで、JavaScriptエラーに気付けるようになったり、不安定なテストの原因が特定しやすくなったりと役立つことが多いかと思います。
- 投稿日:2020-12-04T10:48:40+09:00
【Rails】 備忘録:Deviseのモデル名を間違えて生成してしまったときの対処法
タイトルの通り、Rails Deviseのモデル名を間違えて生成してしまったときの対処法をメモします。
$rails generate devise MODEL #任意のモデル名このモデル名を間違えて生成してしまいました。
結果、以下のように複数ファイルが、間違えたモデル名で生成されました。Running via Spring preloader in process 12067 invoke active_record create db/migrate/20201204011415_devise_create_models.rb create app/models/model.rb invoke test_unit create test/models/model_test.rb create test/fixtures/models.yml insert app/models/model.rb route devise_for :models
ではこれを削除するにはどうするかというと
$rails destroy devise MODEL #任意のモデル名先ほどのgenerateの部分をdestroyにすればいいだけです。
これで先ほど尽かされたファイルや追記されたコードが削除されます。
ターミナルの結果にて確認できます↓Running via Spring preloader in process 12224 invoke active_record remove db/migrate/20201204011415_devise_create_models.rb remove app/models/model.rb invoke test_unit remove test/models/model_test.rb remove test/fixtures/models.yml route devise_for :models
もし、
$rails db:migrate
コマンドでデータベースを反映させてしまった場合はrollbackを実行して、データベースを元に戻してください。
と、その前にまずはどこまでmigrateされているか現在の状況を把握するところから始めます。
(現在の状況を知らずにとにかくrollbackを行うと、何をrollbackしてしまったのかわからなくなるからです。特にチームでやる場合は必須。)←だそうです(実務未経験です)このコマンドで確認します。
$rails db:version Current version: 2020112909502820201129095028(migrateした最新の日付)までmigrateされていることが確認できます。それでrollbackしても問題なければ、以下を実行します。
$ rails db:rollbackもう一度同じ手順で、正しいモデル名でモデルを生成し、$rails db:migrate したら完了です。
- 投稿日:2020-12-04T10:48:40+09:00
【備忘録】Rails Deviseのモデル名を間違えて生成してしまったときの対処法
タイトルの通り、Rails Deviseのモデル名を間違えて生成してしまったときの対処法をメモします。
$rails generate devise MODEL #任意のモデル名このモデル名を間違えて生成してしまいました。
結果、以下のように複数ファイルが、間違えたモデル名で生成されました。Running via Spring preloader in process 12067 invoke active_record create db/migrate/20201204011415_devise_create_models.rb create app/models/model.rb invoke test_unit create test/models/model_test.rb create test/fixtures/models.yml insert app/models/model.rb route devise_for :models
ではこれを削除するにはどうするかというと
$rails destroy devise MODEL #任意のモデル名先ほどのgenerateの部分をdestroyにすればいいだけです。
これで先ほど尽かされたファイルや追記されたコードが削除されます。
ターミナルの結果にて確認できます↓Running via Spring preloader in process 12224 invoke active_record remove db/migrate/20201204011415_devise_create_models.rb remove app/models/model.rb invoke test_unit remove test/models/model_test.rb remove test/fixtures/models.yml route devise_for :models
もし、
$rails db:migrate
コマンドでデータベースを反映させてしまった場合はrollbackを実行して、データベースも削除してください。
と、その前にまずはどこまでmigrateされているか現在の状況を把握するところから始めます。
(現在の状況を知らずにとにかくrollbackを行うと、何をrollbackしてしまったのかわからなくなるからです。特にチームでやる場合は必須。)←だそうです(実務未経験です)このコマンドで確認します。
$rails db:version Current version: 2020112909502820201129095028(migrateした最新の日付)までmigrateされていることが確認できます。それでrollbackしても問題なければ、以下を実行します。
$ rails db:rollbackもう一度同じ手順で、正しいモデル名でモデルを生成し、$rails db:migrate したら完了です。