- 投稿日:2022-02-24T23:47:01+09:00
【Rails】ログイン機能をGem 'devise'で実装
こんにちは。 早く駆け出したい修行僧です。 今回は、Gemの 'devise' を使ったログイン機能を学んだので、 こちらにアウトプットさせていただきます。 尚、「それは違うよ」など訂正箇所がございましたら、 愛のあるご指摘を頂けたら幸いです。 環境 macOS Monterey(M1) 12.2.1 ruby 3.1.0 Rails 6.1.4.6 devise 4.8.1 'devise'とは 特定のページを閲覧したりユーザに制限をかけたりするためにはログイン認証が必要となってきます。ログイン認証とはログインやログアウトの機能を指します。 そして、それらは新規登録をしないと始まりません。そんな機能を簡単に実装してくれる優れたgemが 'devise' です。 インストール rails new でアプリ作成済みであることを前提とします。 まずは導入したいアプリケーションのGemfileに以下のコードを挿入します。 Gemfile gem 'devise' 挿入ができましたら次はターミナルにて以下のコマンドを実行してgemをインストールします。 terminal $ bundle install 次はdeviseを使用するための設定ファイルをdevise専用コマンドで作成する必要があります。 以下のコマンドをターミナルにて実行します。('g'は'generate'の省略です) terminal $ rails g devise:install 上記のコマンドを実行すると、以下のような表示がされたらとりあえず成功です。 terminal =============================================================================== Depending on your application's configuration some manual setup may be required: 1. Ensure you have defined default url options in your environments files. Here is an example of default_url_options appropriate for a development environment in config/environments/development.rb: config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } In production, :host should be set to the actual host of your application. * Required for all applications. * 2. Ensure you have defined root_url to *something* in your config/routes.rb. For example: root to: "home#index" * Not required for API-only Applications * 3. Ensure you have flash messages in app/views/layouts/application.html.erb. For example: <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> * Not required for API-only Applications * 4. You can copy Devise views (for customization) to your app by running: rails g devise:views * Not required * =============================================================================== 何やらたくさんの英文が出てきて一瞬驚かれると思いますが、一つずつ進めていきましょう。 1. 設定するファイルにデフォルトURLの指定 公式にもある通り、デフォルトのURLの指定が必要となります。設定するファイルは以下の引用にも記載されている config/environments/development.rb です。 At this point, a number of instructions will appear in the console. Among these instructions, you'll need to set up the default URL options for the Devise mailer in each environment. Here is a possible configuration for config/environments/development.rb: 引用:公式リファレンス では具体的には何を指定するかは先ほどターミナルで出力された '1' に記載されています。 config/environments/development.rb config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } 2. ルートの設定 続いてルートの設定を行なっていきます。'devise'は新規登録完了後、指定のルートへ飛ぶ設定になっています。ですので、先ほどのメッセージに記載されている config/routes.rb ファイルに設定をしていきます。 config/routes.rb Rails.application.routes.draw do root "homes#index" get "/news", to: "news#news" end *現時点で、まだページを作成されていない方は、 rails g controller コントローラ名 index show などで適当に作っておいても良いと思います。今回、筆者はindexとnewsのページを作成しております。 3. フラッシュメッセージをビューファイルに挿入する フラッシュメッセージとはログインした際に、ヘッダー付近に「ログインしました」のようなメッセージを指します。Progateを既に学習された方は何となく見覚えがあるのではないでしょうか。 では、どこにフラッシュメッセージを挿入するかというと、こちらも先ほどのメッセージに記載されている app/views/layouts/application.html.erb へ挿入をしていきます。 app/views/layouts/application.html.erb <!DOCTYPE html> <% # 省略 %> <body> <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> <%= yield %> </body> 4. deviseのビューを作成する 最後にdeviseのviewを自分でカスタマイズするためには以下のコマンドを実行して作成されたファイルを編集していく必要があります。 terminal $ rails g devise:views 実行した後、以下のような表示がされていれば成功です。 terminal create app/views/devise/shared create app/views/devise/shared/_error_messages.html.erb create app/views/devise/shared/_links.html.erb create app/views/devise/confirmations create app/views/devise/confirmations/new.html.erb create app/views/devise/passwords create app/views/devise/passwords/edit.html.erb create app/views/devise/passwords/new.html.erb create app/views/devise/registrations create app/views/devise/registrations/edit.html.erb create app/views/devise/registrations/new.html.erb create app/views/devise/sessions create app/views/devise/sessions/new.html.erb create app/views/devise/unlocks create app/views/devise/unlocks/new.html.erb create app/views/devise/mailer create app/views/devise/mailer/confirmation_instructions.html.erb create app/views/devise/mailer/email_changed.html.erb create app/views/devise/mailer/password_change.html.erb create app/views/devise/mailer/reset_password_instructions.html.erb create app/views/devise/mailer/unlock_instructions.html.erb 詳しく何のファイルなのか知りたい!っていう方は @cigalecigales さんの 記事 をご覧になっていただけると理解が深まると思います。 モデルの作成 'devise'の設定が終わったら、今度はモデルを作成していきます。通常、モデルを作成する際は rails g model MODEL を実行すると思います。しかし、'devise'を使用する際は、'devise'専用のコマンドを実行してログイン機能に対応したモデルを作成します。 それでは、以下のコマンドを実行しましょう。 terminal $ rails generate devise <任意のモデル名> 実行した後、以下のような表示がされていると思います。 terminal create db/migrate/20220224141911_add_devise_to_users.rb insert app/models/user.rb route devise_for :users そして config/routes.rb を確認してみると、 devise_for :users と自動でルーティングが設定されていることがわかります。 Rails.application.routes.draw do devise_for :users root "homes#index" get "/news", to: "news#news" end モデルが作成できたら、忘れないうちにマイグレーションを実行して、ログイン機能に必要なテーブルを作成しておきましょう。 terminal $ rails db:migrate 最後に たったこれだけの設定でログイン機能が実装できることは非常にありがたいことだと思います。 'devise'にはもっと便利な機能があるみたいなので、もっと触っていき自由に使いこなせるようになりたいです。(追記で更新もしていこうかなとも思っています) 最後までご覧になっていただきありがとうございます。 参考文献 公式レファレンス [Rails] deviseの使い方(rails6版) - @cigalecigalesさん 【Rails】 deviseの使い方とは?ログイン認証機能を実装しよう
- 投稿日:2022-02-24T23:13:49+09:00
クエリパラメータ
クエリパラメータとは クエリパラメータとは、URLの末尾で疑問符「?」に続けてキーと値のペアを記述したもの。 .../edit?email=foo%40example.com 上記の場合、?email=foo%40example.comがクエリパラメータ。 ? からクエリパラメータを示して、emailがキー(パラメータ)としてfoo%40example.comを指定している。 ?キー(パラメータ)=キー(パラメータ)の値 %40はURLエンコード 値の表記に使用できない文字を含めたい場合は、「URLエンコード」あるいは「パーセントエンコーディング」と呼ばれる方式で安全な文字列に変換。 これは「エスケープ」と呼ばれる手法で、通常URLでは扱えない文字を扱えるようにする。 @はURLエンコードだと%40となる。 URLエンコード変換ツール URLのクエリパラメータではキーと値ペアのを複数使うこともでき、その場合は&でつなぐ。 .../edit?name=Foo%20Bar&email=foo%40example.com
- 投稿日:2022-02-24T22:47:18+09:00
アプリを作る パスワード再設定のメール送信
app/mailers/user_mailer.rb class UserMailer < ApplicationMailer def account_activation(user) @user = user mail to: user.email, subject: "アカウントを有効化" end def password_reset(user) @user = user # インスタンス変数に代入する mail to: user.email, subject: "パスワードをリセット" end end app/views/user_mailer/password_reset.text.erb パスワードをリセットするには、以下のリンクをクリックしてください: <%= edit_password_reset_url(@user.reset_token, email: @user.email) %> このリンクは2時間で期限切れになります パスワードのリセットをリクエストしなかった場合は、このメールを無視して、 パスワードはそのままです。 ### app/views/user_mailer/password_reset.html.erb .erb <h1>パスワードをリセット</h1> <p>To reset your password click the link below:</p> <%= link_to "Reset password", edit_password_reset_url(@user.reset_token, email: @user.email) %> <p>このリンクは2時間が期限です。</p> <p> If you did not request your password to be reset, please ignore this email and your password will stay as it is. </p> test/mailers/previews/user_mailer_preview.rb # Preview all emails at http://localhost:3000/rails/mailers/user_mailer class UserMailerPreview < ActionMailer::Preview # Preview this email at http://localhost:3000/rails/mailers/user_mailer/account_activation def account_activation user = User.first user.activation_token = User.new_token UserMailer.account_activation(user) end # Preview this email at http://localhost:3000/rails/mailers/user_mailer/password_reset def password_reset user = User.first user.reset_token = User.new_token UserMailer.password_reset(user) # パスワードリセットのメールを送信 end end test/mailers/user_mailer_test.rb require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:michael) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "アカウントを有効化", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded assert_match CGI.escape(user.email), mail.body.encoded end test "password_reset" do user = users(:michael) user.reset_token = User.new_token mail = UserMailer.password_reset(user) assert_equal "パスワードをリセット", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.reset_token, mail.body.encoded assert_match CGI.escape(user.email), mail.body.encoded end end app/views/password_resets/edit.html.erb .erb <% provide(:title, 'パスワードをリセット') %> <h1>Reset password</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(model: @user, url: password_reset_path(params[:id]), local: true) do |f| %> <%= render 'shared/error_messages' %> <%= hidden_field_tag :email, @user.email %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Update password", class: "btn btn-primary" %> <% end %> </div> </div> app/controllers/password_resets_controller.rb class PasswordResetsController < ApplicationController before_action :get_user, only: [:edit, :update] before_action :valid_user, only: [:edit, :update] before_action :check_expiration, only: [:edit, :update] # (1)への対応 # 編集、更新をする前にget_user,valid_userメソッドが作動 def new end def create @user = User.find_by(email: params[:password_reset][:email].downcase) if @user @user.create_reset_digest @user.send_password_reset_email flash[:info] = "パスワード再発行のためにメールを送信します。" redirect_to root_url else flash.now[:danger] = "Eメールアドレスが見つかりませんでした。" render 'new' end end def edit end def update if params[:user][:password].empty? # (3)への対応 # ユーザーのパスワードが空ではないか? @user.errors.add(:password, :blank) # 例 # > user.errors.add(:name, '文字数オーバー') # > user.errors.full_messages # => ["Name 文字数オーバー"] # 多分 :blankにメッセージが入っていると思う render 'edit' # 表示させるviewファイルを指定して表示 elsif @user.update(user_params) # (4)への対応 # インスタンス変数が更新できれば log_in @user # ログインする flash[:success] = "Password has been reset." # メッセージを表示 redirect_to @user # view の表示には直接は関係なく、新たな HttpRequest が発行されます。※GET のみ else render 'edit' # (2)への対応 # 編集ページを表示 end end private def user_params params.require(:user).permit(:password, :password_confirmation) # RailsでDBを更新する際に、不要なパラメータを取り除く(必要なパラメータだけに絞り込む)ためのメソッドです。 # ユーザーの:passwordと:password_confirmation属性だけ取り出す end def get_user @user = User.find_by(email: params[:email]) # dbからメアドを基に検索 インスタンス変数に代入 # params[:email]のメールアドレスに対応するユーザーをこの変数に保存します。 end # 正しいユーザーかどうか確認する def valid_user unless (@user && @user.activated? && @user.authenticated?(:reset, params[:id])) # インスタンス変数が有効性且つ有効化且つトークンの認証 # def authenticated?(attribute, token) # digest = send("#{attribute}_digest") # シンボルだけでいい # params[:id]はトークンを表すのかな? redirect_to root_url # 決められたコントローラーのアクション以外のアクションなどを実行させ、選択したビューファイルを表示させることができます。 end end # トークンが期限切れかどうか確認する def check_expiration if @user.password_reset_expired? # インスタンス変数 flash[:danger] = "Password reset has expired." # 失敗時のメッセージを表示 redirect_to new_password_reset_url # パスワードリセット画面に移動する # コントローラ外の動き end end end app/models/user.rb class User < ApplicationRecord # 継承させる attr_accessor :remember_token, :activation_token, :reset_token before_save :downcase_email before_create :create_activation_digest validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false } has_secure_password validates :password, presence: true, length: { minimum: 6 }, allow_nil: true def User.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost # コストパラメータはテストでは最小にする BCrypt::Password.create(string, cost: cost) end # ランダムなトークンを返す def User.new_token SecureRandom.urlsafe_base64 end # 永続セッションのためにユーザーをデータベースに記憶する def remember self.remember_token = User.new_token update_attribute(:remember_digest, User.digest(remember_token)) end # トークンがダイジェストと一致したらtrueを返す def authenticated?(attribute, token) digest = send("#{attribute}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token) end # ユーザーのログイン情報を破棄する def forget update_attribute(:remember_digest, nil) end def activate # # アカウントを有効にする update_attribute(:activated, true) update_attribute(:activated_at, Time.zone.now) end # 有効化用のメールを送信する def send_activation_email UserMailer.account_activation(self).deliver_now end # パスワード再設定の属性を設定する def create_reset_digest self.reset_token = User.new_token update_attribute(:reset_digest, User.digest(reset_token)) update_attribute(:reset_sent_at, Time.zone.now) end # パスワード再設定のメールを送信する def send_password_reset_email UserMailer.password_reset(self).deliver_now end # パスワード再設定の期限が切れている場合はtrueを返す def password_reset_expired? reset_sent_at < 2.hours.ago # 2時間まで end private # メールアドレスをすべて小文字にする def downcase_email self.email = email.downcase end # 有効化トークンとダイジェストを作成および代入する def create_activation_digest self.activation_token = User.new_token self.activation_digest = User.digest(activation_token) end end test/integration/password_resets_test.rb require 'test_helper' class PasswordResetsTest < ActionDispatch::IntegrationTest def setup ActionMailer::Base.deliveries.clear # わからない @user = users(:michael) # テストユーザー end test "password resets" do get new_password_reset_path # パスワードリセット画面に移動することを要求 assert_template 'password_resets/new' # 新しくメアド、パスワードを変更画面(ログイン画面)か確認 assert_select 'input[name=?]', 'password_reset[email]' # inputの欄にemailに入力できるか? post password_resets_path, params: { password_reset: { email: "" } } # メールアドレスが無効 # わからない assert_not flash.empty? # flashに中身があるか確認 assert_template 'password_resets/new' # パスワード再発行ページが表示されているか? post password_resets_path, params: { password_reset: { email: @user.email } } # メールアドレスが有効 assert_not_equal @user.reset_digest, @user.reload.reset_digest # reset_digestと再取得したreset_digestが違うか確認 assert_equal 1, ActionMailer::Base.deliveries.size # わからない assert_not flash.empty? # 中身はあるか確認 assert_redirected_to root_url # ホーム画面に移動する user = assigns(:user) # パスワード再設定フォームのテスト get edit_password_reset_path(user.reset_token, email: "") # メールアドレスが無効 assert_redirected_to root_url # 無効なユーザー user.toggle!(:activated) # 無効になっているユーザー get edit_password_reset_path(user.reset_token, email: user.email) # パスワード再発行のページに行くことを要求 assert_redirected_to root_url # ホーム画面に移動する user.toggle!(:activated) get edit_password_reset_path('wrong token', email: user.email) # メールアドレスが有効で、トークンが無効 assert_redirected_to root_url get edit_password_reset_path(user.reset_token, email: user.email) # メールアドレスもトークンも有効 assert_template 'password_resets/edit' assert_select "input[name=email][type=hidden][value=?]", user.email # 無効なパスワードとパスワード確認 patch password_reset_path(user.reset_token), params: { email: user.email, user: { password: "foobaz", password_confirmation: "barquux" } } assert_select 'div#error_explanation' # パスワードが空 patch password_reset_path(user.reset_token), params: { email: user.email, user: { password: "", password_confirmation: "" } } assert_select 'div#error_explanation' # 有効なパスワードとパスワード確認 patch password_reset_path(user.reset_token), params: { email: user.email, user: { password: "foobaz", password_confirmation: "foobaz" } } assert is_logged_in? assert_not flash.empty? assert_redirected_to user end end end 最後のテストはわからないことばっかりだ。 頑張ろう。
- 投稿日:2022-02-24T22:34:09+09:00
Rspecのallow_any_instance_ofを書き換える
はじめに インスタンスメソッドのスタブを allow_any_instance_of で書いていたのですが、どうやらこれが非推奨らしいので、これを書き換える方法をメモがてら記事にしました。 allow_any_instance_of を使ったテスト テスト対象のファイルはこちら test.rb class Test def test_method hoge = Hoge.new hoge.hello end end class Hoge def hello "hello!" end end そして allow_any_instance_of を使ったスペックファイルがこちら test_spec.rb require_relative 'test' describe 'Test' do context 'test_method' do before do allow_any_instance_of(Hoge).to receive(:hello).and_return('hello!(stub)') end it 'returns hello!(stub)' do expect(Test.new.test_method).to eq('hello!(stub)') end end end これでも一応動くのですが、はじめにも書いたように allow_any_instance_of は非推奨なので、今回はこれを instance_double を使った形に書き換えます。 instance_double を使ったテスト 先ほどのスペックファイルを instance_double を使って書き換える test_spec.rb require_relative 'test' describe 'Test' do context 'test_method' do it 'returns hello!(stub)' do hoge_moc = instance_double(Hoge) allow(Hoge).to receive(:new).and_return(hoge_moc) allow(hoge_moc).to receive(:hello).and_return('hello!(stub)') expect(Test.new.test_method).to eq('hello!(stub)') end end end Hoge で new メソッドを呼ばれた際に、instance_double で作ったモックを呼ぶようにしています。 このテストも書き換え前のテスト同様ちゃんと通ります。 おわりに 今回は書き換え前後をただメモ感覚で書き殴っているだけですが、ちゃんと理解したらまた追記するなり別の場所で書くなりしたいと思います。 書き換えの方法もこればっかりではないと思うので、このあたりについてはまた記事にしたいですね。
- 投稿日:2022-02-24T21:09:28+09:00
【個人開発】面倒くさがり屋のための糖質収支ジャッジアプリ「Sugarjudge」をリリースしました
はじめに みなさんは食事管理が得意ですか? 「痩せたくて食事制限をしようとしても面倒ですぐにやめてしまう」 そんなことありませんか? 私はそんな面倒くさがり屋のために食事管理のハードルを下げたアプリ「Sugarjudge」を作成しました。 サービス概要 アプリURL https://www.sugarjudge.com/ GitHub https://github.com/udakohei/Sugarjudge 痩せたいけど食事を管理、制限することが面倒で難しいという悩みを持った人に、 簡単でざっくりと楽しく糖質を管理、制限できる環境を提供する、 糖質収支ジャッジアプリです。 糖質収支を、一食の摂取糖質量から一食に摂取しても良い上限の糖質量を引いた値として考える。糖質を取りすぎたら赤字、制限内だったら黒字。 続けることは考えず一食一食の管理、制限を目指し、簡単な入力だけでその食事による自身の糖質収支が黒字か赤字かを判定 自己管理は難しいので誰かの目が必要。アプリ内、SNSで食事の写真と判定内容を投稿、共有する なぜ作ったのか 私には「食欲が抑えられず、ついついご飯をおかわりするなど、食べすぎて太ってしまう」という悩みがあります。学生の頃、糖質制限をおこない、半年で10キロ痩せた経験がありますが、その後2年でまた10キロ太ってしまいました。それなのに、面倒くさがり屋なので現在は運動や食事制限などはあまり続かず、習慣にするのにはハードルを感じています。自己管理だけじゃ続かないと考え、「簡単に緩くはじめられて、投稿した食事の写真がアプリ内で自動で共有されることで誰かの目を感じつつ、だけどネタにできるサービス」があったらいいなと思いこのサービスを作りたいと思いました。 機能 一般ユーザー 自身の名前、性別、制限レベルの3つの質問の入力と食事の写真と写真の解析から出した選択肢から選択することで、その食事の糖質収支が黒字か赤字かジャッジされる機能 判定内容をアプリ内で自動共有させる 自分の食事が赤字だった場合に謝罪文を打ち込むことができる 全判定にコメントを打ち込むことができる 他のユーザーの判定内容を閲覧できる 判定内容をアプリ内、Twitterで共有できる ログインユーザー 上記に加え、自身の判定内容、累積収支を振り返られる機能 使用技術 Ruby 3.0.2 Rails 6.0.4.1 jQuery Bootstrap v5.0 Chart.js Google Cloud Vision API Google Translate API Sorcery Heroku AWS S3 苦労した点 画像投稿→食品候補リスト表示までの流れの実装 ユーザーが自分の食べた食事をアップロードしてから、 自分の食べた食品を選択する表を表示させるまでの流れの実装が、難しかったです。 なぜ難しかったか 「外部のGoogleのサービスで画像解析を行っている + 解析結果を食品のデータベース内で検索し、候補の食品のデータを集めて表示させる」 といった、データの動きが見えないところで多方面かつ内容を変更させると言った複雑性をもっていたからです。 逆にいうと、ここの機能の部分がサーバーサイドのメインの実装となったので楽しかったです。 どうやって解決したか 紙に書いて、データがどのように流れるか図に書いて、整理しました。 流れの各順序をどのように実装するか考え、タスクを分解していきました。 meals_controller.rb def create @meal = using_user.meals.build(meal_params) if @meal.save sent_image = File.open(meal_params['meal_image'].tempfile) @meal.update!(analyzed_foods: @meal.image_analysis(sent_image)) redirect_to edit_meal_path(@meal), success: t('.success') まずはコントローラーでアップロードされた画像(画像を属性にもつ食事オブジェクト)が保存された場合、4行目で画像を取得し、5行目のimage_analysisメソッドに渡します。 meal.rb def image_analysis(meal_image) image_annotator = Google::Cloud::Vision.image_annotator translate = Google::Cloud::Translate::V2.new response = image_annotator.label_detection( image: meal_image ) results = [] response.responses.each do |res| res.label_annotations.each do |label| translation = translate.translate label.description.downcase, to: 'ja' results << translation.text end end results.join(',') image_analysisメソッドはモデルのインスタンスメソッドとして定義されております。 meal.rbの2行目と3行目でそれぞれ「Google Cloud Vision API」と「Google Translate API」にメソッドを呼び出しただけでリクエスト通信できるようになるオブジェクトを作りました。これはGoogleの提供するGemを使用することで実現しました。(APIキーなどの設定は別途で記述する必要あり) 次に5~7行目で先ほどのオブジェクトのメソッドを呼び出すことで「Google Cloud Vision API」に通信します。引数の画像であるmeal_imageを飛ばしてその画像に何が写っているかなど複数の情報が言葉でコレクションの形でかえってきます。コントローラーで渡したsent_imageを実際の挙動では解析しています。 そこから下の部分では、まず上の結果をeachメソッドで全ての結果を一つひとつ取り出し、使いたい部分だけ取り出して再度eachメソッドを使います。この最後に絞られた一つひとつを「Google Translate API」の機能で日本語に翻訳します。これも最初に作ったオブジェクトのメソッドを呼び出すだけで実行しています。 その結果を空の配列resultsに入れていき、最後にjoinメソッドで文字列にします。 文字列にする理由はデータベースに保存するためです。なぜならGoogleの解析結果が保存されていないと、後に紹介する食品選択ページに遷移されるたびに同じ写真を毎回APIに飛ばす必要が出てしまうからです。そうなってしまうと外部と通信するためリロード時間が毎回かかったり、APIの使用回数が無駄に上がってしまうからです。 meals_controller.rb def create @meal = using_user.meals.build(meal_params) if @meal.save sent_image = File.open(meal_params['meal_image'].tempfile) @meal.update!(analyzed_foods: @meal.image_analysis(sent_image)) redirect_to edit_meal_path(@meal), success: t('.success') そしてコントローラーに戻り、5行目で結果をanalyzed_foods属性に保存しています。 6行目で食品選択ページに遷移します。 食品選択ページでは食べた食品の候補の表(チェックボックス)を表示させます。 meals_controller.rb def edit @meal = using_user.meals.find(params[:id]) @concrete_foods = Food.searched_foods(@meal) 3行目のsearched_foodsというFoodクラスのクラスメソッドで候補の食品を検索、取得しています。 詳細はモデルに記述しており、 food.rb def self.searched_foods(meal) foods_from_foods = concrete.search_foods(meal.pass_to_sql) foods_from_genres = Genre.search_genres(meal.pass_to_sql).map(&:foods) (foods_from_foods + foods_from_genres).flatten.uniq end 2行目と3行目のpass_to_sqlはMealモデルのインスタンスメソッドであり、先ほど保存した結果の文字列を配列に戻しています。 search_foodsとsearch_genresメソッドは、解析結果の配列の全ての要素をfoodsテーブルとgenresテーブル内で検索するスコープです。 food.rb scope :search_foods, lambda { |analyzed_foods| where('name LIKE ? OR name LIKE ? OR name LIKE ? OR name LIKE ? OR name LIKE ? OR name LIKE ? OR name LIKE ? OR name LIKE ? OR name LIKE ? OR name LIKE ?', "%#{analyzed_foods[0]}%", "%#{analyzed_foods[1]}%", "%#{analyzed_foods[2]}%", "%#{analyzed_foods[3]}%", "%#{analyzed_foods[4]}%", "%#{analyzed_foods[5]}%", "%#{analyzed_foods[6]}%", "%#{analyzed_foods[7]}%", "%#{analyzed_foods[8]}%", "%#{analyzed_foods[9]}%") } こうすることで、解析結果をもとに当てはまる食品を検索、取得することができます。 genresテーブルでも検索しているのは、解析の精度がジャンル名で返ってくることが多いためです。例えば、ラーメンとは返ってこないけど、麺とは返ってくるようなイメージです。 取得できたらビューで表にして表示させて、終わりです。 edit.html.erb <table class="table table-hover table-bordered"> <thead> <tr> <th><%= Food.human_attribute_name(:name) %></th> <th>選択する/しない</th> </tr> </thead> <tbody> <%= f.collection_check_boxes :food_ids, @concrete_foods, :id, :name do |food| %> <%= food.label do %> <tr class="js-checkbox-<%= food.object.role %>"> <td><%= food.object.name %></td> <td><%= food.check_box class: 'js-chk' %></td> </tr> <% end %> <% end %> </tbody> </table> 表は実際の食品を選べるように、チェックボックスの表になるようtableタグとcollection_check_boxesメソッドを組み合わせました。 以上で無事、画像投稿→食品候補リスト表示までの流れの実装を終わりました。 工夫したところ ジャッジをしたら自動でアプリ内に共有されるところ Chart.jsを使ってグラフで黒字、赤字の大小がわかりやすくなるよう実装 * 画像解析の精度をカバーするため候補の表に食品がないとき、抽象的な食品を選べるように実装 赤字の(糖質を取りすぎた)場合謝罪文を投稿できる機能。ネタにする意味をこめました。 終わりに 糖質収支ジャッジアプリ、使ってくださると嬉しいです! また、何かご意見、ご感想がございましたら、こちらのDMでくださると嬉しいです! https://twitter.com/udaudakohei/ ぜひ、皆さんのジャッジお待ちしております。
- 投稿日:2022-02-24T19:36:49+09:00
YouTubeDataAPIを用いた複数チャンネルのライブ配信ステータス取得において、リクエストの上限制限を回避する
概要 YouTubeDataAPIでは一日の上限Queries(呼び出し回数 × Cost)が制限されているため、「任意の配信者の動画一覧を取得し、その配信者が現在配信中の動画のリアルタイム情報(視聴者数など)を毎分APIを通して監視しよう」と考えるとすぐにAPI上限に達してしまう YouTubeが公開しているRSSフィード用のURL経由で最新動画1件のvideo_idを調べ、そのidの動画情報のみをAPIを用いて取得することで、Queries上限に達しないよう節約している 使用環境 Rails 6.0.4.1 YouTube Data API v3 MariaDb Ver 15.1 データベース 配信者テーブル (streamers) id title channel_code description 1 YouTubeアカウント名 チャンネルID チャンネル概要 2 27歳高学歴ニートさの UCmFq0wZGLfjtXdUINyOZ7hQ 毎日たのしく生きてたら大卒27歳職歴なしになってしまいました。 3 レンジの中に毒を持て UCfBjINK2PzS432RihFow99w ポーカーで生きる人間の業 動画テーブル (streamer_videos) id streamer_id title video_code audience 1 外部キー 動画タイトル 動画ID 視聴者数 2 2 最速で慶應ロースクールに落ちました UbHSJapfMSAM 258 3 3 Piosolverと配信デート DfG32VJMZV0 1120 対応方針 以下のRSSフィード用URLを用いて最新動画の動画IDを取得することで、「任意の配信者の動画一覧を取得する」というフローのAPI実行を削減している https://www.youtube.com/feeds/videos.xml?channel_id=チャンネルID 実装のイメージは以下です 本質的な処理だけ抽出しているので、実際のソースコードとは異なります。 youtube = Google::Apis::YoutubeV3::YouTubeService.new youtube.key = Rails.application.credentials.google[:api_key] Streamer.all.each do |streamer| # YouTubeが公開しているRSSフィード用のURLは以下 feed_url = "https://www.youtube.com/feeds/videos.xml?channel_id=#{streamer.channel_code}" xml = URI.open(feed_url) parsed_xml = Nokogiri::HTML.parse(xml, nil, 'utf-8') video_ids = parsed_xml.xpath('//entry/videoid').map{|v| v.text} # データベースに保存している最新動画と、RSSフィードで取得した最新動画を突き合わせる saved_latest = streamer.streamer_videos.order(created_at: :desc).first if saved_latest saved_index = video_ids.index(saved_latest.video_code) # 最新動画がすでに保存されているアーカイブ動画である場合は無視 next if saved_index == 0 && saved_latest.uploaded? end # 最新1件の動画に対して、動画情報を取得するAPI実行 response = youtube.list_videos('snippet, liveStreamingDetails, contentDetails', {:id => video_ids[0]}) video = StreamerVideo.find_or_initialize_by(video_code: response.items[0]) if video.new_record? video.streamer_id = streamer.id video.title = r.snippet.title video.description = r.snippet.description end case r.snippet.live_broadcast_content when "live" then # ライブ配信中の動画は、ここで毎分視聴者数を取得して更新している video.video_type = "live" video.audience = r.live_streaming_details.concurrent_viewers.to_i when "none" then # ライブ配信ではない動画は、ここでデータを登録して以降処理を行わない video.video_type = "uploaded" end video.save! end
- 投稿日:2022-02-24T15:28:17+09:00
【環境構築サクッとメモ】rails×reactとlaravel×vuejsとlaravelの参考メモ
新しい言語のキャッチアップをする時には環境構築が必要になります。 Dockerだったら簡単に構築出来ますよね。 他の人が作ったリポジトリを参考にすれば、早く環境構築が終わるので楽ちん。 以下をDockerで構築した記事をメモがわりに残しておきたいと思います。 ・rails×react ブログ 【Docker】Rails6 API + React(TypeScript)のSPA開発環境を構築する https://qiita.com/taki_21/items/613f6a00bc432d1c221d github https://github.com/taki-21/rails-react-ts-docker ・laravel×vuejs Qiita 【導入編】絶対に失敗しないDockerでLaravel + Vue.jsの開発環境(LEMP環境)を構築する方法〜MacOS Intel Chip対応〜 https://yutaro-blog.net/2021/04/28/docker-laravel-vuejs-1/ github https://github.com/shimotaroo/docker-laravel-vuejs-sample ・laravel Qiita Docker ComposeでLaravel開発環境を構築その壱 https://qiita.com/_bobtabo_/items/42b70d334dee33befdfb github https://github.com/bobtabo/docker2
- 投稿日:2022-02-24T11:38:14+09:00
【Ruby On Rails】radio_button:数値として扱いparamsを使って合計点数など計算する
環境 Mac(12.2.1) MacBook Pro (13-inch, 2020) 2 GHz クアッドコアIntel Core i5 16 GB 3733 MHz LPDDR4X ruby (3.0.0p0) rails (7.0.1) mysql2 (0.5.3) 実現したい内容 上記の質問内容に対してラジオボタンを押したときに はい=>2点 いいえ=>0点 どちらでもない=>1点 と、それぞれのラジオボタンを点数として取り扱う 更に、それらの点数を合計し、結果をDBに保存する 使用するメソッド ・ [form_with] ・ [radio_button] ・ [label] ・ [submit] ・ [params] viewには何を書く? viewではform_withを使用し、フォームを作成する form_withのネスト(入れ子)に「radio_button」、「label」を使用 ⇨「はい」、「いいえ」、「どちらでもないの」フォームの内容とそれらにあてはめたい点数(数値など)を入力 「submit」を使用し、DBに保存 controllerには何を書く? controllerではviewの入力情報を受け取り、その入力情報をもとに計算を行う 受け取りは「params」を使用 view名に該当するアクションを作成し、アクション内でparamsで受け取った情報を元に計算とDB保存を行う とある質問フォームを作った時の例 Sampleテーブル id result 1 4 2 2 viewの設定 app/views/sample/test.html.erb <%=form_with modele: @test,url: test_path, local: true,method: :post do |f|%> <li>ここに質問の文言を記述</li> <!--ラジオボタンの記述方法 f.radio_button :オブジェクト名(保存したいカラム名など) ,:ここに点数(数値)を設定--> #設問1つ目 <%= f.radio_button :total_1 , 2, checked: true %> <!--ラベルの記述方法:f.label :ラジオボタンで書いたオブジェクト名(保存したいカラム名など) ,"画面上で表示したいタイトル",value:ラジオボタンで書いた値 --> <%=f.label :total_1 ,"はい",value: 2 %> <%=f.radio_button :total_1 ,0%> <%=f.label :total_1 ,"いいえ",value: 0 %> <%=f.radio_button :total_1 ,1%> <%=f.label :total_1 ,"どちらでもない",value: 1 %> #設問2つ目 <%=f.radio_button :total_2 , 2, checked: true%> <%=f.label :total_2 ,"はい",value: 2%> <%=f.radio_button :total_2 ,0%> <%=f.label :total_2 ,"いいえ",value: 0%> <%=f.radio_button :total_2 ,1%> <%=f.label :total_2 ,"どちらでもない",value: 1%> <%=f.submit%> controllerの設定 controller側でラジオボタンの入力情報をparamsで受け取り(今回の場合はradio_buttonのvalueで設定した値)、計算する app/controllers/sample_controller.rb class SampleController < ApplicationController def create @total=params[:total_1].to_i + params[:total_2].to_i @total_result = Sample.create(result: @total) end ・ ・ ・ web画面上でsubmit(送信ボタン)を押すと、form_withのmethod: :postでcontrollerのcreateアクションが実行 結果 @tatalでフォーム内容をparamsで計算 @total_lesultでSampleテーブルのresultカラムに@totalの内容を保存 おまけ:web画面に結果を出したい場合 web画面に結果を出したい場合、 viewの記述の追加 viewのform_withのmethod: :postからmethod::getに変更 controllerの記述の追加 app/views/sample/test.html.erb <%=form_with modele: @test,url: test_path, local: true,method: :get do |f|%> <li><%= p @toatl%></li> app/controllers/sample_controller.rb class SampleController < ApplicationController def test @total=params[:total_1].to_i + params[:total_2].to_i end ・ ・ ・ おわりに 初学者で試し試し作っているので、間違っているところがあったらごめんなさい 参考 投稿データを保存しよう 【Rails】入門説明書 paramsについて解説 【Rails】form_withの使い方を徹底解説! 【Rails】form_with/form_forについて【入門】 【Rails】paramsがなんなのかやっとわかった! 【Rails】ラジオボタンを表示する方法 【JavaScript】ラジオボタンを利用してレコード内集計する 【JavaScript】ラジオボタンの診断テストで合計点数に応じてコメントを変える方法 【Rails】テーブルにデータを保存しよう 【Rails】 createメソッドの使い方とは?new・saveメソッドとの違い
- 投稿日:2022-02-24T11:38:14+09:00
【Ruby On Rails】radio_buttonを数値として扱いparamsを使って合計点数など計算する
環境 Mac(12.2.1) MacBook Pro (13-inch, 2020) 2 GHz クアッドコアIntel Core i5 16 GB 3733 MHz LPDDR4X ruby (3.0.0p0) rails (7.0.1) mysql2 (0.5.3) 実現したい内容 上記の質問内容に対してラジオボタンを押したときに はい=>2点 いいえ=>0点 どちらでもない=>1点 と、それぞれのラジオボタンを点数として取り扱う 更に、それらの点数を合計し、結果をDBに保存する 主に使用したメソッド ・ [form_with] ・ [radio_button] ・ [label] ・ [submit] ・ [params] viewには何を書く? viewではform_withを使用し、フォームを作成する form_withのネスト(入れ子)に「radio_button」、「label」を使用 ⇨「はい」、「いいえ」、「どちらでもないの」など、フォームに表示したい内容とそれらの値(今回は点数)を記述 「submit」を使用し、DBに保存 controllerには何を書く? controllerではviewの入力情報を受け取り、その入力情報をもとに計算を行う 受け取りは「params」を使用 view名(今回はapp/views/sample/test.html.erb)と同名のアクション(今回はtestアクション)を作成し、そのアクション内でparamsで受け取った情報を受け取り、合計計算とDBへの保存を行う とある質問フォームを作った時の例 Sampleテーブル id result 1 4 2 2 viewの設定 ラジオボタンの記述方法 f.radio_button :「オブジェクト名(保存したいカラム名など)」,:「ここに点数(数値)を設定」 ラベルの記述方法 f.label :「ラジオボタンで書いたオブジェクト名(保存したいカラム名など) 」, "「画面上で表示したいタイトル」",value:「ラジオボタンで書いた値」 app/views/sample/test.html.erb <%= form_with modele: @test,url: test_path, local: true,method: :post do |f| %> <li>ここに質問の文言を記述</li> #設問1つ目 <%= f.radio_button :total_1 , 2, checked: true %> <%= f.label :total_1 ,"はい",value: 2 %> <%= f.radio_button :total_1 ,0 %> <%= f.label :total_1 ,"いいえ",value: 0 %> <%= f.radio_button :total_1 ,1%> <%= f.label :total_1 ,"どちらでもない",value: 1 %> #設問2つ目 <%= f.radio_button :total_2 , 2, checked: true%> <%= f.label :total_2 ,"はい",value: 2%> <%= f.radio_button :total_2 ,0%> <%= f.label :total_2 ,"いいえ",value: 0%> <%= f.radio_button :total_2 ,1%> <%= f.label :total_2 ,"どちらでもない",value: 1%> <%= f.submit%> controllerの設定 controller側でラジオボタンの入力情報をparamsで受け取り(今回の場合はradio_buttonのvalueで設定した値)、計算する app/controllers/sample_controller.rb class SampleController < ApplicationController def create @total=params[:total_1].to_i + params[:total_2].to_i @total_result = Sample.create(result: @total) end ・ ・ ・ web画面上でsubmit(送信ボタン)を押すと、form_withのmethod: :postでcontrollerのcreateアクションが実行 結果 @tatalでフォーム内容をparamsで計算 @total_lesultでSampleテーブルのresultカラムに@totalの内容を保存 おまけ:web画面に結果を出したい場合 web画面に結果を出したい場合、 viewの追記 viewのform_withのmethod: :postをmethod: :getに変更 controllerの追記 app/views/sample/test.html.erb <!-- "viewのform_withのmethod: :postをmethod: :getに変更"--> <%=form_with modele: @test,url: test_path, local: true,method: :get do |f|%> ・ ・ ・ <!--viewの追記--> <li><%= p @toatl %></li> app/controllers/sample_controller.rb #controllerの追記 class SampleController < ApplicationController def test @total=params[:total_1].to_i + params[:total_2].to_i end ・ ・ ・ おわりに 初学者で試し試し作っているので、間違っているところがあったらごめんなさい 参考 【Rails】createメソッドの使い方とは?new・saveメソッドとの違い 【Rails】入門説明書 paramsについて解説 【Rails】paramsがなんなのかやっとわかった! 【Rails】form_withの使い方を徹底解説! 【Rails】form_with/form_forについて【入門】 【Rails】ラジオボタンを表示する方法 【Rails】テーブルにデータを保存しよう 【Rails】投稿データを保存しよう 【JavaScript】ラジオボタンを利用してレコード内集計する 【JavaScript】ラジオボタンの診断テストで合計点数に応じてコメントを変える方法
- 投稿日:2022-02-24T11:34:34+09:00
form_forにおけるsubmitの役割について
140 %> におけるf.submitの役割は @postの中身がPost.new(空の箱)の場合はcreateアクションに遷移する。 投稿内容を書き換えたい場合、 @postの中身を書き換えることになり、中身はPost.newではないので、その場合はupdateアクションに遷移する仕組みとなっている。
- 投稿日:2022-02-24T11:11:31+09:00
「Herokuの起動が重すぎる」を解決する方法
#はじめに 現在オリジナルアプリで朝散歩習慣化アプリを作っています。 Herokuにデプロイしているのですが、立ち上がりがとにかく遅くて使い勝手が悪い。 今回はHerokuの立ち上がりを早くする方法について学んだのでまとめます。 #なぜ起動が遅くなるのか Herokuの立ち上がりが遅い原因は、「30分アクセスがないとスリープモードになる」という仕様があるためらしいです。 そのため、スリープにしないために設定を変える必要があります。 #Herokuにスケジューラを追加 まずはターミナルで下記のコマンドを打ちます。 % heroku addons:create scheduler:standard すると以下のような表示が出ます。 Creating scheduler:standard on ⬢ morning-walk... free To manage scheduled jobs run: heroku addons:open scheduler Created scheduler-flexible-28737 Use heroku addons:docs scheduler to view documentation #ジョブを作成する 続いてターミナルに以下のコマンドを打ちます。 % heroku addons:open scheduler すると以下のような画面が出ます。 「Create job」ボタンを押すと、次に以下のような画面が出ます。 この入力欄に ・Every 10 minutes ・curl https://morning-walk.herokuapp.com/ (curlの後は、自分のアプリのURLを記載してください) ・free と入力すると完了です。 ここまでで、 設定したHerokuアプリを10分毎に起動させる処理にできました。 #まとめ Herokuでアプリを作ると、とにかく起動が重いですが、 このような方法があると知れてよかったです。 今後は、Herokuではない別の環境でアプリを実行させたいと思います。 (これから情報収集するのですが、Heroku以外でおすすめの環境があれば教えていただけると嬉しいです?) 参考文献↓ https://yukitoku-sw.hatenablog.com/entry/2020/02/04/225151
- 投稿日:2022-02-24T02:30:15+09:00
【Rails】macのrailsサーバーに他の端末からアクセスする方法
はじめに 本記事は、プログラミング初学者が、学習を進めていて疑問に思った点について調べた結果を備忘録も兼ねてまとめたものです。 そのため、記事の内容に誤りが含まれている可能性があります。ご容赦ください。 間違いを見つけた方は、お手数ですが、ご指摘いただけますと幸いです。 macのrailsサーバーにiPad等でアクセスする方法 macローカルでrailsウェブサービスを作ってて、スマホ実機でテストしたいとき。 エミュレータでもいいけど、実機じゃないとテスト出来ないときってありますよね... macと他の端末を同じWi-Fiに繋げる macと他の端末を同じWi-Fiに繋げます。 macのローカルIPアドレスを確認する GUIで確認する方法 アップルメニューから「システム環境設定」を選択。 「ネットワーク」を選択。 ローカルIPアドレスを確認 画面左側から選択して、IPアドレスを確認します。 右側の赤枠の箇所に表示されているのがローカルIPアドレスです。 CLIで確認する方法 ターミナルにで以下のどちらかを実行します。 ターミナル $ ifconfig | grep inet 又は $ ifconfig | grep broadcast 表示された結果のinetの後の数字がローカルIPアドレスになります。 IPアドレスを指定してrailsサーバーを立ち上げる 以下の通りターミナルで実行し、上記で確認したローカルIPアドレスを指定してrailsサーバーを立ち上げます。 デフォルトでは、127.0.0.1で立ち上がります。 ターミナル rails s -b 192.168.10.105 上記を実行することで、IPアドレス192.168.10.105、ポート番号3000で立ち上がります。 ポートも指定したい場合には-p 3001等を上記のコマンドの後ろにつけ実行してください。 他の端末のブラウザからアクセスする ブラウザのアドレスバーに以下のような形でURLを入力しすると他の端末からアクセスすることができます。 http://192.168.10.105:3000/ URLはターミナルに出力されるのでそれをコピー&ペーストする等してください。