20210504のRubyに関する記事は21件です。

Rails6でカスタムフォントを使用する方法

はじめに Rails5では簡単にカスタムフォントを追加できたのですが、Rails6で追加するのに手こずりましたので、私が実際に試した方法を簡単にご紹介します。 環境 Ruby 3.0.1 Rails 6.1.3.1 ① カスタムフォントの配置 まずカスタムフォントを GoogleFonts などから使用する素材をダウンロードし /app/assets/fonts 内にカスタムフォントファイルを配置します。 ※ /app/assets/fontsフォルダがない場合は、/app/assets/内にfontsフォルダを作成してください。 ② application.css の編集 application.cssにカスタムフォントの情報を記述していきます。 application.css /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ @font-face { font-family: "NotoSansJP"; font-style: normal; src: asset_url('NotoSansJP-Regular.otf') ; } .css のままだと Rails がカスタムフォントを認識してくれないので、.scssに拡張子を変更します。 Rails6からは、/app/assets/stylesheets 内の applicationファイルの拡張子が .cssで生成されるようになったようです。 ③ assets:precompile の実行 コマンドで、rails assets:precompile を実行し変更を反映させます。 rails assets:precompile 無事反映ができていれば、コマンドの結果にコンパイルされたカスタムフォントのファイル名が表示されます。 最後に カスタムフォントの反映ができず、インターネットで調べましたが、Rails6の情報が少なく解決しませんでした。 カスタムフォントがコンパイルされなかったので、ファイルを調べていると、applicationファイルの拡張子が .cssなのに気づき.scssに変更したところ上手く反映できました。 この事象に悩まされていた方のお役に立てれば幸いです。 以上、Rails6でカスタムフォントを使用する方法のご紹介でした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【rails6】結合テストコードでBASIC認証を通す方法

こんばんは! 引き続きテストコードについて記載していきます 今日は結合テストコードです。まずはUserに関するテストコードを書いていくのですが… 現状 私のアプリケーションにはBASIC認証をかけていますので、何も対策しないとタイムアタックが開催されます 手入力で間に合わせるのは至難の業です というか、テストコードの意味がありません笑 そこで、テストコード内に ”BASIC認証を通す記述” を加えてあげれば良いのです では記述していきましょう… 改善前の記述 spec/system/user_spec.rb require 'rails_helper' RSpec.describe "Users", type: :system do before do @user = FactoryBot.build(:user) end context 'ユーザー新規登録ができるとき' do it '正しく情報を入力すれば新規登録ができる' do # トップページに移動する visit root_path # 以下省略 end end end ここに設定したBASIC認証のIDとパスワードを読み込ませます。 私は上記2つを "環境変数" に代入してアプリケーションを作成しておりますので、環境変数を別の変数に代入し関数として別保管していきます spec/system/user_spec.rb require 'rails_helper' RSpec.describe "Users", type: :system do before do @user = FactoryBot.build(:user) end def basic_auth(path) # ここを追記 name = ENV["BASIC_AUTH_USER"] password = ENV["BASIC_AUTH_PASSWORD"] visit "http://#{name}:#{password}@#{Capybara.current_session.server.host}:# {Capybara.current_session.server.port}#{path}" end context 'ユーザー新規登録ができるとき' do it '正しく情報を入力すれば新規登録ができる' do # トップページに移動する     basic_auth root_path # ここも追記 visit root_path # 以下省略 end end end 上段の追記箇所 def basic_auth(path) ここでは、 ①自身で設定した環境変数を変数に代入する処理 ②visitを使って、BASIC認証のパスにアクセスしている 上記2点を処理させています 次の追記部分 basic_auth root_path 私はroot_pathからスタートさせる結合テストを記述しているのでこうなりました。 root_pathは引数になりますので、記述漏れがあるとエラーを起こしますよ! さて、これで無駄なタイムアタックから脱しましたので、続きの結合テストコードを書くことにします ありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

50歳からのプログラミング言語入門

転職(16) 65歳からのプログラミング入門 https://qiita.com/kaizen_nagoya/items/1561f910c275b22d7c9f 上記記事のうち、50歳からの話を、表を作ったことで切り離すことにした。 50歳から毎年1言語学習を目指してきた。 1言語習得とは言えない。勉強しても使えるようになるとは限らないから。 最低でも基本的な教科書の1冊は全部動かしてみる。 Uml./~python, Ruby, spin, C#, Coq, SML#, VDM, Event-B.uppaal, VHDL, ObjectiveC, R, System C, SMV, alloy。 Haskell, prolog, Z, verifarstは処理系を起動しただけで、ほぼ手が止まっている。 教師の欄は、直接教えてもらった人以外に、 いろいろやりとりのあった人、 本を参考にさせていただいた人などを含みます。 短縮名 内容 Go 打って動かした量 check 検査した量 edu 教育した量 get 習得度。10が先生、1が初心者 量の表示で、0.1は1の100分の1、10は1の100倍。 年齢 西暦 言語 教師 Go check edu. get 50 2004 Verilog HDL 末吉敏則 1 3 6 10 51 2005 UML 児玉公信 3 3 3 9 52 2006 SPIN 青木利晃 1 2 3 6 53 2007 VHDL 渡部謹二 0.5 2 2 5 54 2008 R 石井一夫 1 3 3 7 55 2009 Uppaal 掘武司 0.3 1 3 4 56 2010 Event-B 掘武司 0.5 1 1 3 57 2011 SMV 早水公二 0.4 0.5 1 2 58 2012 VDM 栗田太郎 0.1 0.2 0.4 0.7 59 2013 Ruby 田中和明 1 2 2 5 60 2014 alloy 斉藤直希 0.2 0.2 0.4 0.8 61 2015 Objective C 斉藤直希 0.5 1 1 3 62 2017 coq 今井宜洋 0.2 1 1 2 63 2018 python 東北大学 1 2 5 8 64 2019 SML# 齋藤啓太 0.1 0.4 0.4 0.9 65 2020 C# 尾崎秀典 1 1 2 4 66 2021 Kotlin Youtube 0.1 0.1 0.1 0.3 67 2022 Go これから 0 0 0 0 Errata of the Book "Modeling in Event-B" http://www.event-b.org/A_errata.pdf ちなみに、50歳までにお金になった仕事で取り組んだことがある言語は、 アセンブラ、C、C++, JAVA, Basic, Fortran, COBOL、LaTeX。10年に3つくらいの言語しか学習していない。20代に、アセンブラ、BASIC, COBOL, fortranをやっていて、30代にC++とJAVAとLaTex。40代は、、、。 LISPとSmallTalkは動かして見たけど実用的または研究的なプログラムは1行たりとも書いたことがなかった。 Squarkの子供向け教育は何度か行った。 Small Talkのデバッグは少しした。 電総研では、LISPが主言語の研究室に3ヶ月滞在したことがある。最初の1ヶ月でSmall C Compailerを入力し、次の1ヶ月でPascalで書かれたコンパイラをC言語に移植し、OBJという言語のシンタックスチェッカを作成して学会で発表した。 当時、パナソニック在籍の田中伸明さんも電総研にいたらしいことを後で知った。 仮説・検証(153)成功体験は語っても、成功体験に頼らないために。清水吉男・田中伸明・柏原一雄 https://qiita.com/kaizen_nagoya/items/d32adfaf7b2568bfd9d2 電総研当時に書いたまとめで、SEAで発表した際に、気が付いたことと同じ現象をEvent-Bでも経験した。 Errata of the Book "Modeling in Event-B" http://www.event-b.org/A_errata.pdf 50歳以降で新たに学習した言語で、役にたったのはSlideshareで人気記事のVerilog HDL、VHDL以外には、pythonとRかもしれない。C#, Rubyもなんとか役立てようとしている。 How to use STARC RTL Design Style Guide Verilog-HDL 2011 version https://www.slideshare.net/kaizenjapan/how-to-use-starc-rtl-design-style-guide-veriloghdl-2011-version JAVA, Python, JavaScript, C# あなたはXML処理ソフトをどの言語で書きますか? https://qiita.com/kaizen_nagoya/items/08cc37b64d0995774fb1 趣味で使うなら、機械学習できなきゃ。 組織の経営分析をするのにRかPythonがいいかも。 この記事は、当時64歳のプログラマが、同世代のプログラムを組んだことがない方に、相談に応じた記録でもあります。 機械語、アセンブラでも楽しむ方法はある。アセンブラ短歌とか。 趣味ですぐに役立つプログラミングならpythonかも。 2017 年から開催した「ゼロから始めるDeep Learning読書会」 https://qiita.com/kaizen_nagoya/items/537b1810265bbbc70e73 に65歳以上の方が数人参加されており、pythonの導入、習得にいろいろ苦労されている状態を手助けした記録も兼ねています。 機械学習はdocker利用をお勧めしています。 なぜdockerで機械学習するか 書籍・ソース一覧作成中 (目標100) https://qiita.com/kaizen_nagoya/items/ddd12477544bf5ba85e2 古いWindowsしかない方で、どうしてもWindowsでpythonをしたいという方のために作った資料はこちら。 Windows(M.S.)にPython3(Anaconda3)を導入する(7つの罠) https://qiita.com/kaizen_nagoya/items/7bfd7ecdc4e8edcbd679 自分のQiita記事で、一番viewsが多い。読んで、うまくanacondaが導入できたら「いいね」をお願い。 dockerもWindowsもと思う方に、お勧めしているのはRaspberry PI。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RSpec導入してCircleCIに組み込んで見る

