20201006のRubyに関する記事は25件です。

ActiveStorageを使って、画像投稿機能実装までの流れ

はじめに

 よくSNSで画像をあげるような機能があるが、それをRailsで実装するときに使うGemを紹介する。長くなるため、今回は実装の準備段階に絞って投稿する。
全2回の予定。

Active Storageとは

 現在はGemとしてインストールしなくても、Railsに搭載されている。画像などのファイルのアップロードを簡単に行えるメソッドが使えるようになる、画像を保存するテーブルの作成も簡単に行える。

画像アップロード機能の実装までの流れ

  1. ImageMagickのインストール
  2. 2つのGemのインストール
  3. ローカルサーバーの再起動
  4. Active Strageのインストール
  5. テーブルの生成

1.ImageMagickのインストール

 そもそもImageMagickとは、画像加工ツールであり、Gemではなく、ソフトウェアの部類になる。Homebrewからインストールを行う場合は、

brew install imagemagick

 ImageMagickだけでは、Rubyで扱えないので、次の2つのGemをインストールする必要がある。

2.2つのGemのインストール

(1)MiniMagick
 ImageMagickの機能がRubyで使えるようになる。
(2)ImageProcessing
 MiniMagickだけではできない、画像のサイズの調整をする。

Gemfile
gem 'mini_magick'
gem 'image_processing', '~>1.2' #バージョンの指定

Gemfileのいちばん下でOK。
 
記述をしたら、忘れずに、ターミナルで、

bundle install

3.ローカルサーバーの再起動

rails s

Gemを新たにインストールしたときは、忘れずに。

4.Active Storageのインストール

晴れて、ActiveStorageが使えるようになったので、
ターミナルを使って、インストール

rails active_storage:install

インストールすると、マイグレーションファイルが自動で生成される。

5.テーブルの生成

特にカラムの変更がなければ、そのまま、

rails db:migrate

このマイグレーションによって、2つのテーブルが生成されることを確認。

ポイント

  •  ActiveStorageを使って画像アップロード機能を実装する。
  •  Rubyで使えるようにするために、2つのGemをインストールする。

最後に

次回、画像の保存方法、保存した画像の表示方法についてまとめる。

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

本日の『railsチュートリアル』で詰まったところ(2020/10/06)

詰まったところ

1.3.4 Hello, world!

・事象:$ git push heroku master実行後、URLを開くとMethod Not Allowedと表示される。
・解決方法: 開くURLが間違っていた。
image.png

・留意点
Heroku導入は難しくて理解を飛ばしたため、振り返る必要あり。

感想

・1章読了!
・pushとcommitを頭の中でイメージできるようになってきた。
・もしかしてQiitaの使い方間違ってる?日付毎に新しい記事を量産するのではなく、1記事に追記していく形がいいのかもしれない。Qiitaルール要確認。
・Qiitaのマークダウン記法は読んだので多少記事が見やすくなったはず。

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

ヘッダーやフッターを一度作って別のページでも流用する方法

フッターやヘッダーを作ったら他のページにも使いたい!

ポートフォリを制作中
フッターやヘッダーをページが変わるたびに一から作り直すのかなり面倒だなと感じ調べたらすぐに出てきたので共有させてください!

結論

スクリーンショット 2020-10-06 22.08.16.png

このapp > layouts >application.html.erb
の中にある

application.html.erb
#開くとすでに記述してあります
<!DOCTYPE html>
<html>
  <head>
    <title>PhotoRoke</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
#ここにヘッダーの記述をする
    <%= yield %>
#ここにフッターの記述をする
</body>
</html>

<%= yield %>
をフッターとヘッダーで挟む感じで記述しましょう!

yieldメソッドって何?

レイアウトテンプレートに、各テンプレートファイルを展開するためのメソッドです。とのこと

レイアウトテンプレートとはRailsでいう先ほど編集した applocation.html.erb のことです。
Railsでいうと、ということは他の言語では違うファイルがレイアウトテンプレートなのかもしれません

yieldメソッドが何をしているか

すごく簡単にいうと
yield = トップページのHTML
yield = 新規投稿ページのTHML
yield = その他のページのHTMI
のようにyieldに各ページのHTMLが中に入る

終わり

以上になます!
手を抜くプログラミングを行っていきましょう
ご高覧いただきありがとうございました!

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

【No.008】ActionView::Componentを導入して、ナビバーを全画面に適応させる

github/view_component
[Rails 6.1] ActionView::Component について"
ActionView::Componentの導入

Issue
PR

Tailwind

概要

ActionView::Componentを導入して、ナビバーを全画面に適応させる

ToDo詳細

  • ActionView::Componentのインストール
Gemfile
gem "view_component", require: "view_component/engine"
Terminal
bundle install
  • navbar部分をcomponent化
Terminal
-> % bin/rails generate component Layout::Navbar org          
Running via Spring preloader in process 30401
      create  app/components/layout/navbar_component.rb
      invoke  test_unit
      create    test/components/layout/navbar_component_test.rb
      invoke  slim
      create    app/components/layout/navbar_component.html.slim

  • app/views/orders/ordering_org_sides/index.html.slimにnavbarのcomponentを適応
app/views/orders/ordering_org_sides/index.html.slim
.bg-gray-100
  = render(::Layout::NavbarComponent.new(org: @org))
  .py-10
    .max-w-7xl.mx-auto.sm:px-6.lg:px-8
      .py-5
        .-my-2.overflow-x-auto(class="sm:-mx-6 lg:-mx-8")
        .py-2.align-middle.inline-block.min-w-full(class="sm:px-6 lg:px-8")
          .shadow.overflow-hidden.border-b.border-gray-200(class="sm:rounded-lg")
            table.min-w-full.divide-y.divide-gray-200
              thead
                tr
                  th.px-6.py-3.bg-gray-200.text-left.text-xs.leading-4.font-medium.text-gray-500.uppercase.tracking-wider
                    | 注文No
                  th.px-6.py-3.bg-gray-200.text-left.text-xs.leading-4.font-medium.text-gray-500.uppercase.tracking-wider
                    | 商品画像
                  th.px-6.py-3.bg-gray-200.text-left.text-xs.leading-4.font-medium.text-gray-500.uppercase.tracking-wider
                    | 販売元ページ
                  th.px-6.py-3.bg-gray-200.text-left.text-xs.leading-4.font-medium.text-gray-500.uppercase.tracking-wider
                    | 買付先ページ
                  th.px-6.py-3.bg-gray-200.text-left.text-xs.leading-4.font-medium.text-gray-500.uppercase.tracking-wider
                    | 色・サイズ等
                  th.px-6.py-3.bg-gray-200.text-left.text-xs.leading-4.font-medium.text-gray-500.uppercase.tracking-wider
                    | 数量
                  th.px-6.py-3.bg-gray-200.text-left.text-xs.leading-4.font-medium.text-gray-500.uppercase.tracking-wider
                    | お届け先
                  th.px-6.py-3.bg-gray-200.text-left.text-xs.leading-4.font-medium.text-gray-500.uppercase.tracking-wider
                    | 買付費用
                  th.px-6.py-3.bg-gray-200.text-left.text-xs.leading-4.font-medium.text-gray-500.uppercase.tracking-wider
                    | ステータス
                  th.px-6.py-3.bg-gray-200.text-left.text-xs.leading-4.font-medium.text-gray-500.uppercase.tracking-wider
                    | 編集
              tbody.bg-white.divide-y.divide-gray-200
                - @orders.each do |order|
                  tr
                    td.px-6.py-4.whitespace-no-wrap.text-sm.leading-5.text-gray-500
                      = order.trade_no
                    td.px-6.py-4.whitespace-no-wrap.text-sm.leading-5.text-gray-500
                      | 商品画像
                    td.px-6.py-4.whitespace-no-wrap.text-sm.leading-5.text-gray-500
                      a.text-indigo-600.hover:text-indigo-900 href="#"  販売元ページ
                    td.px-6.py-4.whitespace-no-wrap.text-sm.leading-5.text-gray-500
                      a.text-indigo-600.hover:text-indigo-900 href="#"  買付先ページ
                    td.px-6.py-4.whitespace-no-wrap.text-sm.leading-5.text-gray-500
                      = order.color_size
                    td.px-6.py-4.whitespace-no-wrap.text-sm.leading-5.text-gray-500
                      = order.quantity
                    td.px-6.py-4.whitespace-no-wrap.text-sm.leading-5.text-gray-500
                      |#{order.postal}
                      br
                      = order.address
                    td.px-6.py-4.whitespace-no-wrap.text-sm.leading-5.text-gray-500
                      | $100
                    td.px-6.py-4.whitespace-no-wrap.text-sm.leading-5.text-gray-500
                      = order.status_i18n
                    td.px-6.py-4.whitespace-no-wrap.text-right.text-sm.leading-5.font-medium
                      a.text-indigo-600.hover:text-indigo-900 href="#"
                        i.fas.fa-edit
  • app/views/orgs/index.html.slimにnavbarのcomponentを適応
app/views/orgs/index.html.slim
= render(::Layout::NavbarComponent.new(org: @org))
.bg-white.shadow.m-auto.sm:rounded-md.mt-5(class="w-2/4")
  ul
    - @orgs.each do |org|
      / TODO:Sassで場合分けできるようにする
      - border_t = org == @orgs.first ? '' : 'border-t border-gray-200'
      li.(class=border_t)
        = link_to [org], class: 'block hover:bg-gray-50 focus:outline-none focus:bg-gray-50 transition duration-150 ease-in-out' do
          .flex.items-center.px-4.py-4.sm:px-6
            .min-w-0.flex-1.flex.items-center
              .min-w-0.flex-1.px-4.md:grid.md:grid-cols-2.md:gap-4
                div
                  .text-sm.leading-5.font-medium.text-indigo-600.truncate
                    = org.name
              div
                i.fas.fa-sign-in-alt.fa-lg.bg-gray-50.text-gray-500
  • app/views/orgs/show.html.slimにnavbarのcomponentを適応
app/views/orgs/show.html.slim
= render(::Layout::NavbarComponent.new(org: @org))
.bg-white.shadow.m-auto.mt-5.overflow-hidden.sm:rounded-lg(class="w-1/2")
  .px-4.py-5.border-b.border-gray-200.sm:px-6
    h3.text-lg.leading-6.font-medium.text-gray-900
      |  会社詳細
    p.mt-1.max-w-2xl.text-sm.leading-5.text-gray-500
      |  会社詳細を説明します。
  div
    dl
      .bg-gray-100.px-4.py-5.sm:grid.sm:grid-cols-3.sm:gap-4.sm:px-6
        dt.text-sm.leading-5.font-medium.text-gray-500
          |  会社名
        dd.mt-1.text-sm.leading-5.text-gray-900.sm:mt-0.sm:col-span-2
          = @org.name
      .bg-white.px-4.py-5.sm:grid.sm:grid-cols-3.sm:gap-4.sm:px-6
        dt.text-sm.leading-5.font-medium.text-gray-500
          |  会社タイプ
        dd.mt-1.text-sm.leading-5.text-gray-900.sm:mt-0.sm:col-span-2
          / TODO:enum化する
          = @org.org_type

動作確認

準備

bin/rails db:migrate:reset
bin/rails db:reset

受入基準

  • 下図のように全画面にnavbarが適応されている

※ リンクはめちゃくちゃです。
※ ドロップダウンのJSも後日対応予定です。
e8254b70ac4995008a9457259fc751ce

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

Ruby 3.0 で x ** 2 が速くなった件

x ** 2 は遅い

変数 x に Integer とか Float とかの数値が入っているとする。
x の 2 乗の計算は,

x ** 2

と書けるが,もしできるだけ高速に計算させたいなら

x * x

と書いたほうがいい。
後者のほうが断然速い。
というか前者はめちゃんこ遅い。

ベンチマークテスト

まず

gem install benchmark_driver

して benchmark_driver をインストールしておき,

square.yaml
prelude: |
  x = 3.0
benchmark:
  - "x ** 2"
  - "x * x"