今までプロダクト優先で自動テストを書くのが億劫だったことを反省し、ついに意を決してRSpecを導入しました。 目標は 重要なモデルのバリデーションテスト コントローラー(request)の200確認・DB更新確認 ボタンやリンク経由で画面遷移のシナリオテスト(フィーチャーテスト) CircleCIでRSpecのテストをデプロイ前に入れ込み、OKとならなければデプロイしないようにする をすることです。もちろん究極は先にSpecを書いて、コードを実装するテストドリブン開発が理想ですが、今回後付になってしまったので現状の挙動を担保するという思想でテストを作成してます。 ただ、テストコードを書くと、意外なことにModelを端折ってしまって書いてないので、テストコード自体が動作しないというケースもあったりしてソース自体にも手を加える必要は出てきます。そうすると動いているソースに手を入れない的な昔からあるようなセオリーで押しきれないことがわかります。これもソースのメンテナンス性を上げるためのRSpecの効能と考えていいでしょう。 Gemfile group :development, :test do gem "rspec-rails" # RSpec本体 gem "factory_bot_rails" # FactoryBot gem 'spring-commands-rspec' # RSpecのパフォーマンスを上げるGem gem 'capybara', '~> 2.15.2' # フィーチャーテストのためのCapibara gem 'rspec_junit_formatter' # CircleCI、JenkinsなどCIでSpec結果を読みやすくするためのGem end ちなみにRSpecは実行すると自動的に環境はRAILS_ENV=testになります。なので専用のデータベースをdevelopment用と別に作成する必要があります。またRails 5.1以降はテストで発生したデータを消すためのdatabase_cleanerは不要だそうです。これは5.1以降はテスト後にDBが自動的にRollbackしてデータを消すようになっているためだとか。データベースの作成に合わせてDBの設定にも以下を追加します。ちゃんとテスト用のDB名にしてあげるのがミソです。 config/database.yml # 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: <<: *default database: test_db また、config/environments/test.rbも作成します。環境別固有設定を施すためです。 ほとんどdevelopment環境と一緒なのでconfig/environments/development.rbをコピーするだけですが、自分はweb_consoleを使っていてRSpecが動かないことがあったので以下の設定を追加しています。 config/environments/test.rb # web_console config.web_console.development_only = false ここまでお膳立てしてようやく bundle とジェネレーターで環境を整えます。 bash # RSpecインストール $ bundle install # 定型的な設定ファイルの作成 $ rails generate rspec:install Running via Spring preloader in process 266 create .rspec create spec create spec/spec_helper.rb create spec/rails_helper.rb rails_helper.rbとspec_helper.rbの役割の違いはよくわからないですが、ほとんどのSpecファイルにrails_helper.rbへの読み込みが入るので、こちらにもっぱらヘルパーの設定を加えます。 今回はdevise使うのでそれ用のヘルパーとダミーデータ作成のためのFactoryBotに設定のヘルパーを加えています。コードの最後の方に追加しました。 spec/rails_helper.rb config.include Devise::Test::IntegrationHelpers, type: :request config.include FactoryBot::Syntax::Methods Deviseを使用しているアプリケーションの場合、ヘルパー本体はこちらに書きます。 spec/support/request_spec_helper.rb module RequestSpecHelper include Warden::Test::Helpers def self.included(base) base.before(:each) { Warden.test_mode! } base.after(:each) { Warden.test_reset! } end def sign_in(resource) login_as(resource, scope: warden_scope(resource)) end def sign_out(resource) logout(warden_scope(resource)) end private def warden_scope(resource) resource.class.name.underscore.to_sym end end ベースとなる準備は以上です。 Seed-fuでマスターデータを用意 アプリケーションにもよると思いますが、ある程度マスターデータが用意されてないと凝ったテストが出来ないという人はFactoryBotではなくてSeedでDBに値を事前に入れておくのがいいかと思います。Railsに元からあるseedよりもseed-fuの方が使い勝手良さそうなのでこちらをインストールします。データの自動投入に汎用的に使えるGemです。 Gemfile # Seed -fu gem "seed-fu" bundleで入れたら以下のファイルを作成します。 db/seed.rb Spree::Core::Engine.load_seed if defined?(Spree::Core) Spree::Auth::Engine.load_seed if defined?(Spree::Auth) あとは投入データを作成します。投入するためのスクリプトはこちらです。これがそのままDBの定義とseedの実行母体になります。 db/fixtures/test/001_areas.rb require 'csv' csv = CSV.read('db/fixtures/test/001_areas.csv') csv.each do |csvdata| Area.seed do |s| s.id = csvdata[0] s.prefstate_id = csvdata[1] s.photo = csvdata[2] s.lat = csvdata[3] s.lng = csvdata[4] s.created_at = csvdata[5] s.updated_at = csvdata[6] end end 以下データ。 db/fixtures/test/001_areas.csv 1,13,"",,,"2016-08-15 00:00:00","2016-08-15 00:00:00" 2,13,"",,,"2016-08-15 00:00:00","2016-08-15 00:00:00" 3,13,"",,,"2016-08-15 00:00:00","2016-08-15 00:00:00" ちなみにdb/fixtures/の下のtestは環境名です。上記を設定したら RAILS_ENV=test rails db:seed_fu でデータ投入できますが、この際に環境ごとに投入データを変えられます。そのためにディレクトリで分けているのです。ちなみに実行はファイルの接頭辞の番号順に行われますので、外部キー制約など順番を考慮したい場合はうまく活用してください。 その他DB操作で便利なコマンド RAILS_ENV=test rails db:reset #テーブル作り直し RAILS_ENV=test bundle exec rake db:schema:load #テーブル作り直し RAILS_ENV=test rails db:seed_fu FILTER=029_styles FIXTURE_PATH=./db/fixtures/test #フィクスチャーごとにデータ投入 FactoryBotの作成 テストのたびにbeforeとかにDBのレコードを作成して用意するのが嫌(というかソースも見づらくて気持ち悪い)なので、データの生成をスマートにするためにFactoryBotというツールがあります。元々FactoryGirlという名称でしたが、ジェンダーコンシャスの流れを受けて今の名前になりました。既にGemfileに追加してインストールしているので後はファイルをspec/factories配下に書いていくだけです。必要となる各モデルごとに作成していきます。 FactoryBot.define do factory :user do sequence(:name) { |n| "TEST_NAME#{n}"} # 名前のバリデーションにかからないもの sequence(:email) { |n| "TEST#{n}@example.com"} # メールのバリデーションにかからないもの password { 'password' } # パスワードのバリデーションにかからないもの end end 番号などシーケンシャルなものを挿入したい場合はsequenceというのを使います。 FactoryBot.define do factory :review, class: Review do restaurant_id {1620} user_id {1} rating {5} title {'What a hell?'} image {Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/fixtures/image.jpg'))} comment {'It was yummy, indeed'} end end 画像データをカラムに入れている場合は上記のように画像のダミーデータをspec/fixtures配下に置いてRack::Test::UploadedFile.newとします。 リレーションがあるモデル(has_one/has_many/belongs_to)の場合、 app/models/menu.rb class Menu < ActiveRecord::Base has_many :menu_translations, dependent: :destroy app/models/menu_translation.rb class MenuTranslation < ActiveRecord::Base belongs_to :menu 以下のようにFactoryBotを作成します。 spec/factories/menu.rb FactoryBot.define do factory :menu, class: Menu do restaurant_id {1} menu_type {5} image {Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/fixtures/image.jpg'))} last_update_user_id {1000} add_attribute(:public) {'1'} association :restaurant end end ちなみにカラム名がRSpecの予約語とかぶってしまうものを使っている場合(aliasとかpublicなど)、add_attribute(:public)という書き方をすることが出来ます。 spec/factories/menu_translation.rb FactoryBot.define do factory :menu_translation_ja, class: MenuTranslation do menu_id {} menuname {'パルミジャーノとケールのイタリアン菜園サラダ'} description {'SサイズとMサイズがあります。'} price {1000} currency {'円'} lang {'ja'} association :menu end end associationでリレーションを定義します。 Modelのスペック deviseをログイン認証機構に用いた場合のモデルのテストは以下の通りです。 deviseで注意すべきはモデル(今回だとUser)に書いてないバリデーションもあり、そちらもテストするのか、それ以外のカスタムバリデーションだけテストするのかを考えてコードを記載します。デフォルトではメールアドレスとパスワードの存在チェックはdeviseに組み込まれているようでした。 それ以外は普通にバリデーションやメソッドについてテストを記載します。 spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do let(:user) { build(:user)} describe 'Check Validations' do it 'default OK' do expect(user.valid?).to eq(true) end it 'if no password, then NG' do user.password = '' expect(user.valid?).to eq(false) expect(user.errors[:password]).to include 'パスワードを入力してください' end it 'if no name, then NG' do user.name = '' expect(user.valid?).to eq(false) end it 'if name exceed 120 letters, then NG' do user.name = '1234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567890000000000000123456789012345678901234567890' user.completed_at = Time.current expect(user.valid?).to eq(false) end end end こちらがModelの本体です。これをテストするためにSpecを記載しました。 app/models/user.rb class User < ApplicationRecord mount_uploader :pic, ImageUploader attr_accessor :current_password validates :name, length: { minimum: 1, maximum: 120 }, if: :is_registration? validates :name, presence: true end FactoryBotには正常系を作成しておいて、バリデーションのテストの中で異常データを挿入してvalid?を試すイメージです。 Controllerのスペック EverydayRails ‐ RSpecによるRailsテスト入門というRSpecの有名書籍によればコントローラーのテストはやがてなくなり、requestsのテストとして集約されるとのことです。いろいろな現場でテストを書かせてもらいましたが、contorollerにテストを書いているところは多いです。コントローラーにアクセスして、所定のキーワードがあるかどうか、関連するモデルの数が増えたり減ったりするかといったテストです。スマホアプリ構築の流れでWebAPIの開発というパターンも多いことからこれをテストするためにrequestsにテストを書くということのようですが、ここにControllerも集約されるぽいです。認識違っていたらすみません。 以下は弊社サービスVegewelでレストランガイドをやっておりまして、ページを表示、レビューの投稿をテストするコードを書いたものです。 spec/requests/restaurant_spec.rb require 'rails_helper' RSpec.describe RestaurantController, type: :request do describe "GET index" do before do @restaurants = FactoryBot.create(:restaurant) end it "responds successfully" do get "/restaurant" expect(response).to be_success end end describe "GET show on restaurant detail" do it "responds successfully" do get "/restaurant/#{@restaurants.id}" expect(response).to be_success end end end コントローラーのテストの基本の作りはシンプルです。 ルーティングに合わせてget, post, patch, put, delete...を発効して結果をexpectのtoに期待値を書いていくというものです。 require 'rails_helper' RSpec.describe ReviewsController, type: :request do describe "POST review" do let(:review) { FactoryBot.create :review } before do @user = FactoryBot.create(:user) @restaurant = FactoryBot.create(:restaurant) @user.confirm # deviseのメール認証が必要なケースでは必須 sign_in @user end it "responds successfully" do expect{ post '/reviews', params: { user_id: @user.id, restaurant_id: @restaurant.id, review: review } }.to change(Review, :count).by(1) end end end post など更新がかかるアクションにはパラメータを設定しておきます。 Factoryの使い方としては事前評価と遅延評価という考え方があり、もともとAだったものがBになるということをテストするならベースとなるデータを事前評価ということでlet!でデータをcreateなどしておいて、更新の結果で増減があるか確かめるという流れです。 単純にレストランデータを作って該当URL(restaurant/1234みたいな)もので表示するだけなら遅延評価でletでデータを作成するので十分かと思います。 ちなみにフォームは確認画面とかはなく、登録・更新ボタン一発で変更が加わるようにしています。 追加したら当該モデルの数が+1、更新したら変更後の該当カラムが想定通りに変更されていることを確認するようなコードにしています。 リレーションのあるモデルを同時に更新する場合のコントローラーのSpecの書き方 コントローラーに以下のようなparamsの設定があるとします。 app/controllers/menu_controller.rb private def menu_params params.require(:menu).permit(:id, :restaurant_id , :image1, :image2, :image3, :last_update_user_id, :public, menu_type_list: [], menu_translations_attributes:[:id, :menu_id, :menuname, :description, :price, :currency, :lang]) end パラメータに親子関係を設定したものとFactoryの設定にassciationを仕込ませておいた上で以下のようなテストコードを書きます。 spec/requests/menu_spec.rb # 新規作成 describe "POST /menu with authentication" do before do @admin_user = FactoryBot.create(:admin_user) sign_in @admin_user end it "Create new menu for Japanese" do menu_translation_params_ja = { menu_translations_attributes: { "0": FactoryBot.attributes_for(:menu_translation_ja) } } menu_params = FactoryBot.attributes_for(:menu, restaurant_id: restaurant.id).merge(menu_translation_params_ja) expect{ post '/menu', params: { menu: menu_params } }.to change(Menu, :count).by(1) end # 更新 describe "PATCH /menu with authentication" do let(:restaurant) { FactoryBot.create(:restaurant) } let!(:menu) { FactoryBot.create(:menu, restaurant_id: restaurant.id) } before do @admin_user = FactoryBot.create(:admin_user) sign_in @admin_user end it "Update menu for Japanese" do menu_translation_params_ja = { menu_translations_attributes: { "0": FactoryBot.attributes_for(:menu_translation_ja, menuname: "ベジプレート(大豆ミートの鶏から風)", price: 500, menu_id: menu.id) } } menu_params = FactoryBot.attributes_for(:menu, id: menu.id, restaurant_id: restaurant.id).merge(menu_translation_params_ja) patch "/menu/#{menu.id}", params: {menu: menu_params} expect(menu.menu_translations.first.menuname).to eq("ベジプレート(大豆ミートの鶏から風)") expect(menu.menu_translations.first.price).to eq(500) end end 上記同様にフォームは確認画面とかはなく、登録・更新ボタン一発で変更が加わるようにしています。 追加したら当該モデルの数が+1、更新したら変更後の該当カラムが想定通りに変更されていることを確認するようなコードにしています。 以上でコントローラーのテストについて記載しました。 サイトの表側のふるまいをテストするフィーチャーテスト Capybaraを用いてサイト上の動作をシミュレートしたテスト(フィーチャーテスト)をすることが出来ます。 RSpecをやるまではテストのコードを書くというとこっちのイメージでした。Seleniumはこちらのイメージと言えるでしょう。 試しにサイトのトップページからリンクをたどって別ページに飛んで、そこから検索するというスクリプトを書いてみます。 spec/features/projects_spec.rb require 'rails_helper' RSpec.feature "Projects", type: :feature do scenario "Vegewel success scenario in English" do # Vegewel TOP visit '/en' expect(page).to have_content "Vegewel restaurant guide" expect(page).to have_content "Vegewel Style" # Restaurant Page click_link ("Restaurant") expect(page).to have_content "Tasty & Healthy Restaurants" expect(page).to have_content "VESPERA" # Search Restaurant fill_in('q[g][1][restaurant_search]', with: 'Cafe') check('q[g][0][veganmenu_eq]') click_button ("Search") expect(page).to have_content "Cafe*teria HANIWA" end end ちなみに 特定のページを開く:visit リンクを踏む:click_link ボタンを押す:click_button チェックボックスを押す:check テキストボックスに入力:fill_in で制御できます。ボタン押したり、リンククリックしたりの場所の特定はIDなどのセレクタや、ラベルなどが表示されていればその文言で指定することが出来ます。可読性を上げるためにボタンのラベルに指定するとかもアリでしょう。 その他の動作に関してはこちらを参照して、色々シミュレートしてみることが出来るかと思います。 https://qiita.com/morrr/items/0e24251c049180218db4 ご確認ください。 RSpecのテスト $ bundle exec rspec で全ての(モデル、コントローラー、フィーチャー)のテストは実行されます。個別に実行したければファイルを指定すればOKです。テスト通ればSuccessと表示され、NGだとエラー内容が表示されます。 CircleCIにRSpecのテストを組み込み もともとBitbucketの特定ブランチ(Staging、Master)にソースをコミットしたらデプロイする仕組みを構築していました。そこにRSpecのテストを追加してみました。以下.circleci/config.ymlです。 本当は全体はかなり長いのですが、RSpec組み込んだところだけフォーカスしています。 .circleci/config.yml version: 2.1 orbs: ruby: circleci/ruby@0.1.2 jobs: build: working_directory: ~/xxxx parallelism: 1 docker: - image: circleci/build-image:ubuntu-14.04-XXL-upstart-1189-5614f37 steps: - checkout - run: (諸々Deployコマンド) rspec: parallelism: 3 docker: - image: circleci/ruby:2.5.0-node-browsers environment: - BUNDLER_VERSION: 1.16.1 - RAILS_ENV: 'test' - image: circleci/mysql:5.7 environment: - MYSQL_ALLOW_EMPTY_PASSWORD: 'true' - MYSQL_ROOT_HOST: '127.0.0.1' steps: - checkout - restore_cache: key: v1-bundle-{{ checksum "Gemfile.lock" }} - run: name: install dependencies command: | gem install bundler -v 2.0.2 bundle install --jobs=4 --retry=3 --path vendor/bundle - save_cache: key: v1-bundle-{{ checksum "Gemfile.lock" }} paths: - ~/circleci-demo-workflows/vendor/bundle # Database setup - run: mv ./config/database.yml.ci ./config/database.yml - run: name: Databasesetup command: | bundle exec rake db:create bundle exec rake db:schema:load bundle exec rake db:seed_fu # run tests! - run: name: Run rspec command: | mkdir /tmp/test-results TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \ circleci tests split --split-by=timings)" bundle exec rspec \ --format progress \ --format RspecJunitFormatter \ --out /tmp/test-results/rspec.xml \ --format progress \ $TEST_FILES # collect reports - store_test_results: path: /tmp/test-results - store_artifacts: path: /tmp/test-results destination: test-results workflows: version: 2 build-n-deploy: jobs: - rspec: filters: branches: #Spec走らせるブランチを限定(これないとフィーチャーブランチのコミット&Pushで勝手にCircleCIが走ってしまう) only: - staging - master - build: requires: - rspec # rspecしてからDeployするように filters: branches: only: - staging - master こんな感じでJobを分割してRspecとしたところに - テスト環境構築 - Seedデータを投入 - RSpecテスト実行 を実行しています。 workflowsのところに全体的な流れとして、先にRSpecを流してからOKならDeployを実行する流れにしています。 あと、これが最も重要なポイントですが、これを実行するためにCircleCIに課金しました。 1ヶ月$30 です。 複数Job実行にするのは無料プランでは出来なかったからです。この金額で25000ポイントもらえるので、各リソースの利用状況ごとにポイントが減らされる仕組みになっています。 弊社のサービスは1回のデプロイで300〜400ポイントくらい消費するので実行は慎重にやることにしています。ポイントが無くなってきたら、自動的に+$30してポイント追加になります。 課金するとJobの並行実行も可能です。複数の同時デプロイなどに使えるかと思うので使い方はご検討ください。 その他トラブル対応 特定のURLで200が返ってこなくてエラー 例えば以下のようなテストを書いたとして、エラーになって返ってこないケースです。 it "Login returns 200" do get index_path expect(response).to have_http_status "200" end エラーはこんな感じです。 Failure/Error: expect(response.status).to eq 200 expected: 200 got: 302 つまりリダイレクトしているってことですよね?調べたところapplication_controller.rbでdeviseログイン時やhttp->httpsリダイレクトで302をやらかしていました。また、Staging環境ではBasic認証かけたりとか色々やっていて、条件分岐に環境変数を使っていたことからtest環境へのケアが出来ていなかったのも要因でした。 上記エラーになったらapplication_controller.rbのbefore_actionを片っ端からコメントアウトして1個づつつけたり外したりして試してみるのが吉です。 アプリケーションFQDNがwww.example.comとなってしまう?bad URIも発生 テストに以下のようなコードを書いたところbad URIとなり、しかもアプリケーションはwww.example.comというFQDNになってました。 require 'rails_helper' RSpec.describe "Restaurants", type: :request do describe "GET /index" do it "index responds successfully" do get :index expect(response).to be_success end end end 以下エラー URI::InvalidURIError: bad URI(is not URI?): http://www.example.com:80index これは get :index を get '/' とすることでテストが通りました。:indexとすることで文字列にindexを足してしまうようです。また、www.example.comはRailsでActionPackの中にTestのSessionを司る箇所があり、そこで DEFAULT_HOST='www.example.com' と定義されていて、これがRSpec内でURLに指定がなければ自動的に設定されてしまうのが原因でした。ただ、通常のテストではwww.example.comが問題になることがなく「そういうもの」と考えて特に気にしなくて良い模様です。自分の場合はpryで何度止めてもこのFQDNがサーバ名として定義されているので焦りましたが、RSpecのメンターしてくれた先生に聞いたら気にしなくてOKと言われて安堵しました。 CircleCIでMySQLのDockerImage構築中にDBセットアップを始めようとしてエラーになる この図で言うところの Container circleci/mysql5.7 のところが終わっていないのに、DBのセットアップが始まってしまうということがありました。そもそも当初CircleCIは課金していないので基本的に複数Jobを回すようにしておらず、以下のように環境構築、RSpec実行、AWSCLIの設定、ElasticBeanstalkのデプロイまでまとめてやろうとしていました。 その中でいうとimage: circleci/mysql:5.7のところが長い時間かかっているのですが、これと同期をとる方法がわかりませんでした。 version: 2 jobs: build: working_directory: ~/xxxx parallelism: 1 shell: /bin/bash --login environment: CIRCLE_ARTIFACTS: /tmp/circleci-artifacts CIRCLE_TEST_REPORTS: /tmp/circleci-test-results docker: - image: circleci/build-image:ubuntu-14.04-XXL-upstart-1189-5614f37 - image: circleci/ruby:2.5.0-node-browsers environment: - BUNDLER_VERSION: 1.16.1 - RAILS_ENV: 'test' - image: circleci/mysql:5.7 environment: - MYSQL_ALLOW_EMPTY_PASSWORD: 'true' - MYSQL_ROOT_HOST: '127.0.0.1' steps: - run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS - run: working_directory: ~/xxxx command: pip install pip==20.3.4 - run: working_directory: ~/xxxx command: pip install urllib3==1.26 - run: working_directory: ~/xxxx command: pip install awsebcli --upgrade --user - checkout - run: name: install dependencies command: | gem install bundler -v 2.0.2 bundle install --jobs=4 --retry=3 --path vendor/bundle # Database setup - run: mv ./config/database.yml.ci ./config/database.yml - run: name: Databasesetup command: | bundle exec rake db:create bundle exec rake db:schema:load # run tests! - run: name: Run rspec command: | mkdir /tmp/test-results TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \ circleci tests split --split-by=timings)" bundle exec rspec \ --format progress \ --format RspecJunitFormatter \ --out /tmp/test-results/rspec.xml \ --format progress \ $TEST_FILES # collect reports - store_test_results: path: /tmp/test-results - store_artifacts: path: /tmp/test-results destination: test-results - run: name: Deploy command: | if [ "${CIRCLE_BRANCH}" == "master" ]; then echo "Deploy production" eb deploy Vegewel-production else echo "Deploy staging" eb deploy vegewel-staging fi workflows: version: 2 build-n-deploy: jobs: - build: filters: branches: only: - staging - master 最終的には課金して複数JobでRSpecとデプロイという形にしましたが、CircleCIのサポートによればDockerizeを使って待機させることもできるそうです。こちらのドキュメントを参照ください。 https://circleci.com/docs/ja/2.0/databases/#dockerize-%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E4%BE%9D%E5%AD%98%E9%96%A2%E4%BF%82%E3%81%AE%E5%BE%85%E6%A9%9F acts_as_taggable_onを使った箇所のテスト 動的なタグ生成のためにacts_as_taggable_onを使って開発しているところもあると思います。非常に便利で私もよく利用しています。 これのテストですが、自分は以下のようにしました。前提として料理の種類をCuisineとしていて、コードのID:1に対してCuisineTranslationの日本語(lang:ja)に”和食”、英語(lang:en)に”Japanese”のように2レコード設定しているようなデータ構造とお考えください。また、タグ用のテーブルはtags, taggingsの2つです。 app/models/cuisine.rb class Cuisine < ActiveRecord::Base ActsAsTaggableOn::Tagging.table_name = 'taggings' ActsAsTaggableOn::Tag.table_name = 'tags' has_many :cuisine_translations end app/models/cuisine_translation.rb class CuisineTranslation < ActiveRecord::Base belongs_to :cuisine belongs_to :restaurant scope :with_lang , -> { where(lang: I18n.locale )} end これを扱うRestaurantモデルは app/models/restaurant.rb class Restaurant < ActiveRecord::Base ActsAsTaggableOn::Tagging.table_name = 'taggings' ActsAsTaggableOn::Tag.table_name = 'tags' acts_as_taggable_on :stations, :lines, :cuisines こんな感じです。Factoryの設定はTagに対して行います。 spec/factories/tag.rb FactoryBot.define do factory :cuisine_list, class: ActsAsTaggableOn::Tag do name {Cuisine.first.id} end として、RestaurantのFactoryに以下のようにかけば動くはずです。 spec/factories/restaurant.rb FactoryBot.define do factory :restaurant, class: Restaurant do station_list {[FactoryBot.build(:station_list)]} line_list {[FactoryBot.build(:line_list)]} cuisine_list {[FactoryBot.build(:cuisine_list)]} 最後に かなり長い文章になってしまいました。こういうときは記事を分けるのがいいかなと思いつつ、大半は他のQiitaにも書いてそうなことなので付加価値出そうとしてボリューミーになってしまいました。多少なりともお役に立つ内容があれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby 問題② ハッシュの基礎について

はじめに GWも絶賛学習中! 本日もよろしくお願い致します。 問題 複数のユーザ情報をハッシュとして持つ変数user_dateがあります。 ruby.rb user_data = [ {user: {profile: {name: 'Sugano'}}}, {user: {profile: {name: 'Sakamoto'}}}, {user: {profile: {name: 'Okamoto'}}}, ] このデータの名前だけを出力するように記述してください。 出力結果は以下の通りです。 ruby.rb Sugano Sakamoto Okamoto ヒント ・配列とハッシュを組み合わせ ・名前を配列から取り出している(each文) ・2重にされています 解答 ruby.rb user_data.each do |u| puts u[:user][:profile][:name] end 解説 ハッシュから特定のキーを取り出す場合は ハッシュ[取得したい値のキー]で取り出す事が可能です。 今回は2重になっていました。 userの中にprofileがあり、その中にnameがあります。 奥底にある:nameを取り出すためにはハッシュ[取得したい値のキー][取得したい値のキー] で取り出すため、 ハッシュ[:user][::profile][:name] となります。 また配列の中にハッシュがあるため余計なものを取り除かなければなりません。 1度問題のまま取り除いたらどうなるか検証してみます。 user_data = [ {user: {profile: {name: 'Sugano'}}}, {user: {profile: {name: 'Sakamoto'}}}, {user: {profile: {name: 'Okamoto'}}}, ] puts user_data[0] #添字で呼び出す事で配列[0]に入っているデータが取得できる {:user=>{:profile=>{:name=>"Sugano"}}} #問題のように3人のnameだけを取り出す場合は下記のようになる puts user_data[0][:user][:profile][:name] puts user_data[1][:user][:profile][:name] puts user_data[2][:user][:profile][:name] Sugano Sakamoto Okamoto 3人を呼び出すには同じ記述をしなければいけないのでeach文で呼び出すと簡潔に記述する事ができます 変数.each do |ブロック変数| ブロック変数 = [][][] 解答では user_data.each do |u| puts u[:user][:profile][:name] end となります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オリジナルアプリの要件定義(仮)

今日から、オリジナルアプリ(名前は未定)を作成しようと思う。 そのため要件定義をしようと思うので、未経験の人は参考程度にどうぞ。 ちなみに、私も未経験だ! 要件定義の項目 アプリの名前 アプリの概要 制作背景 ペルソナ 必要機能 使用ツールと使用言語 目標制作期間と実際の制作期間 参考アプリケーション(あれば) 私は未経験で、実務での要件定義をしたことはないので、「とりあえずこれくらいは載せた方がいい」と思ったものを選びました。 また、「目標制作期間と実際の制作期間」については、Read.Meに記載する アプリの名前 未定 つのるん(募るん)みたいにしようと考えています。 アプリ概要 参加者を募るためのアプリケーション タイトル・内容・時間などを入力し参加者を募ることができる 制作背景 ポートフォリオを充実させるため。 ペルソナ 場面 社内ツールを想定。主にPCで使用する。 勉強会でも遊びでも。 人物 年齢:20〜40代、幅広く 性別:どちらも 職業:PCをよく使う職業 何の課題を解決できるか LINEなどのチャットツールでも募ることはできるものの、人数制限など細かいことは自分で確認する必要がある。 主催者側の負担が大きいため、その負担を減らすべく作成。 社内のコミュニケーションにも一役買ってもらう。例えば、「野球しませんか」で募る→Aさんが参加する→Aさんは野球に興味があるということがわかる→話が広がる。 必要機能 ユーザー管理機能 ユーザー名・メールアドレス・パスワードを使って新規登録 メールアドレス・パスワードを使ってログイン ユーザー情報編集機能 ユーザーの写真・名前を変更できるようにする グループ作成・削除機能 グループタイトル・人数制限・日時・詳細・タグ・準備するものを入力してグループを作成できる 削除ボタンを押せばグループは削除される グループ参加・辞退機能 グループ詳細画面から参加辞退ができる タグ機能 グループ作成時にタグをつける。「途中参加OK」「リモートで」など。 チェックボックスを使う。 コメント機能 グループに対して質問などをする メール機能→ユーザー間チャット機能に変更 1対1で話し合える場を設ける。 変更の理由 個人的な意見だが「メール」は少しばかり堅い印象がある。 社内ツールを想定しているので、少しばかりは堅苦しくても良いのだが、連絡の取りやすさなどからもチャットの方がよいと思いました。 使用ツールと使用言語 使用ツール Git/GitHub Trello VSCode Ruby on Rails heroku 使用言語 HTML CSS JavaScript MySQL Ruby 目標制作期間 全体の目標:1ヶ月間 --各機能の目標実装期間 ユーザー管理機能:1日 ユーザー情報編集機能:1日 グループ作成・削除機能:3日 グループ参加・辞退機能:3日 タグ機能:3日 --各テストの目標実装期間 ユーザー単体:1日 グループ単体:1日 グループ参加単体:1日 新規登録結合:1日 ログイン結合:1日 ログアウト結合:1日 グループ作成結合:1日 グループ削除結合:1日 グループ参加結合:1日 グループ辞退結合:1日  おしまい 本当はもっと、細かいところまで決めていくんだと思います。が、未経験がそんな細かいところまで気にしていたら、アプリ作り出す前にモチベーションが消えます。 つまりは、目指すところを重要視しましょう。要件定義とか詳細設計とかの仕事につきたいなら、その辺を全力で。 もしくは、勉強したいところを全力で。 ※この要件定義はいずれ編集されます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Clash of Codeへの誘い

0.はじめに こんにちは、中学3年生のsoraieです。 競技プログラミングをしていてAtCoder青だったりします。 記事で分からないことや不備などがあればTwitterで教えてください。 この記事では CodinGame というサイトの Clash of Code というコンテンツの楽しさを知ってもらい、皆さんに「始めたい」と思ってもらうのが目的です。 きっかけ 上でも書いた CodinGame というサイトの Clash of Code というコンテンツで日本一位をとりました。 今は結構抜かされて転落してしまっていますが、一瞬でも1位になることができてとても嬉しかったので Clash of Code の楽しさを共有したいと思い、この記事を書くことにしました。 目次 章 タイトル 0 はじめに 1 CodinGameとは? 2 Clash of Codeとは? 3 Clash of Code を始めよう! 4 おわりに 1. CodinGameとは? CodinGame は AtCoder Codeforces などのサイトのように出された問題を解くプログラムを作って他の人と競ったり、問題を解きながら技術を向上させることができるサイトです。たまに大規模な大会も開かれています。 問題はいくつかの分野に分かれていて具体的には AIを作って他人のものと競う BOT programing アルゴリズムを使って問題を解く ALGORITHMS 簡単な問題をルールに沿って解いて他人と競う Clash of Code などがあります。 BOT programing BOT programing はこの中で一番人気のあるコンテンツだと思います。自分の作ったAIが他の人と競い合うのを見るのはとても面白いです。大会が開かれるとき、問題は BOT programing であることが多い気がします。 とても楽しいのですがかなり奥が深く、ここの記事に書こうとすると文が長くなりすぎてしまうことや、僕自身あまり詳しくないこともあるので、ここでは他の方の記事へのリンクを貼っておきます。 【CodinGame】ブラウザでコーディングの基礎からトレーニングできるサイト (疑似ゲーム開発環境を使って学べます。解答は25種類のプログラミング言語から選択して記述可能!) CodinGame はBOT(AIプログラム)でバトルするのが正しい楽しみ方かもしれません ALGORITHMS ALGORITHMS の問題の難易度は AtCoder Codeforces などの競技プログラミングサイトと比べると非常に簡単になっています。 競技プログラミングを普段している人はつまらないと感じてしまうかもしれません。 しかし、問題によってはテストケースを実行するとそれに応じて左上の画面が動くものがあり、これは見ていて結構面白いです。 Clash of Code これから説明します。出された問題を解くところは AtCoder Codeforces などと似ていますが、ルールが一風変わっていて普段競技プログラミングをしている人でも楽しめるようになっていると思います。 また、 CodinGame の中には世界中の人と会話できるチャットもあり、分からないことがあれば質問をすることができます。結構優しい人が多いです。(体験談) 2. Clash of Codeとは? 上でも書いた通り出された問題をルールにしたがって解くものです。 毎回のゲームの結果(順位)に応じて自分のレートが上下します。 では Clash of Code と AtCoder Codeforces の異なる点は何でしょうか? それをいくつか説明します。 常時開催されている Clash of Code は 常時開催 されています。参加者が8人集まるとゲームが開始されます。いつでも好きな時に参加することができます。 モードが複数ある 出された問題に対してそれを解くプログラムを時間内に書くということは同じなのですが、それらに加えて以下の3つのモードのうちどれかが選択されます。 選択されたモードに応じてコードを書きます。 fastest mode できるだけ速く問題を解く reverse mode 問題文が 与えられない のでテストケースだけを見て問題をできるだけ速く解く shortest mode できるだけ短いコードで問題を解く いわゆるコードゴルフ fastest mode は AtCoder Codeforces と似ていますね。 しかし shortest mode,reverse mode はあまり馴染みがないのではないでしょうか? 時間が短い 一般的なプログラミングコンテストは問題 3~9 問ほどを 1~2 時間で解くというものが多いと思いますが、Clash of Code は問題1問を15分で解きます。15分の間にコードを書いたり、shortest mode の場合にはもっとコードを短くできないかを検討したりします。 時間が短いので気軽に参加することができます。 作問がかなり気軽にできる これは異なる点といっていいのか分かりませんが、 Clash of Code では問題をかなり気軽に作ることができます。 問題を作って3人の人に認めてもらえばすぐにそれが出題されます。私は自分で作った問題がその日のうちに出題されるという経験をしたことがあります。 3. Clash of Code を始めよう! ある程度他のサイトとの違いが分かったと思うので、ここからは Clash of Code を始めるに当たってのサポートをしたいと思います。 3-1. ゲームの流れ ゲームの流れとしては、 Clash of Codeのページに行き、Join a 5min coding battle! というところをクリック 8人集まるかホストがスタートするとゲームが始まります 始まったら問題とモードを確認する 問題を解いて手元でテストケースを確認する 提出して自分の結果を確認 という感じです。 問題画面です。(一応問題文は隠しています) 左上にモードと残り時間 その下に問題文 左下に他の人の状況 右にIDE(コードを書く場所) コードを書き終わったら右下の PLAY ALL TESTCASES でテスト という感じです。 今回のゲームのモードは fastest mode なので他の人よりも速くコードを書かなければいけません。 「これで大丈夫!」というコードが書けて、テストケースも全て試したら SUBMIT ボタンを押して結果を確認しましょう。 結果画面はこんな感じです。 負けてますが何か!?!?!?!? 結果画面では他の人の結果、もしその人が許可しているならばその人のコードを見ることができます。 今回私は8人中3位で多分これはレートが下がってしまいます。 しかし、レートが下がったところで一回のゲーム時間は高々15分なのでまた別のゲームに参加してレートを戻すことができるのですのです。 ですので、ここでレートが下がったところで痛くもかゆくもありません。(嘘) 3-2. 実践的なこと さて、ここまでの説明で Clash of Code がどのようなものか、ゲームの流れはどんな感じか、ということがなんとなく分かったかと思います。 ここからは Clash of Code で上位に入っていくためにしたこと、各モードにおける基本的な戦略を書いておきます。 もちろん、以下に書いている戦略に無理に従う必要はありません。以下に書いてあることはあくまで私がしたことです。 新しく言語を学んだ これは Clash of Code のために学んだという訳ではないのですが、Python,Ruby を少し書けるようになりました。 普段 AtCoder のコンテストでは 多くの人が C++ でコードを書いています。しかし、C++ では Clash of Code の上位に入っていくのは少し難しいかもしれません。 これは、C++ ではコードを書く時間が他の言語と比べて長くなってしまう、コードが長くなってしまうためです。 shortest mode ではこの影響は顕著に現れます。また、このほかのモードでもかなりの影響があります。 コードを書くときに C++ 以外の言語のほうが多くのメソッドがあり、有利だと実感することが多いです。例えば、入力された n の各桁の和を求めたいときに C++ だと #include<bits/stdc++.h> using namespace std; int main(){ int n; cin >> n; int sum_of_digits = 0; while(n){ sum_of_digits += n % 10; n /= 10; } cout << sum_of_digits << endl; } と書かなければいけませんが、 ruby では p gets.to_i.digits.sum と書くだけでいいです。 Clash of Code は問題があまりにも単純なので、C++ で速くコードを書くことに自信がある人も勉強してでも別の言語をほうが順位が伸びやすいかもしれません。 強い言語 Clash of Code で強い言語をいくつか書いておきます。 Python Ruby Perl Javascript これらの言語が書けるととても強いです。 CodinGame では Javascript の入出力をサポートをする readline 関数と print 関数があり、これがかなり強いです。 各モードの戦略 fastest mode まず問題文を読まずにテストケースを少し見てみます。テストケースからどんな問題か分かった場合はコードを書きましょう。テストケースを見ても分からない場合に問題文を見ればいいです。 問題文を読むときは Output の部分を見ると出力しなければいけないものが書いてある場合があります。 このモードでは Python で提出することが多いです。 たまに正規表現が使いたい時に Ruby で提出します。 shortest mode 15分フルに使えるのでとりあえず問題を読みながらどうすれば短いコードを書けるかを考えます。 自分の知ってるメソッドを頭に思い浮かべて書いてみて色々な言語で書いたり他の解法で書いたりします。 このモードでは Python,Ruby で提出することが多いです。たまに Bash を使うこともあります。 Perl を使う人も多少いるようです。 言語名 コードゴルフ で調べるとその言語でコードを短く書くテクニックが出てきますので調べてみるとよいでしょう。 また、近いうちに同級生の @souta_1326 君が shortest mode の攻略記事を出すらしく、その記事が出たらこの下にリンクを貼るのでそれも見てみてください。 reverse mode このモードが一番好きです。何故なら問題文がないので英語を読む必要がないからです!!(英弱ですみませんでした) テストケースを見て解法を考えます。これに関してては勘だけが頼りで、結構ギャンブル要素が強いです。 このモードでは Python で提出することが多いです。 総合的なこと 説明していませんでしたが、これらのモードではいずれも正答数が第一です。速く解こうと意識しすぎてミスをしてしまっては良い順位は出ず、本末転倒です。 これを防ぐためにテストケースは絶対に全部試しましょう。試さないと大変なことになります。(自戒) また、 shortest mode で短く書こうと意識しすぎて結局コードが書き終わらないということもあってはなりません。 4. おわりに Clash of Code はとても単純で面白いゲームです。ごくたまにヤバい問題(語彙力)が来たり、途中で回線が切れたり(これは自分の責任です)するとキレますがとても楽しいです。 普段競技プログラミングをしている人なら簡単にレートが上がると思いますので是非一度息抜き感覚で遊んでみてはいかがでしょうか? それではまた別の記事で会いましょう!良い競技プログラミングライフを!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】フォロー機能実装(コントローラー、view編)