という YAML ファイルを用意して,

benchmark-driver square.yaml

とやると比較できる。

ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin17] で計測してみたところ,結果は以下のとおり。

     x * x:  92940175.9 i/s 
    x ** 2:  15605744.0 i/s - 5.96x  slower

わお!

まあ,遅いったって,毎秒 1500 万回実行できるわけだから,この遅さが問題になるのは大量の数値計算をやる場合に限られるわけだけれど1

Ruby 3.0 で改善

Float に対する ** 2 を特別扱いして速く計算するような変更が入った。
Ruby 3.0.0-preview1 で確認できる。

rbenv で Ruby を入れている方は benchmark_driver で簡単にテストできる。
さきほどと同じ YAML ファイルに対して,こんどは

benchmark-driver square.yaml --rbenv "2.7.2;3.0.0-preview1"

とsる。
結果は以下のとおり。

                       x ** 2
  3.0.0-preview1:  37039739.4 i/s 
           2.7.2:  16010580.7 i/s - 2.31x  slower

                        x * x
           2.7.2:  91881050.6 i/s 
  3.0.0-preview1:  88138626.5 i/s - 1.04x  slower

x * x で,むしろ 3.0.0-preview1 のほうが遅いが,これは誤差の範囲。
x ** 2 のほうは,2.3 倍速になった。

感想

まあそれでも x * x のほうが x ** 2 より倍以上速いわけだけど,それでも重要な進化だと思う。
世の中に x ** 2 のようなコードはたくさんあって,それらが何もしなくても速くなってくれるわけだから。

場合によっては速度を犠牲にしてあえて ** 2 を使いたいときもある。
それは 2 乗したいものが変数に入っていなくて,式のとき。
まあ,こういうイメージかな:

items.map.with_index{ |item, i| (a * item.value + b[i]) ** 2 }

これを

items.map.with_index{ |item, i| (a * item.value + b[i]) * (a * item.value + b[i]) }

などとすると,冗長なばかりか,おそらくかえって速度を落とすことになる。
そこで,いったん式の値を変数に入れて

items.map.with_index{ |item, i| x = a * item.value + b[i]; x * x }

と書くことになる。
速度が最優先ならこう書くところだが,「速度もそこそこ求めるけど簡潔さも欲しい」といった場合は ** 2 を使うこともあるだろう。


  1. プログラムを高速化したいとき,ベンチマークテストをやってきちんと数字を追うことが大事なのと,プログラム全体の実行時間を左右しない箇所の最適化に血道を上げても無意味,ということは肝に銘じておきたい。 

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

ActionView::Componentの導入

github/view_component
[Rails 6.1] ActionView::Component について
PR

準備

Terminal
rails new view_component_app -d postgresql

環境

Gemfile
ruby '2.7.1'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.0.3', '>= 6.0.3.3'
# Use postgresql as the database for Active Record
gem 'pg', '>= 0.18', '< 2.0'

Slim入れる

Gemfile
gem 'slim-rails'
gem 'html2slim'
Terminal
bundle install

Tailwind入れる

ref: tailwindcss Documentation

1.Install Tailwind via npm

# Using npm
npm install tailwindcss

# Using Yarn
yarn add tailwindcss

2.Add Tailwind to your CSS

app/javascript/src/scss/application.scssを追加

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

app/javascript/packs/application.jsに下記追加

import '../src/scss/application.scss'

3.Create your Tailwind config file (optional)

npx tailwindcss init

4.Process your CSS with Tailwind

postcss.config.jsに下記追加

module.exports = {
  plugins: [
    // ...
    require('tailwindcss'),
    require('autoprefixer'),
    // ...
  ]
}

Scaffold

Terminal
bin/rails g scaffold blog content:text
routes.rb
Rails.application.routes.draw do
  root 'blogs#index' #追加
  resources :blogs
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

DB作成

Terminal
bin/rails db:create
bin/rails db:migrate

ActionView::Componentの導入

インストール

Gemfile
gem "view_component", require: "view_component/engine"
Terminal
bundle install

サンプル1

コンポーネントを用意

Terminal
-> % bin/rails generate component Example title content
Running via Spring preloader in process 24985
      create  app/components/example_component.rb
      invoke  test_unit
      create    test/components/example_component_test.rb
      invoke  slim
      create    app/components/example_component.html.slim
app/components/example_component.rb
class ExampleComponent < ViewComponent::Base
  def initialize(title:, content:)
    @title = title
    @content = content
  end
end
app/components/example_component.html.slim
div
  = @title
div
  = @content

呼び出し元

app/views/blogs/index.html.slim
h1 Listing blogs

table
  thead
    tr
      th Content
      th
      th
      th

  tbody
    - @blogs.each do |blog|
      tr
        td = blog.content
        td = link_to 'Show', blog
        td = link_to 'Edit', edit_blog_path(blog)
        td = link_to 'Destroy', blog, data: { confirm: 'Are you sure?' }, method: :delete

br

= link_to 'New Blog', new_blog_path

= render(ExampleComponent.new(title: 'my title', content: 'my content'))

表示

image.png

サンプル2

コンポーネントを用意

Terminal
bin/rails generate component Test title
app/components/test_component.rb
class TestComponent < ViewComponent::Base
  def initialize(title:)
    @title = title
  end
end
app/components/test_component.html.slim
div
  span(title=@title)
    = content

呼び出し元

app/views/blogs/index.html.slim
h1 Listing blogs

table
  thead
    tr
      th Content
      th
      th
      th

  tbody
    - @blogs.each do |blog|
      tr
        td = blog.content
        td = link_to 'Show', blog
        td = link_to 'Edit', edit_blog_path(blog)
        td = link_to 'Destroy', blog, data: { confirm: 'Are you sure?' }, method: :delete

br

= link_to 'New Blog', new_blog_path

div.mt-3
  | サンプル1
  = render(ExampleComponent.new(title: 'my title', content: 'my content'))

div.mt-3
  | サンプル2
  = render(TestComponent.new(title: "my title")) do
    | Hellow, World!

表示

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

Ruby 2.7.2 がリリースされ、今後は非推奨な警告はデフォルトでは出なくなる

ブログ記事からの転載です。

先日 Ruby 2.7.2 がリリースされました。

このリリースで先日お伝えした『非推奨な警告がデフォルトでは出なくなる』ようになりました。
なので、今後は非推奨な機能が削除されるといきなりアプリケーションが動作しなくなる可能性があります。
このような問題を回避するために今後は以下のようにして『明示的に非推奨な警告が出力されるようする』ことで安全に開発する事ができます。

  • -w-W:deprecated を付けて Ruby を実行す
  • コード上に Warning[:deprecated] = true を追記する

参照

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

フリマアプリ作成におけるJSを用いたカテゴリ選択フォームの実装

はじめに

某プログラミングスクールの最終課題でフリマアプリのクローンを作成中です。
商品出品機能に必要な「カテゴリ選択機能」の実装において作業した内容について記載していきます。

  • JavaScriptを用いたプルダウン選択フォームの作成

※この前段階として、ancestryを使って使用するカテゴリデータのテーブルを作っています。
その記事はコチラ▶︎ancestryを用いた階層型カテゴリデータの作成方法

この記事でわかること

  • Ajax通信で3段階のカテゴリ選択フォームを表示させる

この機能で達成したいゴール

下記のように親要素(レディース)選択したら、子要素の選択フォームが、子要素を選択したら孫要素の選択フォームが非同期通信で表示されるようにします。
また、子や孫の選択肢を変えたらフォーム内容が初期化されたり、フォームそのものが消えたりします。
Image from Gyazo

開発環境

  • ruby 2.6.5
  • rails 6.0.3.2
  • sequel pro

大まかな流れ

  1. gemのインストール
  2. JSを使うためのコントローラー、JSファイルを作成し、呼び出しの記述をする。
  3. ルーティングを追加する。
  4. コントローラーにデータを検索する#search機能を設定する。
  5. jsファイルを編集。
  6. Ajax通信時に用いるjson.jbuilderファイルを編集。
  7. 上記4−6を子要素、孫要素で繰り返す。
  8. 子要素・孫要素を変更した際のアクションを追加する。
  9. 完成

前提として、
- ancestryを用いたデータ作成は完了していることとします。
 *ancestryを使ったカテゴリデータ作成方法はコチラをクリック
- itemsコントローラーも作成済とする。

具体的な実装手順

0. gemのインストール

まずは必要なgemをインストール。

gem jquery-rails
bundle install

1.JavaScriptを使うための記述をする

app/javascript/packs/application.js
require('jquery')
require('item/category')
config/webpack/environment.js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')

environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    jquery: 'jquery',
  })
)

module.exports = environment

ここで早速つまづいたのが、わたしの開発環境がrails6だったこと。
ネットで調べた記事のほとんどがrails5以前での情報になっており、
rails6ではapplication.jsファイルの生成場所や呼び出しのための記述が違うらしく、
最初うまく読み込まれなくて混乱しました。

開発の際はバージョンを確認の上、各自の環境にあわせて記述してください。

rails6でのjquery導入はこの記事を参考にさせていただきました。=>rails6でのjqueryの導入方法

2.ルーティングを追加

のちほどitemsコントローラーにsearchアクションを追加するため、
searchアクションへのルーティングを追記します。

routes.rb
Rails.application.routes.draw do
  resources :items do
    collection do
      get :search
    end
  end
end

3.ビューにカテゴリ選択フォームを作成

こちらは、あくまで私の記述なの参考までに。
このような形でまずは親カテゴリ用のフォームを作成しました。

qpp/view/items/new.html.haml
=form_with model @item do |f|
= f.collection_select :category_id, @parents, :id, :name, {prompt: "選択してください"}, {class: "categoryForm", id: "category_form"}

4.コントローラーにsearchアクションを追加

newアクションでレコードを取得。

items_controller
  def new 
    @item = Item.new
    @item_image = ItemImage.new
  end

また、newアクションとcreateアクションの前に親要素の情報を取得しておきたいので、before_actionを設定しました。

ite,s_controller.rb
  before_action :set_parents, only: [:new, :create]

  def set_parents
    @parents = Category.where(ancestry: nil)
  end

ancestry=nil、つまり親要素の値を取得し、変数@parentsへ代入しています。
newだけではなく、createの時にもset_parentsが必要な理由は、
コチラの記事を参考にさせていただきました。
(正直、コード書いている時はわかったような、わからないような...でした。)



そして、子・孫のカテゴリ検索のため、searchアクションを設定します。

items_controller.rb
  def search
    respond_to do |format|
      format.html
      #ajax通信開始
      format.json do
          #子カテゴリの情報を@childrensに代入
          @childrens = Category.find(params[:parent_id]).children
      end
    end
  end

余談ですが、複数形の"children"に更に"s"を付けるのにどうしても違和感があったのですが、
childとchildrenを使いわけるのもわかりにくいなぁと思い、仕方なくchildrensにしました。

5.json.jbulderファイルを作成

app/view/items/search.json.jbuilder
json.array! @childrens do |children|
  json.id children.id
  json.name children.name
end

とりあえず、いまは子要素の分のみ記載しています。

6.jsファイルを編集し、上記アクションで送られてくるデータを受け取る

1.まずは、親要素を選択した後に現れる「子要素の選択フォーム」を作成。

category.js
// 子要素を選択するフォーム
function add_childSelect_tag() {
  let child_select_form = `
              <select name="item[category_id]" id="item_category_id" class="child_category_id">
              <option value="">カテゴリを選択</option>
              </select>
            `
  return child_select_form;
}

このHTMLの部分を書くのが苦手なのですが、ビューの検証ツールで親要素のフォームをみて書くのが一番簡単な気がしています。

name="item[category_id]はデータの送り先です。

あとで、子要素のフォームを消したり初期化したりしたい(親要素を変更したときなど)ので、子要素特有のclass名も追加しました。


2.表示された選択フォームにデータを取得するためのoptionを記載。

category.js
function add_Option(children) {
  let option_html = `
                    <option value=${children.id}>${children.name}</option>
                    `
  return option_html;
}

 