完成GIF フォローする、フォローを外すを表示 フォロー中と、フォロワーの数を表示 非同期通信で実装 前回の記事はこちら 1.メソッド定義 followとunfollowのメソッドを、userモデルに定義する。 user.rb class User < ApplicationRecord def follow(user_id) relationships.create(followed_id: user_id) //フォローするボタンのメソッド end def unfollow(user_id) relationships.find_by(followed_id: user_id).destroy //フォローを外すボタンのメソッド end def following?(user) followings.include?(user) //フォローしているかどうかのメソッド end end find_byでは、与えられた条件にマッチするレコードのうち最初のレコードだけを返します。 include?は対象の配列に引数が含まれていればtrueを返し、含まれていなければfalseを返します。 2.controller編集 app/controllers/rerationships_controller.rb class RelationshipsController < ApplicationController before_action :authenticate_user! def create //フォローする current_user.follow(params[:user_id]) end def destroy //フォローを外す current_user.unfollow(params[:user_id]) end def followings //フォローした人一覧表示 user = User.find(params[:user_id]) @users = user.followings end def followers //フォロワー一覧表示 user = User.find(params[:user_id]) @users = user.followers end end 非同期通信で行うので、redirect_toは記述しません。 3.Viewの記述 viewは以下のようにして、countもつけている。 後述する非同期通信にてcountを変化させる。 app/views/relationships/_relation_count.html.erb <div class="d-inline-block mr-1"> <%= link_to "#{ user.followings.count }フォロー中", user_followings_path(user), class:"text-dark" %> </div> <div class="d-inline-block ml-1"> <%= link_to "#{ user.followers.count}フォロワー", user_followers_path(user), class:"text-dark" %> </div> クリックするとフォロワー一覧、フォロー一覧へ画面遷移する。 relationships/followers.html.erb <h5><%= full_name(@user)%>さんのフォロワー</h5> <div class="row"> <% if @users.exists? %> <%= render 'users/index', users: @users %> <% else %> <p>フォロワーはいません</p> <% end %> </div> @users.exists?では、フォロー、フォロワーがいるかいないかを判断します。レコードがあればtrueを返し、レコードがなければfalseを返します。 フォローボタンの記述です。 app/views/relationships/_relationships/btn.html.erb <% unless current_user == user %> //current_userがuserでなければtrue <% if current_user.following?(user) %>//current_userがfollowしていれば <%= link_to "フォローを外す", user_relationships_path(user.id), method: :delete, remote: true, class: "btn btn-primary" %> <% else %>//followしていなかったら <%= link_to "フォローする", user_relationships_path(user.id), method: :post, remote: true, class: "btn btn-success" %> <% end %> <% end %> 今回はUserの詳細画面にフォローボタンとフォローカウントの表示をさせます。 views/users/show.html.erb <div class="row"> <div class="col-sm-12 col-lg-5 text-center"> <div> <div><%= attachment_image_tag(@user, :profile_image,size:"300x350",fallback: "no_image.png")%></div> <h5 class="mt-3"><%= full_name(@user)%></h5> <div id="follow-count"> <%= render "relationships/relation_count", user: @user%> </div> <div id="follow-btn" class="mt-2"> <%= render "relationships/relation_btn", user: @user%> </div> </div> </div> 4.js.erbファイルの作成・記述 create.js.erbとdestroy.js.erbを作成します。 views/relationships/create.js.erb $("#follow-count").html("<%= j(render 'relationships/relation_count', user: @user ) %>"); $("#follow-btn").html("<%= j(render 'relationships/relation_btn', user: @user ) %>"); 1行目 follow-countに対してのjs内容 2行目 follow-btnに対してのjs内容
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyのNull演算子や範囲演算子についてまとめた

Railsを勉強していて、JavaやPythonでは見かけない演算子に出会したため、 見かけないものにフォーカスしてRubyの演算子をまとめました。 論理演算子系 Null演算子 item = a&.a.method Ruby2.3.0から使用可能になった演算子です。 「ぼっち演算子」とも呼ばれます。 変数aがnil(null)であるとき、a.methodを通常通り実行しようとするとnilなものを呼び出そうとしてエラーになります。 このエラーを避けるためにこの演算子を使います。 挙動としては、aがnilであればnilが返され、aがnilでなければa.methodが実行されその結果が返されます。 範囲演算子 (1..5).to_a #=> [1, 2, 3, 4, 5] (1...5).to_a #=> [1, 2, 3, 4] Rangeオブジェクトを省略する形で実装されているのが範囲演算子です。 Rangeオブジェクトでは、Range.new(1, 5)とインスタンスを生成する形で利用しなければなりませんが、範囲演算子では1..5で済みます。 「..」「...」の2種があり、前者は終点を含み、後者は終点を含みません。 また、abcなどの文字列でも、次の値を持つシリアルなものであれば範囲演算子を利用可能です。 以上、JavaやPythonでは見かけなかった演算子についてまとめました。 ググったところCやPerlなど様々な言語で実装されているようです。私がたまたま知らなかっただけでした・・・。 また何かあれば追記しようと思います。 参考 たのしいRuby第5版 https://www.sbcr.jp/product/4797386295/ 現場で使えるRubyonRails5速習実践ガイド https://book.mynavi.jp/ec/products/detail/id=93905 Wikipedia Null条件演算子 https://ja.wikipedia.org/wiki/Null%E6%9D%A1%E4%BB%B6%E6%BC%94%E7%AE%97%E5%AD%90
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] Rspecでテストコードの実装④

はじめに テストコードの実装③をみていない方はこちらからご覧ください。 結合テスト 結合テストとは、ユーザの一連の操作を再現して行うテストのことです。今まで行ってきた単体テストは、機能ごとでしたが、結合テストでは一連の流れを一気に行います(例:投稿機能であれば、ログイン→入力→投稿→表示の確認)。結合テストコードを実行するためには、System Specという技術を使用します。 System Spec(参考) System Specを記述するためには、Capybara(カピバラ)というGemを用います。これはすでにデフォルトでRuby on Railsに搭載されています。 group :test do # Adds support for Capybara system testing and selenium driver gem 'capybara', '>= 2.15' gem 'selenium-webdriver' # Easy installation and use of web drivers to run system tests with browsers gem 'webdrivers' end テストコードの具体的な実装 まずはいいつものようにファイルを生成していきましょう rails g rspec:system users 生成されたファイルに以下を記述していきます system/users_spec.rb it '正しい情報を入力すればユーザー新規登録ができてトップページに移動する' do visit root_path expect(page).to have_content('新規登録') visit new_user_registration_path fill_in 'Nickname', with: @user.nickname fill_in 'Email', with: @user.email fill_in 'Password', with: @user.password fill_in 'Password confirmation', with: @user.password_confirmation expect{ find('input[name="commit"]').click }.to change { User.count }.by(1) expect(current_path).to eq(root_path) expect( find('.user_nav').find('span').hover ).to have_content('ログアウト') expect(page).to have_no_content('新規登録') expect(page).to have_no_content('ログイン') end 一個づつ見ていきましょう。 visit 〇〇_path 〇〇のページへ遷移することを実現します。前回にやったgetと似ていますが、getは単にリクエストを送るのみです page visitで訪れた先のページの見える分だけの情報が格納されています。現在見ているページと考えて問題ないです have_content visitで訪れたpageの中に、文字列があるかどうかを判断するマッチャです。 expect(page).to have_content('X')とすれば、現在いるページにXがあるかどうか判断します have_no_content have_contentの真逆です。 fill_in fill_in 'フォームの名前', with: '入力する文字列'とすることで、任意の文字列をフォームに入力できます。フォーム名は検証ツールで調べてみましょう find().click find('クリックしたい要素').clickと記述することで、指定の要素をクリックができます。 ちなみに、click_on "文字列"としても同じ効果を得られますが、同じ文字列が2つ以上あると使えません change expect{ 何かしらの動作 }.to change { モデル名.count }.by(1)と記述することによって、モデルのレコードの数がいくつ変動するのかを確認できます。このとき、カッコが{}となることに注意です current_path 現在いるパスを示します hover find('ブラウザ上の要素').hoverとすると、その要素にカーソルを合わせることが可能です。hoverしないと表示されないものもありますので、こうした表記があります。 以上のマッチャ等をしっかり理解した上で、再度テストコードを見てみるとその意味はかなりわかりやすくなるのではないでしょうか 解説をコメントアウトにいれてありますのでご覧ください。 system/users_spec.rb it '正しい情報を入力すればユーザー新規登録ができてトップページに移動する' do # トップページに移動する visit root_path # トップページにサインアップページへ遷移するボタンがあることを確認する expect(page).to have_content('新規登録') # 新規登録ページへ移動する visit new_user_registration_path # ユーザー情報を入力する fill_in 'Nickname', with: @user.nickname fill_in 'Email', with: @user.email fill_in 'Password', with: @user.password fill_in 'Password confirmation', with: @user.password_confirmation # サインアップ(登録)ボタンを押すとユーザーモデルのカウントが1上がることを確認する expect{ find('input[name="commit"]').click }.to change { User.count }.by(1) # 現在のページがトップページへ遷移したことを確認する expect(current_path).to eq(root_path) # ある要素にカーソルを合わせるとログアウトボタンが表示されることを確認する expect( find('.user_nav').find('span').hover ).to have_content('ログアウト') # サインアップページへ遷移するボタンや、ログインページへ遷移するボタンが表示されていないことを確認する expect(page).to have_no_content('新規登録') expect(page).to have_no_content('ログイン') end (参考)その他よく使う表記まとめ have_selector 指定したセレクタが存在するかどうかを判断するマッチャ。have_selector ".クラス名または#id名"という形で記述できます。 have_link expect('要素').to have_link 'ボタンの文字列', href: 'リンク先のパス'と記述することで、要素の中に当てはまるリンクがあることを確認する。 なお、have_content同様に、have_no_linkもある all all('クラス名')でpageに存在する同名のクラスを持つ要素をまとめて取得できます。all('クラス名')[0]のように添字を加えることで指定版目のものを取得できます find_link().click 個人的にはあまり使いませんが、、、a要素で表示されているリンクをクリックするために用います。find().clickと似ていますが、find_link().clickはa要素のみに対して用いることができます。 おわりに お疲れ様でした。 今回は結合テストについて、簡単にまとめてみました。他にもいろいろなパターンがあるかと思いますので、ぜひ考えてみてください!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Railsインストール時のエラー

概要 Ruby on Railsをインストールする際に下記のようなエラーが発生しインストールが出来ませんでした。 gem install rails実行時のエラー内容 console > gem install rails Temporarily enhancing PATH for MSYS/MINGW... Building native extensions. This could take a while... ERROR: Error installing rails: ERROR: Failed to build gem native extension. current directory: C:/Ruby30-x64/lib/ruby/gems/3.0.0/gems/websocket-driver-0.7.3/ext/websocket-driver C:/Ruby30-x64/bin/ruby.exe -I C:/Ruby30-x64/lib/ruby/site_ruby/3.0.0 -r ./siteconf20210504-17032-oj9pk4.rb extconf.rb creating Makefile current directory: C:/Ruby30-x64/lib/ruby/gems/3.0.0/gems/websocket-driver-0.7.3/ext/websocket-driver make DESTDIR\= clean C:/Ruby30-x64/lib/ruby/gems/3.0.0/gems/make-0.3.1/bin/make:4:in `<top (required)>': undefined local variable or method `make' for main:Object (NameError) from C:/Ruby30-x64/bin/make:23:in `load' from C:/Ruby30-x64/bin/make:23:in `<main>' current directory: C:/Ruby30-x64/lib/ruby/gems/3.0.0/gems/websocket-driver-0.7.3/ext/websocket-driver make DESTDIR\= C:/Ruby30-x64/lib/ruby/gems/3.0.0/gems/make-0.3.1/bin/make:4:in `<top (required)>': undefined local variable or method `make' for main:Object (NameError) from C:/Ruby30-x64/bin/make:23:in `load' from C:/Ruby30-x64/bin/make:23:in `<main>' make failed, exit code 1 対処方法 結論から言うと、"gem uninstall make" を実行してmakeをアンインストールすれば解決しました。 VSCodeの拡張機能でRuby on Railsをインストールしていたことが原因かもしれません。 makeをアンインストール後に、再度Railsのインストールを実行したところ、無事インストールが完了しました。 console > gem uninstall make Remove executables: make in addition to the gem? [Yn] y Removing make Successfully uninstalled make-0.3.1 > gem install rails Temporarily enhancing PATH for MSYS/MINGW... Building native extensions. This could take a while... Successfully installed websocket-driver-0.7.3 Building native extensions. This could take a while... Successfully installed nio4r-2.5.7 Successfully installed actioncable-6.1.3.1 Successfully installed rails-6.1.3.1 Parsing documentation for websocket-driver-0.7.3 Installing ri documentation for websocket-driver-0.7.3 Parsing documentation for nio4r-2.5.7 Installing ri documentation for nio4r-2.5.7 Parsing documentation for actioncable-6.1.3.1 Installing ri documentation for actioncable-6.1.3.1 Parsing documentation for rails-6.1.3.1 Installing ri documentation for rails-6.1.3.1 Done installing documentation for websocket-driver, nio4r, actioncable, rails after 1 seconds 4 gems installed 参考URL
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ルーティングを理解する

ルーティングの役割 ルーティングはコントローラーを繋ぐ役割を持っていて、以下の順番で処理を行う。 ①URLを検索(リクエスト) ②ルーティング ③コントローラー ④ビュー ⑤レスポンスを返す ルーティング理解する 公式 get "URL" => "コントローラー名#アクション名" 実践 ブラウザから、「localhost:3000/home/top」というURLが送信された時 Rails.application.routes.draw do get "home/top" => "home#top" end 上記のように記述すると、homeコントローラーのtopアクションで処理されるようになる。 ⚠︎ "home/top"はURLを表している ⚠︎ "home#top"は"コントローラー名#アクション名" もし、上記のようなルーティングの記述がしてあるのに、「localhost:3000/home/hello」を送信すると、 ルーティングエラーが起きる。これは、URL(home/hello)に対応するルーティングが存在しないためである。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Herokuの本番環境でseeds.rbの内容が反映されなった件

目次 1.前提条件、やりたかったこと 2.エラーメッセージ 3.原因・対策 4.まとめ 1.前提条件、やりたかったこと 開発環境 Ruby 2.6.5 Rails 6.0 Heroku/MySQL/Visual Studio Code railsでアプリ開発をしました。Herokuにデプロイしたところseedファイルの内容が反映されず表示されませんでした。(ローカル環境では表示されています) 修正前 seeds.rb Category.create(name:'野菜') Category.create(name:'果物') Category.create(name:'きのこ類') Product.create(name:'ねぎ', shelf_life:'7', frozen_life:'60', category_id:'1', info:'') Product.create(name:'長ネギ', shelf_life:'7', frozen_life:'60', category_id:'1', info:'') Product.create(name:'玉ねぎ', shelf_life:'60', frozen_life:'30', category_id:'1', info:'') Product.create(name:'じゃがいも', shelf_life:'60', frozen_life:'30', category_id:'1', info:'常温の場合は3~4ヶ月') Product.create(name:'さつまいも', shelf_life:'60', frozen_life:'60', category_id:'1', info:'') #以下省略 2.エラーメッセージ 以下はターミナルのログです。Categoryは読みに行っているのですが、何故かProductではなく再びCategoryを読みに行っているように見えます。 Running rails db:seed on ⬢ fridge-app-35190... up, run.7217 (Free) D, [2021-04-24T02:21:50.167441 #4] DEBUG -- : (2.4ms) SET NAMES utf8, @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483 D, [2021-04-24T02:21:50.259743 #4] DEBUG -- : (16.5ms) SELECT `schema_migrations`.`version` FROM `schema_migrations` ORDER BY `schema_migrations`.`version` ASC D, [2021-04-24T02:21:50.329090 #4] DEBUG -- : (2.2ms) SET NAMES utf8, @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483 D, [2021-04-24T02:21:50.367828 #4] DEBUG -- : (4.8ms) BEGIN D, [2021-04-24T02:21:50.370664 #4] DEBUG -- : Category Create (2.5ms) INSERT INTO `categories` (`name`, `created_at`, `updated_at`) VALUES ('野菜', '2021-04-24 02:21:50.361412', '2021-04-24 02:21:50.361412') D, [2021-04-24T02:21:50.379870 #4] DEBUG -- : (8.6ms) COMMIT D, [2021-04-24T02:21:50.383787 #4] DEBUG -- : (2.3ms) BEGIN D, [2021-04-24T02:21:50.387830 #4] DEBUG -- : Category Create (3.8ms) INSERT INTO `categories` (`name`, `created_at`, `updated_at`) VALUES ('果物', '2021-04-24 02:21:50.380587', '2021-04-24 02:21:50.380587') D, [2021-04-24T02:21:50.403017 #4] DEBUG -- : (14.0ms) COMMIT D, [2021-04-24T02:21:50.410235 #4] DEBUG -- : (3.2ms) BEGIN D, [2021-04-24T02:21:50.413133 #4] DEBUG -- : Category Create (2.6ms) INSERT INTO `categories` (`name`, `created_at`, `updated_at`) VALUES ('きのこ類', '2021-04-24 02:21:50.403760', '2021-04-24 02:21:50.403760') D, [2021-04-24T02:21:50.424287 #4] DEBUG -- : (10.6ms) COMMIT D, [2021-04-24T02:21:50.482544 #4] DEBUG -- : (2.4ms) BEGIN D, [2021-04-24T02:21:50.486608 #4] DEBUG -- : Category Load (3.6ms) SELECT `categories`.* FROM `categories` WHERE `categories`.`id` = 1 LIMIT 1 D, [2021-04-24T02:21:51.409149 #4] DEBUG -- : (2.3ms) ROLLBACK D, [2021-04-24T02:21:51.412205 #4] DEBUG -- : (1.9ms) BEGIN D, [2021-04-24T02:21:51.414466 #4] DEBUG -- : Category Load (2.1ms) SELECT `categories`.* FROM `categories` WHERE `categories`.`id` = 1 LIMIT 1 D, [2021-04-24T02:21:51.417633 #4] DEBUG -- : (1.9ms) ROLLBACK D, [2021-04-24T02:21:51.420801 #4] DEBUG -- : (1.9ms) BEGIN #以下省略 3.原因・対策 HerokuのDBを確認した所、Categoryのidが4.14.24となっていました。 idを1.2.3と連番で付与したところ解決しました。 修正後 seeds.rb Category.create(id:'1', name:'野菜') Category.create(id:'2', name:'果物') Category.create(id:'3', name:'きのこ類') Product.create(name:'ねぎ', shelf_life:'7', frozen_life:'60', category_id:'1', info:'') Product.create(name:'長ネギ', shelf_life:'7', frozen_life:'60', category_id:'1', info:'') Product.create(name:'玉ねぎ', shelf_life:'60', frozen_life:'30', category_id:'1', info:'') Product.create(name:'じゃがいも', shelf_life:'60', frozen_life:'30', category_id:'1', info:'常温の場合は3~4ヶ月') Product.create(name:'さつまいも', shelf_life:'60', frozen_life:'60', category_id:'1', info:'') #以下省略 4.まとめ 詳しくはわかりませんが、Herokuの仕様上idを連番で付与してあげないときちんと読み込みが実行されないようです。原因が特定できず解決までに時間を要しましたので、共有したいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】お問い合わせ機能を実装する方法

テーマ RubyonRailsでお問い合わせ機能を実装する方法 はじめに お問い合わせ内容をGmailアドレスで受け取る方法を記載しています ローカル環境でお問い合わせ機能を実装するための記載になります(後日、本番環境追記します) 環境変数の設定とGoogleセキュリティ設定が必要が必要です。こちらに関しては、URLを参照願います。 環境 Ruby 2.6.5 Rails 6.0.3.6 Visual Studio Code ターミナル ローカル環境 実装の流れを把握する モデル作成 コントローラー作成 ルーティング viewファイル作成 ActionMailer作成 メール本文のレイアウト作成後、previewする Gmailアドレスへ送信するための設定 実際に送信してみる モデル作成 以下を順番に実行します % rails g model inquiry name:string message:string % rails db:migrate コントローラー作成 以下を実行します % rails g controller inquiries 色々なファイルが生成されたと思いますが「inquiries_controller.rb」に以下を記述します。 inquiries_controller.rb class InquiriesController < ApplicationController before_action :authenticate_user!, only: [:new, :create] def new @inquiry = Inquiry.new end def create @inquiry = Inquiry.new(inquiry_params) if @inquiry.save #保存処理 InquiryMailer.send_mail(@inquiry).deliver_now #メール送信処理 redirect_to root_path else render :new end end private def inquiry_params params.require(:inquiry).permit(:name, :message) end end ルーティング 以下を記述します routes.rb Rails.application.routes.draw do devise_for :users root to: "tweets#index" resources :inquiries, only: [:new, :create] #この行を追記する resources :users, only: :show resources :tweets, only: [:new, :create, :edit, :update, :destroy] do member do get 'search' end end end viewファイルを作成します app/views/inquiries/へnew.html.erbを作成します app/views/inquiries/new.html.erb <h2 class="page-heading">お問い合わせ内容</h2> <%= form_with model: @inquiry, local: true do |f| %> <div class="field"> <%= f.label :name, "ニックネーム"%><br /> <%= f.text_field :name%> </div> <div class="field"> <%= f.label :message, "お問い合わせ内容"%><br /> <%= f.text_area :message, class: :form__text %> </div> <div class ="actions"> <%= f.submit "送信", class: :form__btn %> </div> <%end%> ※class名とcssはご自身で設定お願いします。 ActionMailerを作成 以下を実行します % rails g mailer inquiry 色々なファイルが生成されたかと思います。 次に、「app/mailers/inquiry_mailer.rb」を編集します。 app/mailers/inquiry_mailer.rb class InquiryMailer < ApplicationMailer def send_mail(inquiry) @inquiry = inquiry mail to: system@example.com, subject: 'お問い合わせ通知' end end メール本文のレイアウトを作成後、previewする メール本文のレイアウトを作成するためは、以下の命名規則にしたがってerbファイルを作成します。 app/views/{メイラー名}_mailer/{メイラークラスのメソッド名}.text.erb よって、「app/views/inquiry_mailer/send_mail.text.erb」を作成しましょう。 send_mail.text.erb <%= @inquiry.name %> 様 から問い合わせがありました。 ・お問い合わせ内容 <%= @inquiry.message %> 受信するとこんな感じで表示される とはいえ、現段階で受信することはできないので、preview機能を使って確認してみる。 以下のファイルに記述していく。 spec/mailers/previews/inquiry_mailer_preview.rb # Preview all emails at http://localhost:3000/rails/mailers/inquiry class InquiryPreview < ActionMailer::Preview def inquiry @inquiry = Inquiry.new(name: "何食べる太郎", message: "問い合わせテストメッセージ") InquiryMailer.send_mail(@inquiry) end end 実際にpreviewしてみます % rails s サーバー起動後、inquiry_mailer_preview.rbに元から記載されているURLをコピペしアクセスします。 http://localhost:3000/rails/mailers/inquiry このように表示されるかと思います inquiryをクリックします。 以上で、無事メール本文を作成し、previewで確認することができました。 Gmailアドレスへ送信するための設定 メールサーバーを設定していきます 以下のファイル、こちらを追記しましょう config/environments/development.rb #メールサーバーの設定 config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: 'smtp.gmail.com', port: 587, domain: 'gmail.com', user_name: 'お問い合わせを受信したいgmailメールアドレス', password: 'gmailアドレスに対するパスワード', authentication: 'plain', enable_starttls_auto: true } そして、以下を修正します app/mailers/inquiry_mailer.rb class InquiryMailer < ApplicationMailer def send_mail(inquiry) @inquiry = inquiry mail to: 'お問い合わせを受信したいgmailメールアドレス', subject: 'お問い合わせ通知' end end 実際に送信してみる http://localhost:3000/inquiries/newこちらにアクセスして実際にフォームを送信してみましょう。 実際に送信できたかと思います…が、 Googleアカウントにアクセスすると警告文が表示されると思います。 要因としてはセキュリティの甘さが考えられます。 対策として 自分のGmailアドレスを環境変数に設定すること Googleアカウントのセキュリティを強化すること こちらの記事を参考にさせていただいた所、問題解決できましたので試してみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【rails6】モデル単体テストエラー ActiveRecord::StatementInvalid: Mysql2::Error: MySQL client is not connected

こんにちは!プログラミング初学者です! 今日はアプリケーションのモデル単体テストコードでのエラー解決について書いていきます エラー内容 ActiveRecord::StatementInvalid: Mysql2::Error: MySQL client is not connected エラー画面は以下の通りです % bundle exec rspec spec/models/comment_spec.rb Comment 記事へのコメント機能 コメントが投稿できるとき フォームに正しく入力すれば投稿できる コメントが投稿できないとき コメントが空欄では投稿できない (FAILED - 1) ユーザーid(user_id)が空では投稿できない (FAILED - 2) 記事id(article_id)が空では投稿できない (FAILED - 3) Failures: 1) Comment 記事へのコメント機能 コメントが投稿できないとき コメントが空欄では投稿できない Failure/Error: _query(sql, @query_options.merge(options)) ActiveRecord::StatementInvalid: Mysql2::Error::ConnectionError: Lost connection to MySQL server during query # 見にくくなるので省略 # # Showing full backtrace because every line was filtered out. # See docs for RSpec::Configuration#backtrace_exclusion_patterns and # RSpec::Configuration#backtrace_inclusion_patterns for more information. # ------------------ # --- Caused by: --- # Mysql2::Error::ConnectionError: # Lost connection to MySQL server during query # /Users/私の名前/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/mysql2-0.5.3/lib/mysql2/client.rb:131:in `_query' 調べていると、原因は下記2つに絞られる様です ・MySQL serverの停止、再起動が原因で今まで使っていた network socket が使えなくなった ・SQL が read_timeout 以内に実行が終わらなかった 参照元 https://abicky.net/2017/09/17/014241/ ・・・ありがとうございます!! 私の実行ファイルは行数も少ないので、全てに binding.pry を仕込んでみようと思います spec/models/comment_spec.rb RSpec.describe Comment, type: :model do before do @user = FactoryBot.create(:user) @article = FactoryBot.create(:article) @comment_user = FactoryBot.create(:user) @comment = FactoryBot.build(:comment, user_id: @comment_user.id, article_id: @article.id) end describe '記事へのコメント機能' do context 'コメントが投稿できるとき' do it 'フォームに正しく入力すれば投稿できる' do expect(@comment).to be_valid end end context 'コメントが投稿できないとき' do it 'コメントが空欄では投稿できない' do @comment.comment = '' @comment.valid?      binding.pry expect(@comment.errors.full_messages).to include('Commentを入力してください') end it 'ユーザーid(user_id)が空では投稿できない' do @comment.user_id = nil @comment.valid?      binding.pry expect(@comment.errors.full_messages).to include('Userを入力してください') end it '記事id(article_id)が空では投稿できない' do @comment.article_id = nil @comment.valid?      binding.pry expect(@comment.errors.full_messages).to include('Articleを入力してください') end end end end % bundle exec rspec spec/models/comment_spec.rb Comment 記事へのコメント機能 コメントが投稿できるとき フォームに正しく入力すれば投稿できる コメントが投稿できないとき From: /Users/私/projects/アプリ/spec/models/comment_spec.rb:22 : 17: 18: context 'コメントが投稿できないとき' do 19: it 'コメントが空欄では投稿できない' do 20: @comment.comment = '' 21: @comment.valid? => 22: binding.pry 23: expect(@comment.errors.full_messages).to include('Commentを入力してください') 24: end 25: it 'ユーザーid(user_id)が空では投稿できない' do 26: @comment.user_id = nil 27: @comment.valid? [1] pry(#<RSpec::ExampleGroups::Comment::Nested::Nested_2>)> exit コメントが空欄では投稿できない From: /Users/私/projects/アプリ/spec/models/comment_spec.rb:28 : 23: expect(@comment.errors.full_messages).to include('Commentを入力してください') 24: end 25: it 'ユーザーid(user_id)が空では投稿できない' do 26: @comment.user_id = nil 27: @comment.valid? => 28: binding.pry 29: expect(@comment.errors.full_messages).to include('Userを入力してください') 30: end 31: it '記事id(article_id)が空では投稿できない' do 32: @comment.article_id = nil 33: @comment.valid? [1] pry(#<RSpec::ExampleGroups::Comment::Nested::Nested_2>)> exit ユーザーid(user_id)が空では投稿できない From: /Users/私/projects/アプリ/spec/models/comment_spec.rb:34 : 29: expect(@comment.errors.full_messages).to include('Userを入力してください') 30: end 31: it '記事id(article_id)が空では投稿できない' do 32: @comment.article_id = nil 33: @comment.valid? => 34: binding.pry 35: expect(@comment.errors.full_messages).to include('Articleを入力してください') 36: end 37: end 38: end 39: end [1] pry(#<RSpec::ExampleGroups::Comment::Nested::Nested_2>)> exit 記事id(article_id)が空では投稿できない Finished in 4.98 seconds (files took 1.31 seconds to load) 4 examples, 0 failures 問題なく通りました! では、 binding.pry を削除して、 before do 内を書き換えてみます spec/models/comment_spec.rb RSpec.describe Comment, type: :model do before do @user = FactoryBot.create(:user) @article = FactoryBot.create(:article) @comment_user = FactoryBot.create(:user) @comment = FactoryBot.build(:comment, user_id: @comment_user.id, article_id: @article.id) sleep(0.1) # ここを追加 end # 以下の文章は binding.pry のみを削除してください。それ以外は何も変更しません では…いざターミナルで実行!!  % bundle exec rspec spec/models/comment_spec.rb Comment 記事へのコメント機能 コメントが投稿できるとき フォームに正しく入力すれば投稿できる コメントが投稿できないとき コメントが空欄では投稿できない ユーザーid(user_id)が空では投稿できない 記事id(article_id)が空では投稿できない Finished in 0.50371 seconds (files took 1.31 seconds to load) 4 examples, 0 failures 問題なく終了できました〜 この sleep(0.1)ですが、 引数に秒数を渡すことによって、その秒数分処理を停止することができます ms(ミリ秒)の場合は上記の様に引数へ(0.1)や(0.01)と記載すればOKです。 今回は連続した処理で、「SQL が read_timeout 以内に間に合わなかった」と推測します 一つの処理が終わったら0.1秒休憩させて次の処理をさせる そんなイメージを持っていれば良いのではないでしょうか?? みなさんも解決できましたか? さてさて、私は先に進むとします
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsで作ったアプリをHerokuでデプロイする