3.親カテゴリを選択した後に起こるイベントを設定する。

category.js
//親カテゴリを選択したあとのイベント
$("#category_form").on("change", function() {
  let parentValue = $("#category_form").val();
  if (parentValue.length !== 0) {
    $.ajax({
      url: '/items/search',
      type: 'GET',
      data: { parent_id: parentValue},
      dataType: 'json'
    })
    .done(function (data){
      let child_select_form = add_childSelect_tag
      $(".ItemInfo__category--form").append(child_select_form);
      data.forEach(function(d){
        let option_html = add_Option(d)
        $(".child_category_id").append(option_html);
      });
    })
    .fail(function (){
      alert("カテゴリ取得に失敗しました");
});

おおまかな流れとしては、
1. 親要素のデータを取得し、parentValueへ代入。
2. if〜でデータが初期値でなければajax通信。
3. .doneでappendを使って、親要素のフォームの後に前手順で作成した子要素のフォームを表示させる。
4. optionでとりだしたデータをひとつずつ取り出して表示させる。

これで子要素の表示は完成!
親要素を選択すれば、子要素フォームが表示されます。

4.孫要素の表示を作成する。

基本的には、子要素の手順を繰り返すだけです。

app/views/items/search.json.jbuilder
json.array! @grandchildrens do |grandchildren|
  json.id grandchildren.id
  json.name grandchildren.name
end
category.js
// 孫要素の選択フォーム
function add_grandchildSelect_tag(){
  let grandchild_select_form = `
              <select name="item[category_id]" id="item_category_id" class="grandchild_category_id">
              <option value="">カテゴリを選択</option>
              </select>
            `
  return grandchild_select_form
}
categroy.js
// 子カテゴリを選択後のイベント
$(document).on("change", ".child_category_id", function(){
  let childValue = $(".child_category_id").val();
  if (childValue.length !=0){
    $.ajax({
      url: '/items/search',
      type: 'GET',
      data: { children_id: childValue},
      dataType: 'json'
    })
    .done(function (gc_data){
      let grandchild_select_form = add_grandchildSelect_tag
      $(".ItemInfo__category--form").append(grandchild_select_form);
      gc_data.forEach(function (gc_d){
        let option_html = add_Option(gc_d);
        $(".grandchild_category_id").append(option_html);
      })
    })
    .fail(function (){
      alert("カテゴリ取得に失敗しました");
    });
})

これで、子要素を選択すれば孫要素の選択フォームがでてくるようになります。

5.フォームを初期化する記述を追加する。

1-4の手順で一見完成したように見えるのですが、
このままだと以下のような不具合がでます。
- 親や子カテゴリを変更しても子カテゴリの内容がそのまま。
- 子や孫カテゴリを変更すると、孫の下に更に新たなカテゴリが出現してしまう。

要するに、子や孫カテゴリをいじっていると、エンドレスでフォームが追加されていってしまう状態。

なので、望ましい挙動として、

  • 親カテゴリを変更したら、孫カテゴリフォームは消えて、子カテゴリのフォームの内容は親カテゴリに紐づいた内容に変わる。
  • 子カテゴリを変更したら、孫カテゴリの内容は子カテゴリに紐づいたデータに変わる。
  • 親や子カテゴリを初期値(選んでいない状態)にしたら、その次の階層のフォームは消える。

この動作を実現させないといけません。
そのために、下記の記述を加えました。

category.js
// 親カテゴリを選択したあとのイベント
$("#category_form").on("change", function() {
  let parentValue = $("#category_form").val();
  if (parentValue.length !== 0) {
    $.ajax({
      url: '/items/search',
      type: 'GET',
      data: { parent_id: parentValue},
      dataType: 'json'
    })
    .done(function (data){
      $(".child_category_id").remove();
      $(".grandchild_category_id").remove();
      let child_select_form = add_childSelect_tag
      $(".ItemInfo__category--form").append(child_select_form);
      data.forEach(function(d){
        let option_html = add_Option(d)
        $(".child_category_id").append(option_html);
      });
    })
    .fail(function (){
      alert("カテゴリ取得に失敗しました");
    })
  }else{
    $(".child_category_id").remove();
    $(".grandchild_category_id").remove();  
  }
});

追加したのは、①.doneのあとのこの2行と

      $(".child_category_id").remove();
      $(".grandchild_category_id").remove();

②.failのあとにelseで条件分けしたこの部分です。

else{
    $(".child_category_id").remove();
    $(".grandchild_category_id").remove();  
  }

①で親カテゴリの内容が変わるたびに子/孫カテゴリを一度消し、その上で子カテゴリを出現させるようにしました。
②のelse文の後ろは、親が初期値を選択した場合。

同じコードを子カテゴリ選択後のイベント部分にも追記しました。

そして、全部ページを読み込んでからこれらのJSを作動させるため、

category.js
window.addEventListener('load', function () {
}

これでコード全体を括りました。

これで本当に完成!!



このあと、うっかりチームメンバーに「db:seedしてね」と言うのを忘れ、
「カテゴリ選択できない!!なぜ?!」という事態もおきましたが、無事に解決にいたりました。

参考にした記事

[Rails]某フリマアプリのカテゴリー機能の実装方法
フリマアプリのカテゴリ機能〜gem : ancestryを使用〜
[Rails] Ajax通信を用いたカテゴリボックス作成

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

アクション内でrenderを使わずにバリデーションエラーや入力していた内容を前ページに反映する方法

はじめに

フォーム内容を送信する際、バリデーションに引っ掛かったら
renderメソッドで新規投稿画面をレンダリングして
そこでエラーなり入力していた内容なりを表示すると学習した方が多いはず

しかし、renderメソッドが使えない時はどうしますか?
実は制作中のポートフォリオにGoogleMapAPIを使用しているのですが
renderでページをレンダリングするとなぜかMAPが表示しなくなる不具合が発生...

仕様を変えれば解決できる問題だったのですが、変えたくなかったので
renderを使わずに上記の動きを実現する方法を紹介します。

通常renderを使った場合

例としてtasks_controller.rb
新規投稿ページのアクションnewと、
新規投稿内容を保存するアクションcreateがあるとします。

tasks_controller.rb
def create
  @task = Task.new(task_params)
  if @task.save
    redirect_to @task, notice: 'タスクを保存しました。'
  else
    render :new
  end
end

@task.saveが失敗しelse以下のrender :newが実行され、
新規投稿ページがレンダリングされます。
あとはもうご存知のとおり、viewのほうで@task.errors.full_messagesを活用してく感じですね。

renderを使わない場合

redirect_toで再アクセスをするという形をとり、
エラー文や入力内容はアクション内でflashに格納してしまいます。

tasks_controller.rb
def create
  @task = Task.new(task_params)
  if @task.save
    redirect_to @task, notice: 'タスクを保存しました。'
  else
    flash[:error_msgs] = @task.errors.full_messages
    flash[:tmp_body] = @task.body
    redirect_to new_task_url
  end
end

flash[:error_msgs]にはエラー文を、
flash[:tmp_body]には文章内容を、それぞれ格納しておきます。

あとはアクセス先のviewで、flashの値を活用してすればOKです。
以上!

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

ac-library-rb で解く AtCoder 優先度付きキュー(Priority Queue)

はじめに

AtCoder Problems の Recommendation を利用して、過去の問題を解いています。
AtCoder さん、AtCoder Problems さん、ありがとうございます。

今回のお題

AtCoder Beginner Contest D - Powerful Discount Tickets
Difficulty: 826

AtCoder CODE FESTIVAL 2016 qual C B - K個のケーキ
Difficulty: 905

今回のテーマ、優先度付きキュー

ac-library-rb は、AtCoder Library (ACL)のRuby版です。

今回はその中の優先度付きキュー(Priority Queue)を使用しています。
JavaPythonですと標準ライブラリに優先度付きキューがありますが、Rubyにはないので待望のライブラリです。

D - Powerful Discount Tickets

ruby.rb
# frozen_string_literal: true

# Priority Queue
# Reference: https://github.com/python/cpython/blob/master/Lib/heapq.py
class PriorityQueue
  # By default, the priority queue returns the maximum element first.
  # If a block is given, the priority between the elements is determined with it.
  # For example, the following block is given, the priority queue returns the minimum element first.
  # `PriorityQueue.new { |x, y| x < y }`
  #
  # A heap is an array for which a[k] <= a[2*k+1] and a[k] <= a[2*k+2] for all k, counting elements from 0.
  def initialize(array = [], &comp)
    @heap = array
    @comp = comp || proc { |x, y| x > y }
    heapify
  end

  attr_reader :heap

  # Push new element to the heap.
  def push(item)
    shift_down(0, @heap.push(item).size - 1)
  end

  alias << push
  alias append push

  # Pop the element with the highest priority.
  def pop
    latest = @heap.pop
    return latest if empty?

    ret_item = heap[0]
    heap[0] = latest
    shift_up(0)
    ret_item
  end

  # Get the element with the highest priority.
  def get
    @heap[0]
  end

  alias top get

  # Returns true if the heap is empty.
  def empty?
    @heap.empty?
  end

  private

  def heapify
    (@heap.size / 2 - 1).downto(0) { |i| shift_up(i) }
  end

  def shift_up(pos)
    end_pos = @heap.size
    start_pos = pos
    new_item = @heap[pos]
    left_child_pos = 2 * pos + 1

    while left_child_pos < end_pos
      right_child_pos = left_child_pos + 1
      if right_child_pos < end_pos && @comp.call(@heap[right_child_pos], @heap[left_child_pos])
        left_child_pos = right_child_pos
      end
      # Move the higher priority child up.
      @heap[pos] = @heap[left_child_pos]
      pos = left_child_pos
      left_child_pos = 2 * pos + 1
    end
    @heap[pos] = new_item
    shift_down(start_pos, pos)
  end

  def shift_down(star_pos, pos)
    new_item = @heap[pos]
    while pos > star_pos
      parent_pos = (pos - 1) >> 1
      parent = @heap[parent_pos]
      break if @comp.call(parent, new_item)

      @heap[pos] = parent
      pos = parent_pos
    end
    @heap[pos] = new_item
  end
end

HeapQueue = PriorityQueue

n, m = gets.split.map(&:to_i)
a = gets.split.map(&:to_i)

h = PriorityQueue.new(a)
m.times do
  x = h.pop
  h.push(x / 2)
end
puts h.heap.sum

B - K個のケーキ

ruby.rb
# frozen_string_literal: true

# Priority Queue
# Reference: https://github.com/python/cpython/blob/master/Lib/heapq.py
class PriorityQueue
  # By default, the priority queue returns the maximum element first.
  # If a block is given, the priority between the elements is determined with it.
  # For example, the following block is given, the priority queue returns the minimum element first.
  # `PriorityQueue.new { |x, y| x < y }`
  #
  # A heap is an array for which a[k] <= a[2*k+1] and a[k] <= a[2*k+2] for all k, counting elements from 0.
  def initialize(array = [], &comp)
    @heap = array
    @comp = comp || proc { |x, y| x > y }
    heapify
  end

  attr_reader :heap

  # Push new element to the heap.
  def push(item)
    shift_down(0, @heap.push(item).size - 1)
  end

  alias << push
  alias append push

  # Pop the element with the highest priority.
  def pop
    latest = @heap.pop
    return latest if empty?

    ret_item = heap[0]
    heap[0] = latest
    shift_up(0)
    ret_item
  end

  # Get the element with the highest priority.
  def get
    @heap[0]
  end

  alias top get

  # Returns true if the heap is empty.
  def empty?
    @heap.empty?
  end

  private

  def heapify
    (@heap.size / 2 - 1).downto(0) { |i| shift_up(i) }
  end

  def shift_up(pos)
    end_pos = @heap.size
    start_pos = pos
    new_item = @heap[pos]
    left_child_pos = 2 * pos + 1

    while left_child_pos < end_pos
      right_child_pos = left_child_pos + 1
      if right_child_pos < end_pos && @comp.call(@heap[right_child_pos], @heap[left_child_pos])
        left_child_pos = right_child_pos
      end
      # Move the higher priority child up.
      @heap[pos] = @heap[left_child_pos]
      pos = left_child_pos
      left_child_pos = 2 * pos + 1
    end
    @heap[pos] = new_item
    shift_down(start_pos, pos)
  end

  def shift_down(star_pos, pos)
    new_item = @heap[pos]
    while pos > star_pos
      parent_pos = (pos - 1) >> 1
      parent = @heap[parent_pos]
      break if @comp.call(parent, new_item)

      @heap[pos] = parent
      pos = parent_pos
    end
    @heap[pos] = new_item
  end
end

HeapQueue = PriorityQueue

gets
a = gets.split.map(&:to_i)

h = PriorityQueue.new(a)

while h.heap.size > 1
  u = h.pop
  v = h.pop
  h.push(u - 1) if u - 1 > 0
  h.push(v - 1) if v - 1 > 0
end
if h.heap.size.zero?
  puts 0
else
  puts h.pop - 1
end

D - Powerful Discount Ticketsは、実行時間が 880 -> 395 msと速くなっていい感じですね。

まとめ

  • 優先度付きキュー(Priority Queue) を解いた
  • ACL に詳しくなった
  • Ruby に詳しくなった

参照したサイト
ac-library-rb - GitHub
Ruby と Python と Java で解く AtCoder ABC141 D 優先度付きキュー
Ruby と Python で解く AtCoder CODE FESTIVAL 2016 qual C B 優先度付きキュー

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

モモンガプロジェクトの開発者-最近

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

AWS SDK for Ruby profile変更 & assume role

AWS SDK for Rubyにてcredentialsのprofile指定とassume role方法についてわかりづらかったので記載

↓profileはaws-cliで下記のように作成した際のもの

$ aws configure --profile hoge

profile指定

Clientの引数に渡すだけ
例:EC2

ec2 = Aws::EC2::Client.new(
  profile: "hoge",
  # ...
)

assume role

require 'aws-sdk-core'
require 'aws-sdk-ec2'

role_credentials = Aws::AssumeRoleCredentials.new(
  client: Aws::STS::Client.new(opts),
  role_arn: "arn:aws:iam::xxxxxxxxxxxx:role/hoge_role",
  role_session_name: hoge
)

ec2 = Aws::EC2::Client.new(
  credentials: role_credentials,
  # ...
)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyOnRailsで環境変数を使用する方法

■英語語の記事↓
How to Use Environment Variables in Ruby On Rails

環境変数とは

各アプリケーションには、外部サービス用の電子メールアカウント資格情報やAPIキーなどの構成設定が必要です。環境変数を使用して、ローカル構成設定をアプリケーションに渡すことができます。
Ruby On Railsで環境変数を使用する方法はいくつかあり、FigaroGemのようなgemもあります。
この記事には、local_env.ymlファイルを使用して、実装の方法を共有させていただきます。

環境変数ファイルをプライベートとして保持する

GitHubを使用してコードを保存および共有していて、プロジェクトがオープンソースである場合、開発者は誰でもコードにアクセスできます。 個人情報やAPIキーを一般の人と共有したくない、プライベートgitリポジトリを使用してチームで共同作業している場合、ローカル設定がチームのすべてのメンバーに適しているとは限りません。

local_env.ymlファイルを使用:

標準のYAMLファイル形式を使用して、各環境変数のキーと値のペアを含む単純なファイルを作成します。

config/local_env.ymlファイルを作成:

MAIL_USERNAME: 'Your_Username'
MAIL_PASSWORD: 'Your_Username'

.gitignoreに設定
アプリケーションのgitリポジトリを作成した場合、アプリケーションのルートディレクトリには.gitignoreという名前のファイルが含まれている必要があります。
.gitignoreファイルに以下の行を追加

/config/local_env.yml

Railsアプリケーションファイルに設定
環境変数を設定後で、ファイル「local_env.yml」は「config/application.rb」に設定が必要です。
config/application.rbファイルに下記のコードを設定

config.before_configuration do
  env_file = File.join(Rails.root, 'config', 'local_env.yml')
  YAML.load(File.open(env_file)).each do |key, value|
    ENV[key.to_s] = value
  end if File.exists?(env_file)
end

上記のコードは、local_env.ymlファイルから環境変数を設定します。

コードに環境変数を使用
RailsアプリケーションでENV ["MAIL_USERNAME"]を使用することができます。
例:

ActionMailer::Base.smtp_settings = {
    address: "smtp.gmail.com",
    enable_starttls_auto: true,
    port: 587,
    authentication: :plain,
    user_name: ENV["MAIL_USERNAME"],
    password: ENV["MAIL_PASSWORD"],
    openssl_verify_mode: 'none'
    }

コーディングを楽しみましょう!:grinning: :grinning:

ご不明な点がございましたらご連絡して頂ければと思います。

以上です。よろしくお願いいたします。

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

【Rails】migrate時にAnnotateが実行されない

はじめに

Annotateが実行できず、コメントが書き出されない時に試したことをまとめました。

環境

  • Rails6.0.3
  • Ruby2.7.1
  • annotate3.1.1

Annotateとは

schemaに書かれている情報をルーティングの情報をファイルの先頭にコメントしてくれるgemです。
カラム情報やルーティングを確認する手間が省くことができます。
導入や詳細な使い方は下記を参考にしました。
【Rails】annotateの使い方

発生したバグ(migrate時にコメントが書き出されない)

migrate時にannotateが実行され、modelにコメントされる設定にしているのに、実行されないという問題が発生しました。
作業ブランチで1つ目のmigrationファイルを作成して、migrateした時はコメントが書き出されたのに、作業同じブランチで2つ目のmigrationファイルを作成して、migrateしたら、コメントが書き出されませんでした。

いろいろ試しましたが、最終的には一度コメントを削除してから、migrateしたら、上手く行きました。

やったこと

1. 設定ファイルを確認

$ bundle exec rails g annotate:installで生成されたlib/tasks/auto_annotate_models.rakeのAnnotateの設定を確認します。

auto_annotate_models.rake
'skip_on_db_migrate' => 'false', 

falseになっているのでmigrate時にコメントが書き出される設定になっています。(ここは問題なし)

2. 手動でAnnotateを実行

migarate時に実行されないので手動でAnnotateを実行します。
これでもコメントが書き出されない。。

$ bundle exec annoatate --models

3. gemを再インストール

それでもコメントが書き出されない場合は、gemを最インストールします。
再インストールすると直った場合もありました。

4. annotateのコメントを削除する

私の場合、いろいろ試してもコメントが書き出されなかったので、コメントを削除することにしました。

$ bundle exec annotate --delete

modelからコメントが削除されたことを確認して、migrateもしくは手動で実行。

$ bundle exec rails db:migrate

or

$ bundle exec annoatate --models

削除して、migrateすると全てのモデルに書き出されました。

おわりに

migrate時にannotateが実行されない原因は結局わかりませんでしたが、ひとまずコメントが書き出されました。
もし、原因がわかる方がいましたら、コメント頂けますと幸いです。

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

【Ruby on Rails】お問合せフォームの作成

目標

このような問い合わせフォームを作成します。
今回は開発環境でgmail宛に送信する方法です。
問い合わせ.png

開発環境

ruby 2.5.7
Rails 5.2.4.3
OS: macOS Catalina

前提

※ ▶◯◯ を選択すると、説明等が出てきますので、
  よくわからない場合の参考にしていただければと思います。

bootstrap4導入済み

【Ruby on Rails】enumを日本語表記にする方法
お問い合わせ件名の選択に使用します。_i18nが使用されている箇所です。

流れ

1 mailerを作成、編集
2 テーブルの作成
3 config/enviroments/development.rbの編集
4 環境変数の設定
5 controllerの作成
6 routing編集
7 viewの編集
8 googleアカウントで安全性の低いアプリのアクセスを許可する

mailerを作成、編集

ターミナル
$ rails g mailer ContactMailer

送信先と件名を指定します。
ENV['TOMAIL']は後ほど設定します。

app/mailers/thanks_mailer.rb
class ContactMailer < ApplicationMailer
  def send_mail(contact)
    @contact = contact
    mail to:   ENV['TOMAIL'], subject: '【お問い合わせ】' + @contact.subject_i18n
  end
end

テーブルの作成

ターミナル
$ rails g model Contact

カラムは必要なものを追加してください。
enumの設定については次回記事で解説します。

db/migrate/xxxxxxxxxxxxx_create_contacts.rb
class CreateContacts < ActiveRecord::Migration[5.2]
  def change
    create_table :contacts do |t|
      t.string :name, null: false
      t.string :email, null: false
      t.string :phone_number, null: false
      t.integer :subject, default: 0, null: false
      t.text :message, null: false

      t.timestamps
    end
  end
end

config/enviroments/development.rbの編集

config/enviroments/development.rb
...

config.action_mailer.raise_delivery_errors = false 
 ↓ true に変更
config.action_mailer.raise_delivery_errors = true

...

config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
      port: 587,
      address: 'smtp.gmail.com',
      domain: 'smtp.gmail.com',
      user_name: ENV['SMTP_USERNAME'],
      password: ENV['SMTP_PASSWORD'],
      enable_starttls_auto: true
  }
end

環境変数の設定

今回はGitHub上にアドレスとパスワードをアップしないよう
gem "dotenv-rails"を使用します。

Gemfile
gem "dotenv-rails"
ターミナル
$ bundle install

次にGemfileと同じ階層に .envファイルを作成

.env
TOMAIL=送信先のアドレス

SMTP_USERNAME=送信元のgmailアドレス
SMTP_PASSWORD=送信元のgmailアドレスのパスワード

失敗談【SMTP_USERNAME】
はじめにSMTP_USERNAMEをMAILと設定しようとしたところ、
何度やってもエラーになりました。
なので、設定する際にはこのように表記をした方がいいかもしれません。
命名規則を勉強した上でまた更新したいと思います。

.gitignoreファイルの一番下に下記を追加。

.gitignore
/.env

controllerの作成

ターミナル
$ rails g controller contacts
app/controllers/contacts_controller.rb
class Public::ContactsController < ApplicationController
  def new
    @contact = Contact.new
  end

  # 確認画面を作成する場合はこのような記述になるかと思います。
  # newアクションから入力内容を受け取り、
  # 送信ボタンを押されたらcreateアクションを実行します。
  def confirm
    @contact = Contact.new(contact_params)
    if @contact.invalid?
      render :new
    end
  end
 
  # 入力内容に誤りがあった場合、
  # 入力内容を保持したまま前のページに戻るのが当たり前になっているかと思いますが、
  # backアクションを定義することで可能となります。
  def back
    @contact = Contact.new(contact_params)
    render :new
  end

  # 実際に送信するアクションになります。
  # ここで初めて入力内容を保存します。
  # セキュリティーのためにも一定時間で入力内容の削除を行ってもいいかもしれません。
  def create
    @contact = Contact.new(contact_params)
    if @contact.save
      ContactMailer.send_mail(@contact).deliver_now
      redirect_to done_path
    else
      render :new
    end
  end

  # 送信完了画面を使用する場合お使いください。
  def done
  end

  private

  def contact_params
    params.require(:contact)
          .permit(:email,
                  :name,
                  :phone_number,
                  :subject,
                  :message
                 )
  end
end

routing編集

config/routes
resources :contacts, only: [:new, :create]
post 'contacts/confirm', to: 'contacts#confirm', as: 'confirm'
post 'contacts/back', to: 'contacts#back', as: 'back'
get 'done', to: 'contacts#done', as: 'done'

viewの編集

app/views/contact_mailer配下に
send_mail.html.erb
send_text.html.erb を作成してください。
※なぜ2つ作成するかはわからないので、未来の自分に託したいと思います。。。
内容は同じ内容を記入してください

app/views/contact_mailer/send_mail.html.erb,send_text.html.erb
<%= @contact.name %> 様 から問い合わせがありました。<br>
【Tel】:<%= @contact.phone_number %><br>
【Mail】:<%= @contact.email %><br>
【用件】:<%= @contact.subject_i18n %><br>
【お問い合わせ内容】<br>
<span style="white-space: pre-wrap;"><%= @contact.message %></span>

実際の入力フォームを作成していきます。
※名前とmessageのみ記述します。

app/views/contacts/new.html.erb
<%= form_for(@contact, url: confirm_path) do |f| %>
  <div class="form-group">
    <%= f.label :name, 'お名前*' %>
    <%= f.text_field :name, autofocus: true, class: 'form-control' %>
  </div>

  <div class="form-group">
    <%= f.label :message, 'メッセージ*' %>
    <%= f.text_area :message, size: '10x10', class: 'form-control' %>
  </div>

  <div>
    <%= f.submit '入力内容確認' %>
  </div>
<% end %>

確認画面から入力画面に戻るコードを記述します。
※名前とmessageのみ記述します。

app/views/contacts/confirm.html.erb
<table>
  <tbody>
    <tr>
      <td class="text-center" style="width: 30%;">お名前</td>
      <td><%= @contact.name %></td>
    </tr>
    <tr>
      <td class="text-center">メッセージ</td>
      <td style="white-space: pre-wrap;"><%= @contact.message %></td>
    </tr>
  </tbody>
</table>

<%= form_for(@contact) do |f| %>
  <%= f.hidden_field :name %>
  <%= f.hidden_field :email %>
  <%= f.hidden_field :phone_number %>
  <%= f.hidden_field :subject %>
  <%= f.hidden_field :message %>
  <div><%= f.submit '送信' %></div>
<% end %>
<%= form_for @contact, url: back_path do |f| %>
  <%= f.hidden_field :name %>
  <%= f.hidden_field :email %>
  <%= f.hidden_field :phone_number %>
  <%= f.hidden_field :subject %>
  <%= f.hidden_field :message %>
  <div><%= f.submit '入力画面に戻る' %></div>
<% end %>

補足【<%= @contact.name %>】
new画面のフォームでconfirmアクションに飛ばしているため、
paramsでは@contactに値が入っている状態が作れています。
したがって保存せずとも表示が可能です。

補足【送信】
@contactで表示はされているものの、保存は出来ていない状態です。
このままcreateアクションに飛ばしてしまうと保存されない状態でparamsを投げてしまいます。
そこでf.hidden_fieldを使い、それぞれparamsを再代入し、createに飛ばしています。

補足【入力画面に戻る】
こちらは送信と同じ理由になりますので、上記をご確認ください
@contactで表示はされているものの、保存は出来ていない状態です。

googleアカウントで安全性の低いアプリのアクセスを許可する

Google 安全性の低いアプリのアクセスを有効にする方法
こちらの記事がわかりやすかったので参考にしてください。

参考

まとめ

個人のアドレスや電話番号を扱うことになるため、
プライバシーポリシーや定期的な情報の削除は必須だと思います。
しかし、メールを相手に自動的に送信できるのは様々なこtにおいて優先順位は高めだと思いますので、
しっかり理解したほうがいいかもしれません。

またtwitterではQiitaにはアップしていない技術や考え方もアップしていますので、
よければフォローして頂けると嬉しいです。
詳しくはこちら https://twitter.com/japwork

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

Mysqlの構文を使って計算した結果をWHEREで取り出す

はじめに

はじめての投稿です。
20代は不動産付け、30代未経験、就活一社でそのままその自社開発企業に内定頂きました駆け出しエンジニアです。
今日はMysqlについて書きます。

土地のデータから一定の条件を満たすものを取り出すというプログラムです。
坪単価100万円以下の土地をピックアップします。

create TABLEから。

-- テーブルをcreate (カラム:id :m2 :price)
CREATE TABLE rands(
 id INT NOT NULL AUTO_INCREMENT,
 m2 INT,
 price INT,
 PRIMARY KEY (id)
);

-- 次にデータをテーブルに入れ込みます
INSERT INTO rands (m2, price) VALUES
 (200, 100000000),
 (300, 30000000),
 (500, 40000000),
 (100, 50000000);

-- *(データぜんぶ)をSELECTしてrandsテーブルからWHEREで条件にあう形でデータを取り出します。
-- 「坪単価」なので定数3.30578を入れます。

SELECT * FROM rands WHERE price / m2 * 3.30578 < 1000000 ;

今回はテーブルを作り、データを入れ込むところからやりましたが、
もし、データが蓄積されているような場合であれば、

SELECT * FROM テーブル名 WHERE 条件;

で取ってくることができますね。
以上です。

※ INT:整数ですよ、という宣言です。
※ NOT NULL:NULLを許さないよという記述です。主キーなのにNULLではこまりますので。
※ AUTO_INCREMENT:自動でidを連番で入れ込んでくれる便利なメソッドです。
※ PRIMARY KEY (id):idカラムのデータが主キーですよ〜という宣言です。これがないとAUTO_INCREMENTは使えません。
※ INSERT INTO:これから先に宣言するデータを入れ込みますよ〜という記述です。
※ VALUES:値のことです。上で言うと、(m2,price)となっているので、(m2 => 200, price => 1000000)と言う意味です。

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

Mysqlの構文を使ってデータをWHEREで取り出す

はじめに

はじめての投稿です。
20代は不動産付け、30代未経験、就活一社でそのままその自社開発企業に内定頂きました駆け出しエンジニアです。
今日はMysqlについて書きます。

土地のデータから一定の条件を満たすものを取り出すというプログラムです。
坪単価100万円以下の土地をピックアップします。

create TABLEから。

-- テーブルをcreate (カラム:id :m2 :price)
CREATE TABLE rands(
 id INT NOT NULL AUTO_INCREMENT,
 m2 INT,
 price INT,
 PRIMARY KEY (id)
);

-- 次にデータをテーブルに入れ込みます
INSERT INTO rands (m2, price) VALUES
 (200, 100000000),
 (300, 30000000),
 (500, 40000000),
 (100, 50000000);

-- *(データぜんぶ)をSELECTしてrandsテーブルからWHEREで条件にあう形でデータを取り出します。
-- 「坪単価」なので定数3.30578を入れます。

SELECT * FROM rands WHERE price / m2 * 3.30578 < 1000000 ;

今回はテーブルを作り、データを入れ込むところからやりましたが、
もし、データが蓄積されているような場合であれば、

SELECT * FROM テーブル名 WHERE 条件;

で取ってくることができますね。
以上です。

※ INT:整数ですよ、という宣言です。
※ NOT NULL:NULLを許さないよという記述です。主キーなのにNULLではこまりますので。
※ AUTO_INCREMENT:自動でidを連番で入れ込んでくれる便利なメソッドです。
※ PRIMARY KEY (id):idカラムのデータが主キーですよ〜という宣言です。これがないとAUTO_INCREMENTは使えません。
※ INSERT INTO:これから先に宣言するデータを入れ込みますよ〜という記述です。
※ VALUES:値のことです。上で言うと、(m2,price)となっているので、(m2 => 200, price => 1000000)と言う意味です。

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

配列の線形探索問題

はじめに

プログラミングとアルゴリズムに関しては初心者の私が(恐らく)線形探索という問題をといたので大したことない(自分は嬉しい)けど記事にしてみました。

問題とソース

配列の中に合致した数字がある場合それをそれを配列のindex番号と共に出力。ない場合はありませんと出力。

array = [3, 5, 9 ,12, 15, 21, 26, 34, 42, 51, 55, 56, 62, 65, 74, 123]

def search(a,array)
  count = 0
  array.each_with_index  do |num, i|
    if num == a 
      puts "#{i}番目にあります。"
    else
    count = count + 1
    end
  end
  if count == array.length
    puts "その数は含まれません"
  end
end

search(5,array)

このほかにもelse以降は記述せずにその代わりにreturnとして条件がtrueのときの処理を返し、繰り返しの処理の外でない場合の出力をする方法があるらしい。そうすれば、繰り返し処理を含むif文のようなメソッドが作れる。

最後に

初投稿ということもあり勝手に緊張と嬉しさがありますが、恐縮にも投稿させていただきました。

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

RSpec でGoogleMap のピンをクリックしたい

はじめに

RSpecで GoogleMap のピンをクリックするテストを書きたかったがハマってしまったので備忘録です。
記載しているソース、エラーコードは出力されたものを不要なものを省いた状態で載せていますので参考にする場合は注意してください。

環境

  • macOS 10.15.6
  • Ruby 2.5.7
  • Rails 5.2.3
  • gem capybara
  • gem selenium-webdriver

参考URL

https://qiita.com/jnchito/items/607f956263c38a5fec24

https://qiita.com/kon_yu/items/52a0f5f0016564486061

https://stackoverflow.com/questions/31479958/capybara-rspec-not-finding-and-clicking-css-element

https://code-kitchen.dev/html/map-area/

目標

GoogleMap 内のピンをクリックしてinfowindow が表示されるのかテストしたい

googlemap.png

問題

  • capybara でピンがクリックできなかった
  • img タグをクリックするとSelenium::WebDriver::Error::ElementClickInterceptedError:
  • map タグをクリックするとSelenium::WebDriver::Error::ElementNotInteractableError:

原因

  • 検索したい要素にdisplay:none, disabled, hidden になっているとcapybara で検索できない
  • 検索したい要素の高さが0px なのでcapybara で認識できない

解決策

visible: false オプション付きでarea タグをfind するとクリックできる

googlemap.html
<div id="map"> //地図全体
  <img src="https://maps.gstatic.com/mapfiles/api-3/images/spotlight-poi2_hdpi.png" usemap="#gmimap0"> //ピン画像
  <map name="gmimap0" id="gmimap0">
    <area> //コイツをクリックすればok
  </map>
</div>
googlemap_spec.rb
require 'rails_helper'

RSpec.describe "GoogleMap", type: :system do
  describe "GoogleMap が表示されているページ" do
    context "GoogleMap の動作確認", js: true do
      it "ピンをクリックするとinfowindow が表示されること" do
        pin = find("map#gmimap0 area", visible: false)
        pin.click
        expect(page).to have_css "div.infowindow" # infowindow クラスの有無をテスト
      end
    end
  end
end

ダメなパターン その1

ピンのimg タグを探してしてクリックする

  • そもそもimg タグはクリックできない
  • Selenium::WebDriver::Error::ElementClickInterceptedError:
  • area タグはクリック可能というヒントが表示される
googlemap_spec.rb
# ピンのimg タグを探してクリック
  describe "GoogleMap が表示されているページ" do
    context "GoogleMap の動作確認", js: true do
      it "ピンをクリックするとinfowindow が表示されること" do
        pin = find("img[src$='spotlight-poi2_hdpi.png']") #ピンのイメージを検索
        pin.click
        expect(page).to have_css "div.infowindow"
      end
    end
  end
# img タグはクリックできないよエラー
# area タグはクリックできるよ
Failures:

  1) GoogleMap が表示されているページ GoogleMap 表示内容 ピンをクリックするとinfowindow が表示されること
     Failure/Error: find("img[src$='spotlight-poi2_hdpi.png']").click

     Selenium::WebDriver::Error::ElementClickInterceptedError:
       element click intercepted: Element <img alt="" src="https://maps.gstatic.com/mapfiles/api-3/images/spotlight-poi2_hdpi.png" draggable="false" style="position: absolute; left: 0px; top: 0px; width: 27px; height: 43px; user-select: none; border: 0px; padding: 0px; margin: 0px; max-width: none;"> is not clickable at point (699, 750). Other element would receive the click: <area log="miw" coords="13.5,0,4,3.75,0,13.5,13.5,43,27,13.5,23,3.75" shape="poly" title="" style="cursor: pointer; touch-action: none;">
         (Session info: headless chrome=85.0.4183.121)

     [Screenshot]: スクリーンショットが保存されているディレクトリへのパス


     # 0   chromedriver                        0x00000001083091b9 chromedriver + 4911545
     .
     .
     .
     # 24  libsystem_pthread.dylib             0x00007fff6c49bb8b thread_start + 15

Finished in 9.05 seconds (files took 0.55005 seconds to load)
1 example, 1 failure

Failed examples:

ダメなパターン その2

map タグを探してクリックする

  • map タグが見つからない
  • エラーコードにヒントは無し
googlemap_spec.rb
# map タグを探してクリック
  describe "GoogleMap が表示されているページ" do
    context "GoogleMap の動作確認", js: true do
      it "infowindow が表示されること テスト" do
        pin = find("map#gmimap0").click #mapタグを検索
        pin.click
        expect(page).to have_css "div.infowindow"
      end
    end
  end
# map タグは見つからないよエラー
Failures

  1) GoogleMap が表示されているページ GoogleMap 表示内容 ピンをクリックするとinfowindow が表示されること
     Failure/Error: pin = find("map#gmimap0").click

     Capybara::ElementNotFound:
       Unable to find visible css "map#gmimap0"

     [Screenshot]: スクリーンショットが保存されているディレクトリへのパス


Finished in 7.84 seconds (files took 0.54001 seconds to load)
1 example, 1 failure

Failed examples:

ダメなパターン その3

map タグをvisible: false で探してクリックする

  • map タグはゼロサイズだからクリックできない
  • Selenium::WebDriver::Error::ElementNotInteractableError:
googlemap_spec.rb
# map タグにvisible: false を付けてfind
  describe "GoogleMap が表示されているページ" do
    context "GoogleMap 表示内容", js: true do
      it "infowindow が表示されること テスト" do
        pin = find("map#gmimap0", visible: false).click #非表示要素の検索オプションを付加してmap タグを検索
        pin.click
        expect(page).to have_css "div.infowindow"
      end
    end
  end
# map タグはゼロサイズだからクリックできないよのエラー
Failures:

  1) GoogleMap が表示されているページ GoogleMap 表示内容 infowindow が表示されること
     Failure/Error: pin = find("map#gmimap0", visible: false).click

     Selenium::WebDriver::Error::ElementNotInteractableError:
       element not interactable: element has zero size
         (Session info: headless chrome=85.0.4183.121)

     [Screenshot]: スクリーンショットが保存されているディレクトリへのパス


     # 0   chromedriver                        0x0000000107a8c1b9 chromedriver + 4911545
     .
     .
     # 21  libsystem_pthread.dylib             0x00007fff7034eb8b thread_start + 15

Finished in 8.19 seconds (files took 0.52799 seconds to load)
1 example, 1 failure

学び

  • capybara で非表示要素を検索したい場合はvisible: false オプションでfind するとクリックできる
  • エラーコードにヒントが隠されているので見逃さない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VagrantにHerokuを入れる際に起きたエラーと解決方法と参考記事

はじめに

ドットインストールで構築したローカル環境でアプリを開発し、公開するため、いざherokuを入れようとしたらエラーが起きまくりました。
【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】
この記事を参考にしていました。
しかし、herokuを入れる際に、この記事の開発環境が全く違うため、その先に進めずエラーと戦うことになりました。

パスがない問題

エラー解決に必死だったため、エラーのコピーは取っていませんが、このようなエラーがでたと思います。

↓エラーの最後の文
Your path is missing /usr/local/bin, you need to add this to use this installer.

/usr/local/binのパスが無いと言われているため、パスを作る必要があるみたいです。

パスの確認

$ echo $PATH

パスを確認してみますが/usr/local/bin:はあっても/usr/local/binがないと駄目みたいです。

解決方法の前に、解決する中で知った知識

sudoとは、僕の解釈では権限の高いコマンド。
$#で違いもあり、#のほうが権限が高い。($の方で実行できないことが#で実行できたから)

解決方法:偉い権限を使ったり、viエディタで書き換えたりする。

パスの設定に必要な.bash_profileの設定とsudoの設定は以下を参考にしました。
仮想環境構築後に設定しておきたいこと -メモ-

sudoの設定はこちらの方が親切です。
CentOS で sudo 時に実行ユーザーのPATHを引き継ぐ

$ sudo visudo

の実行後に、viエディタの操作が必要だったため以下を参考にしました。
viエディタの使い方

パスが通ったと思ったら、シンタックスエラー問題

$ heroku --version

herokuのバージョン確認をすると、以下のエラーが...

/usr/local/lib/heroku/node_modules/@oclif/command/lib/index.js:3
const path = require("path");
^^^^^
SyntaxError: Use of const in strict mode.
    at Module._compile (module.js:439:25)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Module.require (module.js:364:17)
    at require (module.js:380:17)
    at Object.<anonymous> (/usr/local/lib/heroku/bin/run:5:1)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)