作ったアプリをデプロイする Heroku CLIをインストール 一応念の為にデプロイしたいアプリのディレクトリで行いましょう。 brew tap heroku/brew && brew install heroku 完了を確認するために下記のコマンドを入力 % heroku --version heroku/7.40.0 darwin-x64 node-v12.16.2 上記のようにバージョンが出力されれば成功 Herokuにログイン # Herokuへログインするためのコマンド % heroku login --interactive => Enter your Heroku credentials. # メールアドレスを入力し、エンターキーを押す => Email: # パスワードを入力して、エンターキーを押す => Password: Heroku上にアプリケーションを作成 % heroku create アプリ名 アプリをデプロイする前に、まずデータベースの設定 Herokuでは、使用するデータベースの設定が、デフォルトでPostgreSQLというデータベースになっています。 今回は開発環境と同様にMySQLを使用するための設定を行います。 ClearDBアドオン ClearDBアドオンとは、MySQLを使うためのツール % heroku addons:add cleardb Creating cleardb on ⬢ ajax-app-123456... free Created cleardb-vertical-00000 as CLEARDB_DATABASE_URL Use heroku addons:docs cleardb to view documentation 上記のようにでればOK。 設定を変更 データベースをMySQLに設定できました。しかし、Ruby on Railsを使う場合は、MySQLに対応するGemについて考慮する必要があり、そちらの設定を変更します。 % heroku_cleardb=`heroku config:get CLEARDB_DATABASE_URL` これでClearDBデータベースのURLを変数heroku_cleardbに格納 % heroku config:set DATABASE_URL=mysql2${heroku_cleardb:5} これでデータベースのURLを再設定できました。 DATABASE_URLの冒頭がmysql2://に変更されていることも確認しておきましょう。 アプリケーションを開発する際、サーバーのアクセスキーやAPIキーなど、外部に漏らしたくない情報を扱う場面があります。デプロイをする際、それらの情報はセキュリティの観点から暗号化する必要があります。暗号化された情報は、開発環境および本番環境であらかじめ用意した鍵を用いて復号(暗号化された情報を使えるように)します。 Railsのバージョン5.2以降では、credentials.yml.encを利用して、それらの情報を暗号化します。 credentials.yml.encファイル Railsにて、外部に漏らしたくない情報を扱う際に用いるファイルです。 通常時は、英数字の文字列で構成された暗号文が表示され、ファイル内に何が書かれているのか分からないようになっています。しかし、このcredentials.yml.encと対になるmaster.keyが存在する場合、credentials.yml.encの暗号文を復号し、ファイル内の記述を確認できます。 master.keyファイル credentials.yml.encの暗号文を復号する、鍵の役割を持ったファイルです。特定のcredentials.yml.encと対になっているため、その他のcredentials.yml.encへは、効果を発揮しません。 また、master.keyは非常に重要なファイルなので、リモートリポジトリに反映されることは好ましくありません。そのため、デフォルトで.gitignoreに記述されており、Gitで管理されない仕組みになっています。 Heroku上にmaster.keyを設置 Heroku上には、環境変数としてmaster.keyの値を設置します。Herokuへ環境変数を設定するためには、 heroku configというコマンドを使用します。 環境変数 OSが提供するデータ共有機能の1つで、「どのディレクトリ・ファイルからでも参照できる変数」を設けることができます。一般的な用途の1つとして、「外部に漏らしたくない情報を環境変数にセットする」というものがあります。 heroku configコマンド Heroku上で環境変数の参照・追加・削除等をする場合に用います。環境変数の追加であればheroku config:set 環境変数名="値"と実行します。そうすることによって、Heroku上で環境変数を追加できます。 Heroku上で環境変数を設定 % heroku config:set RAILS_MASTER_KEY=`cat config/master.key` Heroku上で環境変数を確認 % heroku config 動作環境を変更 Stackとは、Herokuにおけるアプリケーションの動作環境のことです。Stackはデプロイされたアプリケーションを読み取り正常に稼働させるために用意されています。 デフォルトのStackは「Heroku-20」です。一方で、別のStackを選択することもできます。 今回私が作ったアプリのRubyのバージョンは、Heroku-20では使用できません。 そのため、Rubyのバージョン2.6.5が動作するStackを指定します。 % heroku stack:set heroku-18 -a アプリ名 Herokuへアプリケーションを追加 git push heroku masterを実行 % git push heroku master Heroku上でマイグレーションファイルを実行 % heroku run rails db:migrate 以下のコマンドで公開されたアプリケーションの詳細を見ることができます。 % heroku apps:info デプロイ後のエラー対処方法 heroku logs --tail --app <<アプリケーション名>> をしてエラーを探す。。 はじめてデプロイをする場合 Herokuにアカウント登録する Heroku CLIをインストールする masterブランチへcommitする Heroku上にアプリケーションを作成する MySQLを使用できるように設定する master.keyを環境変数として設定する Herokuへアプリケーションの情報をpushする Heroku上でマイグレーションを実行する デプロイ済みのアプリケーションに変更修正を加えた場合 変更修正をcommitする ブランチを作成していた場合は、masterブランチへマージする Heroku上にpushする (テーブルに変更を加えた場合は)Heroku上でマイグレーションを実行する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】Pythonistaの自分がRubyのPythonにはない機能をまとめていく

【Ruby】Pythonistaの自分がRubyのPythonにはない機能をまとめてみた Rubyを新しく学習するにあたって、RubyのPythonにはない(知らないだけであるのかもしれないけど)機能や、新しく知ったことのみを随時まとめていきます。 真偽値と条件分岐 真偽値ルール falseまたはnilであれば偽 それ以外は全部真。 # !!を前につけることでbool値に変換する !!true #=> true !!1 #=> true !!0 #=> true !!-1 #=> true !!'true' #=> true !!'false' #=> true !!'' #=> true !![] #=> true 真偽値ルールがpythonと違うのは結構厄介な気がする、こんがらがりそう。 空判定 ''.empty? #=> true [].empty? #=> true 'abc'.empty? #=> false [1, 2, 3].empty? #=> false 0判定 0.zero? #=> true 1.zero? #=> false 文字列が含まれているか判定 'soccer'.include?('ce') #=> true 'soccer'.include?('at') #=> false 奇数判定 1.odd? #=> true 2.odd? #=> false nil判定 nil.nil? #=> true 'abc'.nil? #=> false case文 複数の条件を指定するときに、シンプルに書くことができる case 対象のオブジェクトや式 when 値1 # 値1に一致する場合の処理 when 値2 # 値2に一致する場合の処理 when 値3 # 値3に一致する場合の処理 else # どれにも一致しない場合の処理 end country = 'japan' message = case country when 'japan' 'こんにちは' when 'us' 'Hello' when 'italy' 'ciao' else '???' end puts message #=> 'こんにちは' thenを使って以下のようにも書くことができるが、使用頻度は高くないらしい。個人的にはわかりやすくて好き。 country = 'us' message = case country when 'japan' then 'こんにちは' when 'us' then 'Hello' when 'italy' then 'ciao' else '???' end puts message #=> 'Hello' 条件演算子(三項演算子) シンプルなif/elseであれば、スッキリ書けるが、複雑な場合は読みづらくなることがあるとのこと、確かに。 n = 11 message = n > 10 ? '10より大きい' : '10以下' puts message #=> '10より大きい' 配列・繰り返し処理 配列 Pythonでいうところのリスト # 空の配列を作る a = [] puts a #=> [] puts a.class #=> Array 配列の各要素を取得する方法も基本的にはpythonと同じそう。 a = [1, 2, 3] # 1つ目の要素を取得 puts a[0] #=> 1 puts a[1] #=> 2 puts a[2] #=> 3 存在しない要素を指定してもエラーにはならず、nilが返る。 a = [1, 2, 3] a[100] #=> nil 個人的にはエラーが出て欲しいところ。 a = [1, 2, 3] a.size #=> 3 a.length #=> 3 要素の変更、追加、削除 a = [1, 2, 3] a[1] = 20 puts a 元の大きさよりも大きい添字を指定すると、間の値がnilで埋められる。 a = [1, 2, 3] a[5] = 50 puts a #=> [1, 2, 3, nil, nil, 50] 使う場面が想像できない。エラーにした方が保守性が高い気もするけどどうなんだろう。 <<を使うと配列の最後に要素を追加することができる。pythonでいうところのappend。 a = [1, 2, 3] a << 50 puts a #=> [1, 2, 3, 50] a << 100 a << 150 puts a #=> [1, 2, 3, 50, 100, 150] 配列内の特定の位置にある要素を削除したい場合はdelete_atメソッドを使う。 a = [1, 2, 3, 50, 100, 150] # 2番目の要素を削除する(削除した値が戻り値になる) a.delete_at(1) #=> 2 puts a #=> [1, 3, 50, 100, 150] # 存在しない添字を指定するとnilが返る a.delete_at(100) #=> nil puts a #=> [1, 3, 50, 100, 150] ブロック ブロックはメソッドの引数として渡すことができる処理のかたまりのこと。ブロック内で記述した処理は必要に応じてメソッドから呼び出される。 Rubyの繰り返し処理  Rubyにもfor文はあるが、ほとんどのRubyプログラマはfor文を使わないらしい。Rubyの場合はforのような構文で繰り返し処理をさせるのではなく、配列自身に対して「繰り返せ」という命令を送る。 numbers = [1, 2, 3, 4] sum = 0 numbers.each do | n | sum += n end puts sum #=> 10 eachメソッドの役割は配列の要素を最後まで順番に取り出すことです。取り出した要素をどう扱うのかは、そのときの要件で変わってくる。そこで登場するのがブロックである。配列の要素を順番に取り出す作業はeachメソッドで行い、その要素をどう扱うかはブロックに記述する。上記でいうと、doからendまでがブロックになる。 | n |のnはブロック引数と呼ばれるもので、eachメソッドから渡された配列の要素が入る。 do ... end と{} Rubyの文法上、改行を入れなくてもブロックは動作する。 numbers = [1, 2, 3, 4] sum = 0 # do ... endの代わりに{}を使う numbers.each {| n | sum += n} puts sum #=> 10 ブロックを使う配列のメソッド delete_if delete_ifメソッドは各要素に対してブロックを評価して、値が真であれば、ブロックに渡した要素を配列から削除する。 numbers = [1, 2, 3, 1, 2, 3] del_numbers = numbers.delete_if { | n | n.odd?} puts del_numbers #=> [2, 2] map/collect mapメソッドは各要素に対してブロックを評価した結果を新しい配列にして返す。 numbers = [1, 2, 3, 4, 5] # ブロックの戻り値が新しい配列の各要素になる new_numbers = numbers.map {| n | n * 10} puts new_numbers #=> [10, 20, 30, 40, 50] select/find_all/reject selectメソッドは各要素に対してブロックを評価し、その戻り値が真の要素を集めた配列を返すメソッドである。 numbers = [1, 2, 3, 4, 5, 6] # ブロックの戻り値が真になった要素だけが集められる even_numbers = numbers.select { | n | n.even? } puts even_numbers #=> [2, 4, 6] rejectメソッドはselectメソッドの反対で、ブロックの戻り値が真になった要素を除外した配列を作成する。 numbers = [1, 2, 3, 4, 5, 6] # 3の倍数を除外する(3の倍数以外を集める) non_multiples_of_three = numbers.reject {| n | n % 3 == 0} puts non_multiples_of_three #=> [1, 2, 4, 5] find/detect findメソッドはブロックの戻り値が真になった最初の要素を返す。 numbers = [1, 2, 3, 4, 5, 6] # ブロックの戻り値が最初に真になった要素を返す even_number = numbers.find { |n| n.even? } puts even_number #=> 2 inject/reduce injectメソッドは畳み込み演算を行うメソッドである。 numbers = [1, 2, 3, 4] sum = 0 numbers.each { | n | sum += n} puts sum #=> 10 上のコードはinjectメソッドを使うと次のように書くことができる。 numbers = [1, 2, 3, 4] sum = numbers.inject(0) { | result, n | result + n} puts sum #=> 10 ブロックの第一引数(上記コードのresult)は初回のみinjectメソッドの引数(上記のコードでは0)が入る。2回目以降は前回のブロックの戻り値が入る。 ブロックの第二引数(上記コードのn)は配列の各要素が順番い入る。ブロックの戻り値は次の回に引き継がれ、ブロックの第一引数(result)に入る。繰り返し処理が最後まで終わると、ブロックの戻り値がinjectメソッドの戻り値になる。 1回目: result = 0, n = 1で、0+1=1。これが次のresultに入る 2回目: result = 1, n = 2で、1+2=3。これが次のresultに入る 3回目: result = 3, n = 3で、3+3=6。これが次のresultに入る 4回目: result = 6, n = 4で、6+4=10。最後の要素に達したのでこれがinjectメソッドの戻り値になる。 基礎知識系 ガベージコレクション  使用されなくなったオブジェクトを回収し、自動的にメモリを開放してくれる機能がRubyには備わっている。 エイリアスメソッド  Rubyにはまったく同じメソッドに複数の名前がついている場合があるらしい。 # length, size 'hello'.length #=> 5 'hello'.size #=> 5 意味があるのだろうか? 式(Expression)と文(statement) Rubyではほかの言語では文と見なされるような要素が式になっていることが多い。 式: 値を返し、結果を変数に代入できるもの 文: 値を返さず、変数に代入しようとすると構文エラーになるもの 上記で分類すると、Rubyのif文やメソッド定義は文ではなく、式になっている。 a = if true '真' else '偽' end puts a #=> 真 b = def foo; end puts b #=> foo 参考 プロを目指す人のためのRuby入門
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】数字を当てるゲーム(guess number)をrubyで作ってみた

初めに 今まで学習した内容をもとに、requireを使って数字を当てるゲームを作ってみました。 ※内容に間違いなどがある場合はご指摘をよろしくお願いします。 結果物 文字(アルファベット)と記号、数字の数を順番に入力すると入力した合計分の強力なパスワードが生成される。 作業手順 ①やりたいことをまず文章で書く(アルゴリズム) ②ASCIIを生成 ②細かな問題の記述 ③文章を元にフローチャートを書く ④プログラムを書く 大きな流れ ロゴを用意する 答えを1~100までの数字で用意する 難易度を設定する処理(ターンが決まる) 答えをチェックする処理 残りのターンによる分岐点を用意する 細かな作業&ロジック ロゴと難易度の設定、答えのチェックはそれぞれ別のファイルに記述・管理する 難易度はeasyとhardがあり、難易度に応じて残りのターンが決まる 答えが入力した数字と合っていればゲームは終了 残りのターンが0の場合、ゲーム終了 フローチャート ロゴファイルを作成 以下のサイトからゲームに使われるロゴを作成します。ゲーム名は"Guess Number" ASCII:http://patorjk.com/software/taag/#p=display&f=ANSI%20Shadow&t=GUESS%20NUMBER 生成されたASCIIをコピーしたら、ファイルに貼り付けます。 #art_guess_number.rb LOGO = " ██████╗ ██╗ ██╗███████╗███████╗███████╗ ███╗ ██╗██╗ ██╗███╗ ███╗██████╗ ███████╗██████╗ ██╔════╝ ██║ ██║██╔════╝██╔════╝██╔════╝ ████╗ ██║██║ ██║████╗ ████║██╔══██╗██╔════╝██╔══██╗ ██║ ███╗██║ ██║█████╗ ███████╗███████╗ ██╔██╗ ██║██║ ██║██╔████╔██║██████╔╝█████╗ ██████╔╝ ██║ ██║██║ ██║██╔══╝ ╚════██║╚════██║ ██║╚██╗██║██║ ██║██║╚██╔╝██║██╔══██╗██╔══╝ ██╔══██╗ ╚██████╔╝╚██████╔╝███████╗███████║███████║ ██║ ╚████║╚██████╔╝██║ ╚═╝ ██║██████╔╝███████╗██║ ██║ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═ " 難易度を決める処理 ゲームの難易度をeasyかhard、どちらかに選択してもらう関数を作ります。 #set_difficulty.rb EASY_LEVEL = 10 HARD_LEVEL = 5 def set_difficulty() loop do puts "難易度を選んでください。easyかhardをどちらかを入力してください" level = gets.chomp if level == "easy" return EASY_LEVEL elsif level == "hard" return HARD_LEVEL else puts "入力が間違っています" end end end 答えをチェックする処理 ユーザーが入力した数字が答えと合っているかどうかを判定する関数を作成します。これも難易度設定と同じく別のファイルで作成します。 #check_answer.rb def check_answer(guess, answer, turns) if guess > answer puts "#{guess}より低い数字です" return turns -1 elsif guess < answer puts "#{guess}より高い数字です" return turns -1 else puts "さすがです。答えは#{answer}でした" end end ゲームの処理(メイン) 上記で作成したロゴと難易度設定、答えチェックをrequireを使って読み込みます。gameという関数を定義したら、ゲームを実行します。最初にロゴ画面が出てきて、ユーザーに難易度を入力してもらい数字を当てていきます。ターン数は難易度によって5回か10回になり、0になったらゲーム終了です。 require './art_guess_number' require './set_difficulty' require './check_answer' def game() puts LOGO puts "私が考えている数字は何でしょう?" answer = rand(1..100) #答えをランダムで生成する turns = set_difficulty() #難易度によるターンの設定 guess = 0 loop do puts "チャンスはあと#{turns}回です。" puts "数字を当ててみてください。範囲は1〜100の間です。" guess = gets.to_i turns = check_answer(guess, answer, turns) #答えのチェック if turns == 0 puts "残念です。あなたの負けです。" break elsif guess != answer puts "違いますね。もう一度チャンスをあげます。" else break end end end game() 実行結果 ❯ ruby guess_number.rb ██████╗ ██╗ ██╗███████╗███████╗███████╗ ███╗ ██╗██╗ ██╗███╗ ███╗██████╗ ███████╗██████╗ ██╔════╝ ██║ ██║██╔════╝██╔════╝██╔════╝ ████╗ ██║██║ ██║████╗ ████║██╔══██╗██╔════╝██╔══██╗ ██║ ███╗██║ ██║█████╗ ███████╗███████╗ ██╔██╗ ██║██║ ██║██╔████╔██║██████╔╝█████╗ ██████╔╝ ██║ ██║██║ ██║██╔══╝ ╚════██║╚════██║ ██║╚██╗██║██║ ██║██║╚██╔╝██║██╔══██╗██╔══╝ ██╔══██╗ ╚██████╔╝╚██████╔╝███████╗███████║███████║ ██║ ╚████║╚██████╔╝██║ ╚═╝ ██║██████╔╝███████╗██║ ██║ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═ 私が考えている数字は何でしょう? 難易度を選んでください。easyかhardをどちらかを入力してください hard チャンスはあと5回です。 数字を当ててみてください。範囲は1〜100の間です。 4 4より高い数字です 違いますね。もう一度チャンスをあげます。 チャンスはあと4回です。 数字を当ててみてください。範囲は1〜100の間です。 10 10より高い数字です 違いますね。もう一度チャンスをあげます。 チャンスはあと3回です。 数字を当ててみてください。範囲は1〜100の間です。 40 40より低い数字です 違いますね。もう一度チャンスをあげます。 チャンスはあと2回です。 数字を当ててみてください。範囲は1〜100の間です。 30 30より低い数字です 違いますね。もう一度チャンスをあげます。 チャンスはあと1回です。 数字を当ててみてください。範囲は1〜100の間です。 20 20より高い数字です 残念です。あなたの負けです。 参考記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails × Carrierwave で作成した画像アップロードAPIをReact側から叩くためのチュートリアル

概要 バックエンド: Rails(APIモード) フロントエンド: React(もしくはVue) 最近、こんな感じで役割分担しつつ開発する人が増えていると思います。(特に未経験からのエンジニア転職を目指している人の多くは大体そんな感じでポートフォリオを作られている印象) 自分自身も上記の構成が好きでシコシコ個人開発してたりするわけですが、画像アップロード機能を実装しようと思った際に少し手こずったのでメモ書きとして残しておきます。 ※「チュートリアル」と銘打ってはいるものの、細かいコードの説明などはあまりありません。一応、手順通りに進めれば同じものは作れるはずなのでまずは自分の手を動かした後に各コードを読み込んでみてください。 完成イメージ 画像プレビュー機能なども付けてそれっぽく仕上げてみました。 使用技術 Rails6(APIモード) React TypeScript バックエンド まず最初にバックエンド側から作っていきましょう。 ディレクトリ&各種ファイルを作成 $ mkdir mkdir rails-react-carrierwave-backend && cd rails-react-carrierwave-backend $ touch Dockerfile $ touch docker-compose.yml $ touch entrypoint.sh $ touch Gemfile $ touch Gemfile.lock ./Dockerfile FROM ruby:3.0 RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs ENV APP_PATH /myapp RUN mkdir $APP_PATH WORKDIR $APP_PATH COPY Gemfile $APP_PATH/Gemfile COPY Gemfile.lock $APP_PATH/Gemfile.lock RUN bundle install COPY . $APP_PATH COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 CMD ["rails", "server", "-b", "0.0.0.0"] ./docker-compose.yml version: "3" services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: password command: --default-authentication-plugin=mysql_native_password volumes: - mysql-data:/var/lib/mysql - /tmp/dockerdir:/etc/mysql/conf.d/ ports: - 3306:3306 api: build: context: . dockerfile: Dockerfile command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: - .:/myapp - ./vendor/bundle:/myapp/vendor/bundle environment: TZ: Asia/Tokyo RAILS_ENV: development ports: - "3001:3000" depends_on: - db volumes: mysql-data: ./entrypoint.sh #!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /myapp/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@" ./Gemfile # frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } gem "rails", "~> 6" /Gemfile.lock # 空欄でOK rails new おなじみのコマンドでプロジェクトを作成します。 $ docker-compose run api rails new . --force --no-deps -d mysql --api Gemfileが更新されたので再ビルド。 $ docker-compose build 「.config/database.yml」を編集 ./config/database.yml default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password # デフォルトだと空欄になっているはずなので変更 host: db # デフォルトだとlocalhostになっているはずなので変更 development: <<: *default database: myapp_development test: <<: *default database: myapp_test production: <<: *default database: <%= ENV["DATABASE_NAME"] %> username: <%= ENV["DATABASE_USERNAME"] %> password: <%= ENV["DATABASE_PASSWORD"] %> host: <%= ENV["DATABASE_HOST"] %> データベースを作成 $ docker-compose run api rails db:create 動作確認 $ docker-compose up -d localhost:3001 にアクセスしていつもの画面が表示されればOK。 APIを作成 carrierwaveをインストール。 ./Gemfile gem 'carrierwave' Gemfileを更新したので再ビルド。 $ docker-compose build アップローダーを作成。 $ docker-compose run api rails g uploader Image すると「./app/uploaders/image_uploader.rb」が自動生成されるので次のように変更します。 .app/uploaders/image_uploader.rb class ImageUploader < CarrierWave::Uploader::Base storage :file def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end # 受け付け可能なファイルの拡張子を指定 def extension_allowlist %w(jpg jpeg png) end end また、「./config/initializers/」配下にcarrierwave設定用のファイルを作成。 $ touch config/initializers/carrierwave.rb ./config/initializers/carrierwave.rb CarrierWave.configure do |config| config.asset_host = "http://localhost:3001" config.storage = :file config.cache_storage = :file end Postモデルを作成。 $ docker-compose run api rails g model Post content:text image:string $ docker-compose run api rails db:migrate 先ほど作成したアップローダーをマウントします。(ついでにバリデーションもやっておきましょう。) ./app/models/post.rb class Post < ApplicationRecord mount_uploader :image, ImageUploader validates :content, presence: true, length: { maximum: 140 } end コントローラーを作成。 $ docker-compose run api rails g controller api::v1::posts ./app/controllers/api/v1/posts_controller.rb class Api::V1::PostsController < ApplicationController before_action :set_post, only: %i[destroy] def index render json: { posts: Post.all.order("created_at DESC") } end def create post = Post.new(post_params) post.save end def destroy post = Post.find(params[:id]) post.destroy end private def set_post @post = Post.find(params[:id]) end def post_params params.permit(:content, :image) end end ルーティングを記述。 ./config/routes.rb Rails.application.routes.draw do namespace :api do namespace :v1 do resources :posts, only: %i[index create destroy] end end end これで準備は完了です。 あとは動作確認のためルートディレクトリに適当な画像を「sample.jpg」という名前配置し、次のcurlコマンドを実行しましょう。 $ curl -F "content=test" -F "image=@sample.jpg" http://localhost:3001/api/v1/posts 特にエラーっぽいレスポンスが返ってこなければ上手くいっているはずなので、次のcurlコマンドで確認します。 $ curl -X GET http://localhost:3001/api/v1/posts { "posts": [ { "id": 1, "content": "test", "image": { "url": "http://localhost:3001/uploads/post/image/1/sample.jpg" }, "created_at": "2021-05-03T17:36:33.147Z", "updated_at": "2021-05-03T17:36:33.147Z" } ] } こんな感じで画像のパスが保存されていればOK。 CORSの設定 これでAPIは完成ですが、今の状態のままReact側から呼び出そうとするとセキュリティ的な問題でエラーが生じます。 そこで、CORSの設定を行わなければなりません。 参照: CORS とは? RailsにはCORSの設定を簡単に行えるgemが存在するのでインストールしましょう。 rb./Gemfile gem 'rack-cors' APIモードで作成している場合、すでにGemfile内に記載されているのでそちらのコメントアウトを外せばOKです。 Gemfileを更新したので再度ビルド。 $ docker-compose build あとは「./config/initializers/」配下にある設定ファイルをいじくり外部からアクセス可能なようにしておきます。 ./config/initializers/cors.rb Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins "localhost:3000" # React側はポート番号3000で作るので「localhost:3000」を指定 resource "*", headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end これで設定は完了。 フロントエンド フロントエンド側は別リポジトリで作成します。(その方がコードの見通しも良くなるので) create-react-app $ mkdir rails-react-carrierwave-frontend && cd rails-react-carrierwave-frontend $ npx create-react-app . --template typescript 不要なファイルを整理 $ rm src/App.css src/App.test.tsx src/logo.svg src/reportWebVitals.ts src/setupTests.ts 「./src/index.tsx」と「./src/App.tsx」を次のように変更します。 ./src/index.tsx import React from "react" import ReactDOM from "react-dom" import "./index.css" import App from "./App" ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") ) ./src/App.tsx import React from "react" const App: React.FC = () => { return ( <h1>Hello World!</h1> ) } export default App 一旦、動作確認してみましょう。 $ yarn start localhost:3000 にアクセスして「Hello World!」と返ってくればOK。 各種ディレクトリ・ファイルを準備 $ mkdir components $ mkdir components/post $ mkdir interfaces $ mkdir lib $ mkdir lib/api $ touch components/post/PostForm.tsx $ touch components/post/PostItem.tsx $ touch components/post/PostList.tsx $ touch interfaces/index.ts $ touch lib/api/client.ts $ touch lib/api/posts.ts $ mv components interfaces lib src 最終的に次のような構成になっていればOK。 rails-react-carrierwave-frontend ├── node_modules ├── public │   ├── favicon.ico │   ├── index.html │   ├── logo192.png │   ├── logo512.png │   ├── manifest.json │   └── robots.txt ├── src │   ├── components │   │   └── post │   │   ├── PostForm.tsx │   │   ├── PostItem.tsx │   │   └── PostList.tsx │   ├── interfaces │   │   └── index.ts │   ├── lib │   │   └── api │   │   ├── client.ts │   │   └── posts.ts │   ├── App.tsx │   ├── index.css │   ├── index.tsx │   ├── react-app-env.d.ts ├── .gitignore ├── package.json ├── README.md ├── tsconfig.json └── yarn.lock 各種ライブラリをインストール $ yarn add @material-ui/core@next @material-ui/icons@next @material-ui/lab@next @material-ui/styled-engine @emotion/react @emotion/styled axios react-router-dom @types/react-router-dom material-ui UIを整える用のライブラリ。(ついこの前最新バージョンv5が発表されたので試しにそちらを使用) emotion material-uiの最新バージョンを使う際に追加で必要なライブラリ。 axios APIリクエスト用のライブラリ。 react-router-dom ルーティング設定用のライブラリ。 型定義 ./src/interfaces/index.ts export interface Post { id: string content: string image?: { url: string } } export interface PostApiJson { posts: Post[] } APIクライアントを作成 ./src/lib/api/client.ts import axios, { AxiosInstance, AxiosResponse } from "axios" let client: AxiosInstance export default client = axios.create({ baseURL: "http://localhost:3001/api/v1", headers: { "Content-Type": "multipart/form-data" // 画像ファイルを取り扱うのでform-dataで送信 } }) client.interceptors.response.use( (response: AxiosResponse): AxiosResponse => { const data = response.data return { ...response.data, data } } ) ./src/lib/api/posts.ts import { AxiosPromise } from "axios" import client from "./client" import { PostApiJson } from "../../interfaces/index" // post取得 export const getPosts = (): AxiosPromise<PostApiJson> => { return client.get("/posts") } // post作成 export const createPost = (data: FormData): AxiosPromise => { return client.post("/posts", data) } // post削除 export const deletePost = (id: string): AxiosPromise => { return client.delete(`/posts/${id}`) } ビュー部分を作成 ./src/components/PostList.tsx import React, { useEffect, useState } from "react" import { Container, Grid } from "@material-ui/core" import { makeStyles } from "@material-ui/core/styles" import PostForm from "./PostForm" import PostItem from "./PostItem" import { getPosts } from "../../lib/api/posts" import { Post } from "../../interfaces/index" const useStyles = makeStyles(() => ({ container: { marginTop: "3rem" } })) const PostList: React.FC = () => { const classes = useStyles() const [posts, setPosts] = useState<Post[]>([]) const handleGetPosts = async () => { const { data } = await getPosts() setPosts(data.posts) } useEffect(() => { handleGetPosts() }, []) return ( <Container maxWidth="lg" className={classes.container}> <Grid container direction="row" justifyContent="center"> <Grid item> <PostForm handleGetPosts={handleGetPosts} /> { posts?.map((post: Post) => { return ( <PostItem key={post.id} post={post} handleGetPosts={handleGetPosts} /> )} )} </Grid> </Grid> </Container> ) } export default PostList ./src/components/post/PostItem.tsx import React, { useState } from "react" import { makeStyles } from "@material-ui/core/styles" import Card from "@material-ui/core/Card" import CardHeader from "@material-ui/core/CardHeader" import CardMedia from "@material-ui/core/CardMedia" import CardContent from "@material-ui/core/CardContent" import CardActions from "@material-ui/core/CardActions" import Avatar from "@material-ui/core/Avatar" import IconButton from "@material-ui/core/IconButton" import Typography from "@material-ui/core/Typography" import FavoriteBorderIcon from "@material-ui/icons/FavoriteBorder" import FavoriteIcon from "@material-ui/icons/Favorite" import ShareIcon from "@material-ui/icons/Share" import DeleteIcon from "@material-ui/icons/Delete" import MoreVertIcon from "@material-ui/icons/MoreVert" import { Post } from "../../interfaces/index" import { deletePost } from "../../lib/api/posts" const useStyles = makeStyles(() => ({ card: { width: 320, marginTop: "2rem", transition: "all 0.3s", "&:hover": { boxShadow: "1px 0px 20px -1px rgba(0,0,0,0.2), 0px 0px 20px 5px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12)", transform: "translateY(-3px)" } }, delete: { marginLeft: "auto" } })) interface PostItemProps { post: Post handleGetPosts: Function } const PostItem = ({ post, handleGetPosts }: PostItemProps) => { const classes = useStyles() const [like, setLike] = useState<boolean>(false) const handleDeletePost = async (id: string) => { await deletePost(id) .then(() => { handleGetPosts() }) } return ( <> <Card className={classes.card}> <CardHeader avatar={ <Avatar> U </Avatar> } action={ <IconButton> <MoreVertIcon /> </IconButton> } title="User Name" /> { post.image?.url ? <CardMedia component="img" src={post.image.url} alt="post image" /> : null } <CardContent> <Typography variant="body2" color="textSecondary" component="span"> { post.content.split("\n").map((content: string, index: number) => { return ( <p key={index}>{content}</p> ) }) } </Typography> </CardContent> <CardActions disableSpacing> <IconButton onClick={() => like ? setLike(false) : setLike(true)}> { like ? <FavoriteIcon /> : <FavoriteBorderIcon /> } </IconButton> <IconButton> <ShareIcon /> </IconButton> <div className={classes.delete}> <IconButton onClick={() => handleDeletePost(post.id)} > <DeleteIcon /> </IconButton> </div> </CardActions> </Card> </> ) } export default PostItem ./src/components/post/PostForm.tsx import React, { useCallback, useState } from "react" import { experimentalStyled as styled } from '@material-ui/core/styles'; import { makeStyles, Theme } from "@material-ui/core/styles" import TextField from "@material-ui/core/TextField" import Button from "@material-ui/core/Button" import Box from "@material-ui/core/Box" import IconButton from "@material-ui/core/IconButton" import PhotoCameraIcon from "@material-ui/icons/PhotoCamera" import CancelIcon from "@material-ui/icons/Cancel" import { createPost } from "../../lib/api/posts" const useStyles = makeStyles((theme: Theme) => ({ form: { display: "flex", flexWrap: "wrap", width: 320 }, inputFileBtn: { marginTop: "10px" }, submitBtn: { marginTop: "10px", marginLeft: "auto" }, box: { margin: "2rem 0 4rem", width: 320 }, preview: { width: "100%" } })) const Input = styled("input")({ display: "none" }) const borderStyles = { bgcolor: "background.paper", border: 1, } interface PostFormProps { handleGetPosts: Function } const PostForm = ({ handleGetPosts }: PostFormProps) => { const classes = useStyles() const [content, setContent] = useState<string>("") const [image, setImage] = useState<File>() const [preview, setPreview] = useState<string>("") const uploadImage = useCallback((e) => { const file = e.target.files[0] setImage(file) }, []) // プレビュー機能 const previewImage = useCallback((e) => { const file = e.target.files[0] setPreview(window.URL.createObjectURL(file)) }, []) // FormData形式でデータを作成 const createFormData = (): FormData => { const formData = new FormData() formData.append("content", content) if (image) formData.append("image", image) return formData } const handleCreatePost = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() const data = createFormData() await createPost(data) .then(() => { setContent("") setPreview("") setImage(undefined) handleGetPosts() }) } return ( <> <form className={classes.form} noValidate onSubmit={handleCreatePost}> <TextField placeholder="Hello World" variant="outlined" multiline fullWidth rows="4" value={content} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setContent(e.target.value) }} /> <div className={classes.inputFileBtn}> <label htmlFor="icon-button-file"> <Input accept="image/*" id="icon-button-file" type="file" onChange={(e: React.ChangeEvent<HTMLInputElement>) => { uploadImage(e) previewImage(e) }} /> <IconButton color="inherit" component="span"> <PhotoCameraIcon /> </IconButton> </label> </div> <div className={classes.submitBtn}> <Button type="submit" variant="contained" size="large" color="inherit" disabled={!content || content.length > 140} className={classes.submitBtn} > Post </Button> </div> </form> { preview ? <Box sx={{ ...borderStyles, borderRadius: 1, borderColor: "grey.400" }} className={classes.box} > <IconButton color="inherit" onClick={() => setPreview("")} > <CancelIcon /> </IconButton> <img src={preview} alt="preview img" className={classes.preview} /> </Box> : null } </> ) } export default PostForm ./src/App.tsx import React from "react" import { BrowserRouter as Router, Switch, Route } from "react-router-dom" import PostList from "./components/post/PostList" const App: React.FC = () => { return ( <Router> <Switch> <Route exact path="/" component={PostList} /> </Switch> </Router> ) } export default App http://localhost:3000 にアクセスしてこんな感じになっていればOKです。 投稿を作成できるか、画像プレビュー機能が動いているか、投稿を削除できるかなど一通り確認してください。 あとがき お疲れ様でした。もし手順通りに進めて不具合などありましたらコメント欄にて指摘していただけると幸いです。 今回作成したコード
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】resoucesを使用しても、editやdestroyでActionController::RoutingErrorとエラーが発生してしまう場合の対処法