Node.jsのバージョンが古すぎたみたいです。

$ node -v

確認してみたところv0.10.48でした。

解決方法:キャッシュを消して、新しいバージョンを入れる。

いろいろと試したため、ダイレクトな解決方法にならない場合はすみません。

npmがインストールされていなかったので、インストールしました。ついでにnodeもインストール。
Herokuを入れるためにnpmが必要かどうかはわかりませんが、とりあえずインストールしました。

$ sudo yum install nodejs npm

偉い権限の方に行く。

$ sudo -s

左のやつが$から#に変わる。

インストールするために必要なやつ(正直、よくわかってないですがsetup_11.xの数字の部分でインストールするバージョンを指定。)

# curl --silent --location https://rpm.nodesource.com/setup_11.x | bash 

ここから、インストールとアンインストールを繰り返したため、手順が間違っていたらすみません。
以下を参考にしました。先の読んでおいて欲しいです。
yumでのnodejsのバージョンアップにはまった話と解決方法

古い方のrpmを削除。

# rm /etc/yum.repos.d/nodesource-el.repo

アンインストールする。

# yum -y remove nodejs

キャッシュを削除。

# yum clean all

インストールする。

# yum -y install nodejs

バージョンを確認。

# node -v
v11.15.0

成功です!
そして、偉い権限のところから出る。

# su vagrant

herokuのバージョンも確認

$ heroku -v
heroku/7.44.0 linux-x64 node-v11.15.0

他の参考記事です。
https://qiita.com/daskepon/items/16a77868d38f8e585840
https://inaba.hatenablog.com/entry/2018/11/13/023933

ほぼ同じエラーが出ていた記事
https://teratail.com/questions/256490

感謝

参考にさせていただいたサイト管理人の皆様、誠にありがとうございました。

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

before_action使うべし!!

before_actionってなに?

ここでbefore_actionがなんなのか簡単に説明させて頂きます。

before_actionとは、before_actionを使用するとcontrollerで定義された
アクションが実行される前に、共通の処理を行うことができるメソッドです。
つまり、railsのcontrollerアクションを実行する前に処理を行いたい時、同じ記述の処理をまとめたい時に使用します。

言葉で説明されても「どゆこと?」、「イメージができない。。。」と
思いますので下記に例を上げて説明したいと思います。

before_action記述例

[例] before_action
class コントローラ名 < ApplicationController
  before_action :処理させたいメソッド名

また、ここではオプションを同時に使用します。
全部のアクションにbefore_actionを使用する必要はないので、
どのアクションで実行する前に処理を行わせたいのか指定します。
そこで使用するオプションがonlyオプションです。