症状 Rails6.0.3のAPIモードでeditアクションに対してアクセスしようとしたとき、下記エラーが発生しアクセスできませんでした。 No route matches「ルーティングがマッチしていない」と怒られています。 route.rb ActionController::RoutingError (No route matches [GET] "/api/v1/hoges/2/edit"): actionpack (6.0.3.6) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call' railties (6.0.3.6) lib/rails/rack/logger.rb:37:in `call_app' railties (6.0.3.6) lib/rails/rack/logger.rb:26:in `block in call' activesupport (6.0.3.6) lib/active_support/tagged_logging.rb:80:in `block in tagged' activesupport (6.0.3.6) lib/active_support/tagged_logging.rb:28:in `tagged' activesupport (6.0.3.6) lib/active_support/tagged_logging.rb:80:in `tagged' railties (6.0.3.6) lib/rails/rack/logger.rb:26:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/request_id.rb:27:in `call' rack (2.2.3) lib/rack/runtime.rb:22:in `call' activesupport (6.0.3.6) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/executor.rb:14:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/static.rb:126:in `call' rack (2.2.3) lib/rack/sendfile.rb:110:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/host_authorization.rb:82:in `call' rack-cors (1.1.1) lib/rack/cors.rb:100:in `call' railties (6.0.3.6) lib/rails/engine.rb:527:in `call' puma (4.3.7) lib/puma/configuration.rb:228:in `call' puma (4.3.7) lib/puma/server.rb:713:in `handle_request' puma (4.3.7) lib/puma/server.rb:472:in `process_client' puma (4.3.7) lib/puma/server.rb:328:in `block in run' puma (4.3.7) lib/puma/thread_pool.rb:134:in `block in spawn_thread' ルーティングを確認すると、何となく問題なさそうです。 resouceだと、特定のデータにアクセスするようなルーティングは制限されますが、今回使っているのはeditやshowなどの特定のデータにアクセスできるアクションにも対応できるようにしています。 route.rb Rails.application.routes.draw do namespace :api do namespace :v1 do resources :fugasdo resources :hoges end resources :hoges end end end 実際にどのようなルーティングが存在しているかをrake routesで確認します。 rakeroutes rake routes Prefix Verb URI Pattern Controller#Action api_v1_fuga_hoges GET /api/v1/fugas/:fuga_id/hoges(.:format) api/v1/hoges#index POST /api/v1/fugas/:fuga_id/hoges(.:format) api/v1/hoges#create api_v1_fugat_food GET /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#show PATCH /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#update PUT /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#update DELETE /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#destroy api_v1_fugas GET /api/v1/fugas(.:format) api/v1/fugas#index POST /api/v1/fugas(.:format) api/v1/fugas#create api_v1_fugat GET /api/v1/fugas/:id(.:format) api/v1/fugas#show PATCH /api/v1/fugas/:id(.:format) api/v1/fugas#update PUT /api/v1/fugas/:id(.:format) api/v1/fugas#update DELETE /api/v1/fugas/:id(.:format) api/v1/fugas#destroy api_v1_hoges GET /api/v1/hoges(.:format) api/v1/hoges#index POST /api/v1/hoges(.:format) api/v1/hoges#create api_v1_food GET /api/v1/hoges/:id(.:format) api/v1/hoges#show PATCH /api/v1/hoges/:id(.:format) api/v1/hoges#update PUT /api/v1/hoges/:id(.:format) api/v1/hoges#update DELETE /api/v1/hoges/:id(.:format) api/v1/hoges#destroy rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create rails_mandrill_inbound_health_check GET /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#health_check rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#show PATCH /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update PUT /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update DELETE /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#destroy rails_conductor_inbound_email_reroute POST /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) rails/conductor/action_mailbox/reroutes#create rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create 上記の中に「edit」に該当するルーティングは存在しませんでした。 resoucesを使っても[edit」のルーティングが存在しないようです。 調べてみると、Railsガイドの「浅いネスト」に今回の原因となりそうな記述がありました。 要約すると、「親スコープ(fuga)の中でidを指定しないアクションのみを生成できる」というものでした。 つまり、今回のhugaに関するルーティングでは、routeファイルが上から読み込まれている関係上、最初にfuga以下にあったhogeリソースにヒットし、そこではid指定があるアクションが生成されていなかったから、このようになってしまったのかと思いましたが、resoucesの順序を変更してもルーティングにeditが追加されませんでした。 解決方法 resoucesで使用するアクションを明示的に書いたら、ルーティングエラーが出なくなりました。 route.rb resources :hoges,only: [:index, :show, :new, :create, :destroy, :edit, :update] do end RailsAPIモードだったので、生成されるルーティングは下記になります。 ゆえに、editやdestroyなどがルーティングでそもそも生成されない仕様で、resoucesでも使えないようです。 rakeroutes rake routes api_v1_posts GET /api/v1/posts(.:format) api/v1/posts#index POST /api/v1/posts(.:format) api/v1/posts#create api_v1_post GET /api/v1/posts/:id(.:format) api/v1/posts#show PATCH /api/v1/posts/:id(.:format) api/v1/posts#update PUT /api/v1/posts/:id(.:format) api/v1/posts#update DELETE /api/v1/posts/:id(.:format) api/v1/posts#destroy 参考 Railsガイド「浅い」ネスト https://railsguides.jp/routing.html#%E3%80%8C%E6%B5%85%E3%81%84%E3%80%8D%E3%83%8D%E3%82%B9%E3%83%88  Railsのresourcesとresourceついて https://qiita.com/Atsushi_/items/bb22ce67d14ba1abafc5 Railsで超簡単API https://qiita.com/k-penguin-sato/items/adba7a1a1ecc3582a9c9
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】resoucesを使用しても、editやdestroyでActionController::RoutingErrorとエラーが発生してしまう場合について

症状 Rails6.0.3のAPIモードでeditアクションに対してアクセスしようとしたとき、下記エラーが発生しアクセスできませんでした。 No route matches「ルーティングがマッチしていない」と怒られています。 route.rb ActionController::RoutingError (No route matches [GET] "/api/v1/hoges/2/edit"): actionpack (6.0.3.6) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call' railties (6.0.3.6) lib/rails/rack/logger.rb:37:in `call_app' railties (6.0.3.6) lib/rails/rack/logger.rb:26:in `block in call' activesupport (6.0.3.6) lib/active_support/tagged_logging.rb:80:in `block in tagged' activesupport (6.0.3.6) lib/active_support/tagged_logging.rb:28:in `tagged' activesupport (6.0.3.6) lib/active_support/tagged_logging.rb:80:in `tagged' railties (6.0.3.6) lib/rails/rack/logger.rb:26:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/request_id.rb:27:in `call' rack (2.2.3) lib/rack/runtime.rb:22:in `call' activesupport (6.0.3.6) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/executor.rb:14:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/static.rb:126:in `call' rack (2.2.3) lib/rack/sendfile.rb:110:in `call' actionpack (6.0.3.6) lib/action_dispatch/middleware/host_authorization.rb:82:in `call' rack-cors (1.1.1) lib/rack/cors.rb:100:in `call' railties (6.0.3.6) lib/rails/engine.rb:527:in `call' puma (4.3.7) lib/puma/configuration.rb:228:in `call' puma (4.3.7) lib/puma/server.rb:713:in `handle_request' puma (4.3.7) lib/puma/server.rb:472:in `process_client' puma (4.3.7) lib/puma/server.rb:328:in `block in run' puma (4.3.7) lib/puma/thread_pool.rb:134:in `block in spawn_thread' ルーティングを確認すると、何となく問題なさそうです。 resouceだと、特定のデータにアクセスするようなルーティングは制限されますが、今回使っているのはeditやshowなどの特定のデータにアクセスできるアクションにも対応できるようにしています。 route.rb Rails.application.routes.draw do namespace :api do namespace :v1 do resources :fugasdo resources :hoges end resources :hoges end end end 実際にどのようなルーティングが存在しているかをrake routesで確認します。 rakeroutes rake routes Prefix Verb URI Pattern Controller#Action api_v1_fuga_hoges GET /api/v1/fugas/:fuga_id/hoges(.:format) api/v1/hoges#index POST /api/v1/fugas/:fuga_id/hoges(.:format) api/v1/hoges#create api_v1_fugat_food GET /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#show PATCH /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#update PUT /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#update DELETE /api/v1/fugas/:fuga_id/hoges/:id(.:format) api/v1/hoges#destroy api_v1_fugas GET /api/v1/fugas(.:format) api/v1/fugas#index POST /api/v1/fugas(.:format) api/v1/fugas#create api_v1_fugat GET /api/v1/fugas/:id(.:format) api/v1/fugas#show PATCH /api/v1/fugas/:id(.:format) api/v1/fugas#update PUT /api/v1/fugas/:id(.:format) api/v1/fugas#update DELETE /api/v1/fugas/:id(.:format) api/v1/fugas#destroy api_v1_hoges GET /api/v1/hoges(.:format) api/v1/hoges#index POST /api/v1/hoges(.:format) api/v1/hoges#create api_v1_food GET /api/v1/hoges/:id(.:format) api/v1/hoges#show PATCH /api/v1/hoges/:id(.:format) api/v1/hoges#update PUT /api/v1/hoges/:id(.:format) api/v1/hoges#update DELETE /api/v1/hoges/:id(.:format) api/v1/hoges#destroy rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create rails_mandrill_inbound_health_check GET /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#health_check rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#show PATCH /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update PUT /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update DELETE /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#destroy rails_conductor_inbound_email_reroute POST /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) rails/conductor/action_mailbox/reroutes#create rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create 上記の中に「edit」に該当するルーティングは存在しませんでした。 resoucesを使っても[edit」のルーティングが存在しないようです。 調べてみると、Railsガイドの「浅いネスト」に今回の原因となりそうな記述がありました。 要約すると、「親スコープ(fuga)の中でidを指定しないアクションのみを生成できる」というものでした。 つまり、今回のhugaに関するルーティングでは、routeファイルが上から読み込まれている関係上、最初にfuga以下にあったhogeリソースにヒットし、そこではid指定があるアクションが生成されていなかったから、このようになってしまったのかと思いましたが、resoucesの順序を変更してもルーティングにeditが追加されませんでした。 解決方法 resoucesで使用するアクションを明示的に書いたら、ルーティングエラーが出なくなりました。 route.rb resources :hoges,only: [:index, :show, :new, :create, :destroy, :edit, :update] do end RailsAPIモードだったので、生成されるルーティングは下記になります。 ゆえに、editやdestroyなどがルーティングでそもそも生成されない仕様で、resoucesでも使えないようです。 rakeroutes rake routes api_v1_posts GET /api/v1/posts(.:format) api/v1/posts#index POST /api/v1/posts(.:format) api/v1/posts#create api_v1_post GET /api/v1/posts/:id(.:format) api/v1/posts#show PATCH /api/v1/posts/:id(.:format) api/v1/posts#update PUT /api/v1/posts/:id(.:format) api/v1/posts#update DELETE /api/v1/posts/:id(.:format) api/v1/posts#destroy 参考 Railsガイド「浅い」ネスト https://railsguides.jp/routing.html#%E3%80%8C%E6%B5%85%E3%81%84%E3%80%8D%E3%83%8D%E3%82%B9%E3%83%88  Railsのresourcesとresourceついて https://qiita.com/Atsushi_/items/bb22ce67d14ba1abafc5 Railsで超簡単API https://qiita.com/k-penguin-sato/items/adba7a1a1ecc3582a9c9
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む