onlyオプションってなに?(記述例)

resourcesと同様にonlyオプションを使用することによって、どのアクションの実行前に、処理を実行させるかなど制限するとこが可能です。

『resources』とは、7つのアクションのルーティングをまとめて設定ができるメソッドのことです。

[例] before_action
class コントローラ名 < ApplicationController
  before_action :処理させたいメソッド名, only: [:アクション名, :アクション名]

上記のようにbefore_actionの後ろにonlyオプションを記述して使用します。
例のように記述することでアクションを指定することができます。

基本は上記のように記述します。

この例を参照してもどう書いたらいいの?となると思いますので、
次はcontrollerの全体的な記述例を上げて説明します。

controller全体記述例

ここでは、controllerの全体の記述例を上げて説明していきたいと思います。

先にbefore_actionを使用していない時の記述例です。
下記の例を見て頂きたいのですが、同じ記述がありますね。

使用しない場合

[例] controller
class TweetsController < ApplicationController

  def index
    @tweets = Tweet.all
  end

  def new
    @tweet = Tweet.new
  end

  def create
    Tweet.create(tweet_params)
  end

  def destroy
    tweet = Tweet.find(params[:id])
    tweet.destroy
  end

  def edit
    @tweet = Tweet.find(params[:id])    ⬅️ ここの箇所!!
  end

  def update
    tweet = Tweet.find(params[:id])
    tweet.update(tweet_params)
  end

  def show
    @tweet = Tweet.find(params[:id])    ⬅️ ここの箇所!!
  end

  private

  def tweet_params
    params.require(:tweet).permit(:name, :image, :text)
  end
end

tweetsコントローラーのeditアクションとshowアクションを見ると、
@tweet = Tweet.find(params[:id])が繰り返し記述されています。
この箇所の記述をまとめていきたいと思います。

上記の例だともうひとつ被っている記述がありますが、
今回は一箇所だけbefore_actionを使用してまとめていきますので、
気にしないで下さい。w

次は使用した時の記述例です。

使用する場合

editアクションとshowアクションの処理"のみ"を実行させて記述を
まとめていきたいと思います。
今回は、before_actionで呼び出すメソッドをtest_tweetとしましょう!

[例] controller
class TweetsController < ApplicationController
  before_action :set_tweet, only: [:edit, :show]

  def index
    @tweets = Tweet.all
  end

  def new
    @tweet = Tweet.new
  end

  def create
    Tweet.create(tweet_params)
  end

  def destroy
    tweet = Tweet.find(params[:id])
    tweet.destroy
  end

  def edit
  end

  def update
    tweet = Tweet.find(params[:id])
    tweet.update(tweet_params)
  end

  def show
  end

  private

  def tweet_params
    params.require(:tweet).permit(:name, :image, :text)
  end

  def test_tweet
    @tweet = Tweet.find(params[:id])
  end
end

before_actionを使用することで、editアクションとshowアクションの
実行前に、test_tweetというメソッドを呼び出しています。

そうすることで、2つのアクションの共通の処理である
@tweet = Tweet.find(params[:id])が動きます。

上記の例のようにbefore_actionを使用してまとめてあげることで繰り返し
記述することがないのでスッキリして見やすくなりましたね!!

まとめ

・before_actionは、コントローラで定義されたアクションが実行される前に、
指定した共通の処理を行うことができるメソッドのこと

・onlyオプションは、resourcesと同様にonlyオプションを使用することによって、どのアクションの実行前に、処理を実行させるかなど制限すること

!!これらの事からコードを記述する際は、同じ記述を繰り返さずに見やすく記述していくのがいいですね!!

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

Ruby on rails 学習記録 -2020.10.05

Webフォームのデータ送信方式

GETメソッド:URLに含める
http://example.net?id=3&content=hello

POSTメソッド:リクエストメッセージに含める
article[name]=paiza
article[content]=hello+world

Railsサーバーのログなどが、画面上部に消えてしたったら、マウスのホイールが役に立つ。

ホイール操作できない場合
Mac:CTRLキー + Altキー + 上下矢印キー
Windows: CTRLキー + 上下矢印キー

Railsのルーター機能

Routesの設定内容の確認
rails routes
GETとPOSTの確認ができる

ルーターの振り分けを設定

config/routes.rb
Rails.application.routes.draw do
  get 'welcome/index'

  resources :articles
  root 'welcome#index'
end

welcome#indexがトップページになる

ビューのテンプレート

ヘルパーメソッド:ビューを作るときに利用できる専用コマンド

<%= link_to 'Edit', edit_article_path(@article) %> |
<%= link_to 'Back', articles_path %>

Railsアプリのページ間のリンクを記述できる。

<%= link_to 'text', path %>

これは、次のhtmlに変換できる。

<a href="path">text</a>

投稿フォームの動作

部分テンプレート:複数のビューを記述するテンプレート
呼び出すには、renderメソッドを利用する。

<%= render 'form', article: @article %>

この場合、「_form.html.erb」が部分テンプレートのファイル名になる。
また、article変数で、@articleのオブジェクトを利用できる。

フォームを作成するヘルパーメソッド
form_for:投稿フォームのように、Modelの新規作成・更新に使用する
form_tag:検索フォームのように、Modelを更新しない場合に使用する

form_forメソッド

<%= form_for(@article) do |f| %>
  <div class="field">
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>

  <div class="field">
    <%= f.label :content %>
    <%= f.text_field :content %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

controllerのデータの書き込み

ストロングパラメーター:
データベースに安全にアクセスするために、書き込みできるカラムをリストアップ
controllerのarticle_paramsメソッドに記述する。

articles_controller.rb(一部)
# Never trust parameters from the scary internet, only allow the white list through.
def article_params
  params.require(:article).permit(:content, :name, :feeling)
end

検索フォームの追加

ビューに検索フォームの追加

index.html.erb(一部)
<%= form_tag('/articles', method: 'get') do %>
  <%= label_tag(:name_key, 'Search name:') %>
  <%= text_field_tag(:name_key) %>
  <%= submit_tag('Search') %> <%= link_to 'Clear', articles_path %>
<% end %>

<br>

コントローラーにindexメソッドの検索コードを追加

articles_controller.rb(一部)
# GET /articles
# GET /articles.json
def index
  if params[:name_key]
    @articles = Article.where('name LIKE ?', "%#{params[:name_key]}%")
  else
    @articles = Article.all
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Passenger 6.0.5 で ActiveRecord 使った Sinatra アプリが死んだ

何の話?

CentOS 8 + Apache 2.4 + Phusion Passenger 6.0 で,ActiveRecord を使った Sinatra アプリを動かしていた。
Ruby のバージョンは 2.7.1 だが,たぶんこのバージョンはあまり関係無い。

Passenger 6.0.4 では動いていたのに 6.0.6 では,本番(production)環境で起動すらしなくなった。
手許(ローカル)ではちゃんと動作している。

Apache のエラーログ(/var/log/httpd/error_log)には以下のように出ていた。

'production' database is not configured. Available: [] (ActiveRecord::AdapterNotSpecified)

原因

どうも Passenger 6.0.5 で導入された変更が原因のようだ。
ここに issue が上がっている。
https://github.com/phusion/passenger/issues/2281

同様の問題は Sinatra 以外でも起こりうるが,Rails では起こらない(たぶん)。

死んだコード

件の Sinatra アプリは,SQLite3 データベースにあらかじめ入れておいたデータを検索して表示するだけのごく簡単なもの。
データベースファイルは,とくに production 用とか development 用とか分けていない。

接続のところは

ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: db_file)

てな感じに書いていた。
ローカル変数 db_file には,SQLite3 ファイルのパスが代入されている。

修正

どうも,ActiveRecord::Base.configurations をセットしてやらなければならないようなので,上記を

ActiveRecord::Base.configurations = {
  "production" => {"adapter" => "sqlite3", "database" => db_file },
  "development" => {"adapter" => "sqlite3", "database" => db_file },
}
ActiveRecord::Base.establish_connection

のように変えた。

これで本番環境でも動くようになった。

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

pay.jpの導入

Pay.jpの導入方法

クレジットカード決済の代行サービスで、簡単なオープンAPIで導入する事ができる

Why

コード量も非常に少なく、Javascriptで簡単にフォーム送信までできるのでご紹介します(割愛しながらの説明になります)

アプリケーションの作成

ターミナル
% cd ~/projects(好きなように)
% rails _6.0.0_ new payjp_practice -d mysql
% cd payjp_practice
% rails db:create

Orderモデル作成

ターミナル
% rails g model order

Ordersテーブル作成

db/migrate/**************_create_orders.rb
class CreateOrders < ActiveRecord::Migration[6.0]
  def change
    create_table :orders do |t|
      t.integer :price  ,null: false
      t.timestamps
    end
  end
end

忘れずに!

ターミナル
% rails db:migrate

バリデーション

app/models/order.rb
class Order < ApplicationRecord
  validates :price, presence: true
end

ルーティング

config/routes.rb
Rails.application.routes.draw do
  root to: 'orders#index'
  resources :orders, only:[:create]
end

ordersコントローラー

ターミナル
% rails g controller orders
app/controllers/orders_controller.rb
class OrdersController < ApplicationController

  def index
  end

  def create
  end

end

ビューを作成

app/views/orders/index.html.erb
<%= form_with  model: @order, id: 'charge-form', class: 'card-form',local: true do |f| %>
  <div class='form-wrap'>
    <%= f.label :price, "金額" %>
    <%= f.text_field :price, class:"price", placeholder:")2000" %>
  </div>
  <%= f.submit "購入" ,class:"button" %>
<% end %>

CSS記述

app/assets/stylesheets/style.css
.card-form{
  width: 500px;
}

.form-wrap{
  display: flex;
  flex-direction: column;
}

.exp_month{
  resize:none;
}

.exp_year{
  resize:none;
}

.input-expiration-date-wrap{
  display: flex;
}


.button{
  margin-top: 30px;
  height: 30px;
  width: 100px;
}

コントローラー編集

app/controllers/orders_controller.rb
class OrdersController < ApplicationController

  def index
    @order = Order.new
  end

  def create
    @order = Order.new(order_params)
    if @order.valid?
      @order.save
      return redirect_to root_path
    else
      render 'index'
    end
  end

  private

  def order_params
    params.require(:order).permit(:price)
  end

end

部分テンプレート

app/views/orders/index.html.erb
<%= form_with  model: @order, id: 'charge-form', class: 'card-form',local: true do |f| %>
  <%= render 'layouts/error_messages', model: @order %>
  <div class='form-wrap'>
<%# 省略 %>

ビュー記述

app/views/orders/index.html.erb
<%= form_with  model: @order, id: 'charge-form', class: 'card-form',local: true do |f| %>
  <%= render 'layouts/error_messages', model: @order %>
  <div class='form-wrap'>
    <%= f.label :price, "金額" %>
    <%= f.text_field :price, class:"price", placeholder:")2000" %>
  </div>
  <div class='form-wrap'>
    <%= f.label :number,  "カード番号" %>
    <%= f.text_field :number, class:"number", placeholder:"カード番号(半角英数字)", maxlength:"16" %>
  </div>
  <div class='form-wrap'>
    <%= f.label :cvc ,"CVC" %>
    <%= f.text_field :cvc, class:"cvc", placeholder:"カード背面4桁もしくは3桁の番号", maxlength:"3" %>
  </div>
  <div class='form-wrap'>
    <p>有効期限</p>
    <div class='input-expiration-date-wrap'>
      <%= f.text_field :exp_month, class:"exp_month", placeholder:")3" %>
      <p>月</p>
      <%= f.text_field :exp_year, class:"exp_year", placeholder:")24" %>
      <p>年</p>
    </div>
  </div>
  <%= f.submit "購入" ,class:"button" %>
<% end %>

turbolinks削除&コード追加

app/views/layouts/application.html.erb
<%# 省略 %>
    <%= stylesheet_link_tag 'application', media: 'all'  %>
    <%= javascript_pack_tag 'application' %>
<%# 省略 %>
app/javascript/packs/application.js
// 省略
require("@rails/ujs").start()
// require("turbolinks").start() // コメントアウトする
require("@rails/activestorage").start()
require("channels")
// 省略

payjp.js読み込み

app/views/layouts/application.html.erb
<%# 省略 %>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <script type="text/javascript" src="https://js.pay.jp/v1/"></script>
    <%= stylesheet_link_tag 'application', media: 'all' %>
    <%= javascript_pack_tag 'application' %>
<%# 省略 %>

トークン化準備

app/javascript/packs/application.js
// 省略
require("@rails/ujs").start()
// require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
require("../card")
// 省略

イベント発火

app/javascript/card.js
const pay = () => {
  const form = document.getElementById("charge-form");
  form.addEventListener("submit", (e) => {
    e.preventDefault();
    console.log("フォーム送信時にイベント発火")
  });
};

window.addEventListener("load", pay);

公開鍵設定

app/javascript/card.js
const pay = () => {
  Payjp.setPublicKey("pk_test_******************"); // PAY.JPテスト公開鍵
  const form = document.getElementById("charge-form");
  form.addEventListener("submit", (e) => {
    e.preventDefault();
    console.log("フォーム送信時にイベント発火")
  });
};

window.addEventListener("load", pay);

フォームの情報取得

app/javascript/card.js
const pay = () => {
  Payjp.setPublicKey("pk_test_******************"); // PAY.JPテスト公開鍵
  const form = document.getElementById("charge-form");
  form.addEventListener("submit", (e) => {
    e.preventDefault();

    const formResult = document.getElementById("charge-form");
    const formData = new FormData(formResult);

    const card = {
      number: formData.get("order[number]"),
      cvc: formData.get("order[cvc]"),
      exp_month: formData.get("order[exp_month]"),
      exp_year: `20${formData.get("order[exp_year]")}`,
    };
  });
};

window.addEventListener("load", pay);

カードの情報トークン化

app/javascript/card.js
const pay = () => {
  Payjp.setPublicKey("pk_test_******************"); // PAY.JPテスト公開鍵
  const form = document.getElementById("charge-form");
  form.addEventListener("submit", (e) => {
    e.preventDefault();

    const formResult = document.getElementById("charge-form");
    const formData = new FormData(formResult);

    const card = {
      number: formData.get("order[number]"),
      cvc: formData.get("order[cvc]"),
      exp_month: formData.get("order[exp_month]"),
      exp_year: `20${formData.get("order[exp_year]")}`,
    };

    Payjp.createToken(card, (status, response) => {
      if (status == 200) {
        const token = response.id;
        console.log(token)
      }
    });
  });
};

window.addEventListener("load", pay);

一度pay.jpが用意しているテスト用のカード情報を入力してチェックしておきましょう!

カード番号 4242424242424242(16桁)
CVC 123
有効期限 登録時より未来(04/25など)

トークンの値をフォームに含める

app/javascript/card.js
const pay = () => {
  Payjp.setPublicKey("pk_test_******************"); // PAY.JPテスト公開鍵
  const form = document.getElementById("charge-form");
  form.addEventListener("submit", (e) => {
    e.preventDefault();

    const formResult = document.getElementById("charge-form");
    const formData = new FormData(formResult);

    const card = {
      number: formData.get("order[number]"),
      cvc: formData.get("order[cvc]"),
      exp_month: formData.get("order[exp_month]"),
      exp_year: `20${formData.get("order[exp_year]")}`,
    };

    Payjp.createToken(card, (status, response) => {
      if (status == 200) {
        const token = response.id;
        const renderDom = document.getElementById("charge-form");
        const tokenObj = `<input value=${token} name='token'>`;
        renderDom.insertAdjacentHTML("beforeend", tokenObj);
        debugger;
      }
    });
  });
};

window.addEventListener("load", pay);

トークンの値を非表示

app/javascript/card.js
const pay = () => {
  Payjp.setPublicKey("pk_test_******************"); // PAY.JPテスト公開鍵
  const form = document.getElementById("charge-form");
  form.addEventListener("submit", (e) => {
    e.preventDefault();

    const formResult = document.getElementById("charge-form");
    const formData = new FormData(formResult);

    const card = {
      number: formData.get("order[number]"),
      cvc: formData.get("order[cvc]"),
      exp_month: formData.get("order[exp_month]"),
      exp_year: `20${formData.get("order[exp_year]")}`,
    };

    Payjp.createToken(card, (status, response) => {
      if (status == 200) {
        const token = response.id;
        const renderDom = document.getElementById("charge-form");
        const tokenObj = `<input value=${token} name='token' type="hidden"> `;
        renderDom.insertAdjacentHTML("beforeend", tokenObj);
        debugger;
      }
    });
  });
};

window.addEventListener("load", pay);

クレジットカードの情報を削除

app/javascript/card.js
const pay = () => {
  Payjp.setPublicKey("pk_test_******************"); // PAY.JPテスト公開鍵
  const form = document.getElementById("charge-form");
  form.addEventListener("submit", (e) => {
    e.preventDefault();

    const formResult = document.getElementById("charge-form");
    const formData = new FormData(formResult);

    const card = {
      number: formData.get("order[number]"),
      cvc: formData.get("order[cvc]"),
      exp_month: formData.get("order[exp_month]"),
      exp_year: `20${formData.get("order[exp_year]")}`,
    };

    Payjp.createToken(card, (status, response) => {
      if (status == 200) {
        const token = response.id;
        const renderDom = document.getElementById("charge-form");
        const tokenObj = `<input value=${token} name='token' type="hidden"> `;
        renderDom.insertAdjacentHTML("beforeend", tokenObj);
      }

      document.getElementById("order_number").removeAttribute("name");
      document.getElementById("order_cvc").removeAttribute("name");
      document.getElementById("order_exp_month").removeAttribute("name");
      document.getElementById("order_exp_year").removeAttribute("name");
    });
  });
};

window.addEventListener("load", pay);

フォームの情報をサーバーサイドに送信

app/javascript/card.js
const pay = () => {
  Payjp.setPublicKey("pk_test_******************"); // PAY.JPテスト公開鍵
  const form = document.getElementById("charge-form");
  form.addEventListener("submit", (e) => {
    e.preventDefault();

    const formResult = document.getElementById("charge-form");
    const formData = new FormData(formResult);

    const card = {
      number: formData.get("order[number]"),
      cvc: formData.get("order[cvc]"),
      exp_month: formData.get("order[exp_month]"),
      exp_year: `20${formData.get("order[exp_year]")}`,
    };

    Payjp.createToken(card, (status, response) => {
      if (status == 200) {
        const token = response.id;
        const renderDom = document.getElementById("charge-form");
        const tokenObj = `<input value=${token} name='token' type="hidden"> `;
        renderDom.insertAdjacentHTML("beforeend", tokenObj);
      }

      document.getElementById("order_number").removeAttribute("name");
      document.getElementById("order_cvc").removeAttribute("name");
      document.getElementById("order_exp_month").removeAttribute("name");
      document.getElementById("order_exp_year").removeAttribute("name");

      document.getElementById("charge-form").submit();
    });
  });
};

window.addEventListener("load", pay);

ストロングパラメーター編集

app/controllers/orders_controller.rb
#省略

  private

  def order_params
    params.require(:order).permit(:price).merge(token: params[:token])
  end

end

Orderモデルに追記

app/models/order.rb
class Order < ApplicationRecord
  attr_accessor :token
  validates :price, presence: true
end

Gem導入

Gemfile
# 省略
gem 'payjp'

決済処理の記述とリファクタリング

app/controllers/orders_controller.rb
class OrdersController < ApplicationController

  def index
    @order = Order.new
  end

  def create
    @order = Order.new(order_params)
    if @order.valid?
      pay_item
      @order.save
      return redirect_to root_path
    else
      render 'index'
    end
  end

  private

  def order_params
    params.require(:order).permit(:price).merge(token: params[:token])
  end

  def pay_item
    Payjp.api_key = "sk_test_***********"  # 自身のPAY.JPテスト秘密鍵を記述しましょう
    Payjp::Charge.create(
      amount: order_params[:price],  # 商品の値段
      card: order_params[:token],    # カードトークン
      currency: 'jpy'                 # 通貨の種類日本円
    )
  end

end

バリデーション

app/models/order.rb
class Order < ApplicationRecord
  attr_accessor :token
  validates :price, presence: true
  validates :token, presence: true
end

環境変数(Mac Catalina以降の場合)

ターミナル
% vim ~/.zshrc
# iを押してインサートモードに移行し下記を追記する既存の記述は消去しない
export PAYJP_SECRET_KEY='sk_test_************'
export PAYJP_PUBLIC_KEY='pk_test_************'
# 編集が終わったらescキーを押してから:wqと入力して保存して終了
ターミナル
# 編集した.zshrcを読み込み直して追加した環境変数を使えるようにする
% source ~/.zshrc

秘密鍵代入した環境変数の呼び込み

app/controllers/orders_controller.rb
#省略
def pay_item
   Payjp.api_key = ENV["PAYJP_SECRET_KEY"]
   Payjp::Charge.create(
     amount: order_params[:price],
     card: order_params[:token],
     currency:'jpy'
   )
end

JavaScriptで環境変数の呼び込み

ターミナル
% touch config/initializers/webpacker.rb
config/initializers/webpacker.rb
Webpacker::Compiler.env["PAYJP_PUBLIC_KEY"] = ENV["PAYJP_PUBLIC_KEY"]
app/javascript/card.js
const pay = () => {
 Payjp.setPublicKey(process.env.PAYJP_PUBLIC_KEY);
  // 省略

まとめ

簡単と言っておきながら意外と記述は多かったかもしれません。ですが、APIの中でも比較的簡単な決済機能の導入なので抑えておくといいかもしれません。オリジナルでカラム等追加する場合がほとんどだと思いますので、ゆっくり順に書くことをお勧めします!以上!

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

【No.007】組織の管理画面と組織へのログイン処理

Issue
PR
【No.006】組織の管理画面とログインのおおまかなデザイン

概念等は組織としていたが、実装は会社とすることにした

概要

組織の管理画面と組織へのログイン処理を追加する

ToDo詳細

  • Orgモデル・テーブル追加
Terminal
bin/rails g model org name:string org_type:integer
  • orgs_controllerを追加
Terminal
bin/rails g controller orgs

要らないファイルは削除しておく

Terminal
-> % bin/rails g controller orgs
Running via Spring preloader in process 22619
      create  app/controllers/orgs_controller.rb
      invoke  slim
      create    app/views/orgs
      invoke  test_unit
      create    test/controllers/orgs_controller_test.rb <- これは削除した
      invoke  helper
      create    app/helpers/orgs_helper.rb <- これは削除した
      invoke    test_unit
      invoke  assets
      invoke    coffee
      invoke    scss
      create      app/assets/stylesheets/orgs.scss <- これは削除した
  • orgs#indexのrouteを設定し、ビュー追加
config/routes.rb
Rails.application.routes.draw do
  root 'orders/ordering_org_sides#index'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  resource :orgs, only: %i[index] #<-追加
  namespace :orders do
    resources :ordering_org_sides, only: %i[index]
  end
end
Terminal
touch app/views/orgs/index.html.slim

参考

app/views/orgs/index.html.slim
.bg-white.shadow.m-auto.sm:rounded-md.mt-5(class="w-2/4")
  ul
    - @orgs.each do |org|
      / TODO:Sassで場合分けできるようにする
      - border_t = org == @orgs.first ? '' : 'border-t border-gray-200'
      li.(class=border_t)
        = link_to [org], class: 'block hover:bg-gray-50 focus:outline-none focus:bg-gray-50 transition duration-150 ease-in-out' do
          .flex.items-center.px-4.py-4.sm:px-6
            .min-w-0.flex-1.flex.items-center
              .min-w-0.flex-1.px-4.md:grid.md:grid-cols-2.md:gap-4
                div
                  .text-sm.leading-5.font-medium.text-indigo-600.truncate
                    = org.name
              div
                i.fas.fa-sign-in-alt.fa-lg.bg-gray-50.text-gray-500
image.png
  • orgs#showのrouteを設定し、ビュー追加
config/routes.rb
Rails.application.routes.draw do
  root 'orders/ordering_org_sides#index'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  resource :orgs, only: %i[index, show] #<-showも追加
  namespace :orders do
    resources :ordering_org_sides, only: %i[index]
  end
end
Terminal
touch app/views/orgs/show.html.slim

参考

app/views/orgs/show.html.slim
.bg-white.shadow.m-auto.mt-5.overflow-hidden.sm:rounded-lg(class="w-1/2")
  .px-4.py-5.border-b.border-gray-200.sm:px-6
    h3.text-lg.leading-6.font-medium.text-gray-900
      |  会社詳細
    p.mt-1.max-w-2xl.text-sm.leading-5.text-gray-500
      |  会社詳細を説明します。
  div
    dl
      .bg-gray-100.px-4.py-5.sm:grid.sm:grid-cols-3.sm:gap-4.sm:px-6
        dt.text-sm.leading-5.font-medium.text-gray-500
          |  会社名
        dd.mt-1.text-sm.leading-5.text-gray-900.sm:mt-0.sm:col-span-2
          = @org.name
      .bg-white.px-4.py-5.sm:grid.sm:grid-cols-3.sm:gap-4.sm:px-6
        dt.text-sm.leading-5.font-medium.text-gray-500
          |  会社タイプ
        dd.mt-1.text-sm.leading-5.text-gray-900.sm:mt-0.sm:col-span-2
          = @org.org_type
image.png
  • 右斜上にそれぞれの導線・リンクを配置

参考

app/views/orders/ordering_org_sides/index.html.slim
.bg-gray-100
  nav.bg-white.shadow-sm
    .max-w-7xl.mx-auto.px-4.sm:px-6.lg:px-8
      .flex.justify-between.h-16
        .flex
          .flex-shrink-0.flex.items-center
          .hidden.sm:ml-6.space-x-8.sm:flex
            a.inline-flex.items-center.px-1.pt-1.border-b-2.border-indigo-500.text-sm.font-medium.leading-5.text-gray-900.focus:outline-none.focus:border-indigo-700.transition.duration-150.ease-in-out[href="#"]
              | Sample_1
            a.inline-flex.items-center.px-1.pt-1.border-b-2.border-transparent.text-sm.font-medium.leading-5.text-gray-500.hover:text-gray-700.hover:border-gray-300.focus:outline-none.focus:text-gray-700.focus:border-gray-300.transition.duration-150.ease-in-out[href="#"]
              | Sample_2
            a.inline-flex.items-center.px-1.pt-1.border-b-2.border-transparent.text-sm.font-medium.leading-5.text-gray-500.hover:text-gray-700.hover:border-gray-300.focus:outline-none.focus:text-gray-700.focus:border-gray-300.transition.duration-150.ease-in-out[href="#"]
              | Sample_3
            a.inline-flex.items-center.px-1.pt-1.border-b-2.border-transparent.text-sm.font-medium.leading-5.text-gray-500.hover:text-gray-700.hover:border-gray-300.focus:outline-none.focus:text-gray-700.focus:border-gray-300.transition.duration-150.ease-in-out[href="#"]
              | Sample_4
        .hidden.sm:ml-6.sm:flex.sm:items-center
          button.p-1.border-2.border-transparent.text-gray-400.rounded-full.hover:text-gray-500.focus:outline-none.focus:text-gray-500.focus:bg-gray-100.transition.duration-150.ease-in-out[aria-label="Notifications"]
            svg.h-6.w-6[stroke="currentColor" fill="none" viewbox="0 0 24 24"]
              path[stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"]
          .ml-3.relative
            div
              button#user-menu.flex.text-sm.border-2.border-transparent.rounded-full.focus:outline-none.focus:border-gray-300.transition.duration-150.ease-in-out[aria-label="User menu" aria-haspopup="true"]
                i.fas.fa-user-circle.fa-2x.text-gray-700
            .origin-top-right.absolute.right-0.mt-2.w-48.rounded-md.shadow-lg
              .py-1.rounded-md.bg-white.shadow-xs
                = link_to [:orgs], class: 'block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out' do
                  | 会社切替
                / TODO:リンクを@orgに書き換える
                = link_to '/', class: 'block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out' do
                  | 会社詳細

image

JSは別PRで対応予定。

  • seedにorgsを追加
db/seeds.rb
orgs = Org.create(
  [
    {name: '会社_a', org_type: 0},
    {name: '会社_b', org_type: 1},
    {name: '会社_c', org_type: 0}
  ]
)

動作確認

準備

bin/rails db:migrate:reset
bin/rails db:reset

受入基準

  • 下図のように画面遷移する

85553aaf9b9e864a3642ee1dda1b55c4

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