20190704のRailsに関する記事は22件です。

関連する ActiveModel::Serializer を切り替える

はじめに

  • Associations において利用する Serializer を切り替えました
  • AMS で該当する機能は見つからなかったので素朴に実現しました

目次

  • はじめに
  • TL;DR
  • 環境
  • 構成
    • Model
    • API
  • やりたいこと
  • 実現方法
    • Controller
    • Serializer
  • まとめ
  • 参考

TL;DR

  • Rendering の際に options を渡す
reservations_controller.rb
  render json: reservation,
          include: '**', restaurant_type: :detail
  • Serializer の initialize で options を受け取る
reservation_serializer.rb
  def initialize(object, options = {})
    super
    @restaurant_type = options[:restaurant_type]
  • options の値によって 関連先で利用する Serializer を切り替える
reservation_serializer.rb
  attributes :id, :restaurant

  def restaurant
    serializer = case @restaurant_type
                  when :base then RestaurantSerializer
                  when :detail then RestaurantDetailSerializer
                  else RestaurantSerializer
                  end

    serializer.new(object.restaurant, @options)

環境

- ruby: 2.6.2
- rails: 5.2.3
- active_model_serializers: 0.10.9 

構成

Model

app/models/restaurant.rb
# == Schema Information
#
# Table name: restaurants
#
#  id                   :bigint(8)        not null, primary key
#  name                 :string           not null
#  phone                :string           not null
#  address              :string           not null

class Restaurant < ApplicationRecord
  has_many :reservations
app/models/reservation.rb
# == Schema Information
#
# Table name: reservations
#
#  id                   :bigint(8)        not null, primary key
#  restaurant_id        :bigint(8)        not null

class Reservation < ApplicationRecord
  belongs_to :restaurant

API

reservations_list.json
{
  "reservations": [
    {
      "id": 42,
      "restaurant": {
        "id": 42,
        "name": "Domino's Pizza"
      }
    }
  ]
}
reservation_detail.json
{
  "reservation": {
    "id": 42,
    "restaurant": {
      "id": 42,
      "name": "Domino's Pizza",
      "phone": "03-1234-5678",
      "address": "東京都XXXXXX"
    }
  }
}

やりたいこと

  • Reservations List API では restaurant の id, name のみを含める
  • Reservation Detail API では restaurant の id, name, phone, address を含める

実現方法

Controller

  • restaurant_type を Serializers に渡す
reservations_controller.rb
module API
  class ReservationsController < API::ApplicationController
    def index
      reservations = Reservation.all
      render json: reservations,
              include: '**', restaurant_type: :base
    end

    def show
      reservation = Reservation.find(params[:id])
      render json: reservation,
              include: '**', restaurant_type: :detail
    end
  end
end

Serializers

  • initialize で options[:restaurant_type] を受け取る
  • options[:restaurant_type] の値によって, Serializer を切り替える
  • @options を 渡すことで, options や scope を 引き継ぐ
reservation_serializer.rb
class ReservationSerializer < ActiveModel::Serializer
  attributes :id, :restaurant

  def initialize(object, options = {})
    super
    @options = options
    @restaurant_type = options[:restaurant_type]
  end

  def restaurant
    serializer = case @restaurant_type
                  when :base then RestaurantSerializer
                  when :detail then RestaurantDetailSerializer
                  else RestaurantSerializer
                  end

    serializer.new(object.restaurant, @options)
  end
end
restaurant_serializer.rb
class RestaurantSerializer < ActiveModel::Serializer
  attributes :id, :name
end
restaurant_detail_serializer.rb
class RestaurantDetailSerializer < RestaurantSerializer
  attributes :address, :phone
end

まとめ

  • 今回は牧歌的な方法で切り替えました
  • 良い方法があれば教えてください :bow:

参考

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

ステップバイステップで学ぶCapistrano 3によるRails 5.2 + puma + nginxのデプロイ

このドキュメントを書いた理由

qiita内も含め、同じような事例は数多く公開されているが、残念ながらイチから学ぶ上ではほとんど参考にならなかった。

なぜなら重要なのは、最終的な作業内容や設定ファイルの中身ではないからだ。それらはソフトウェアの構成やバージョンが変われば容易に変化する。

本当に重要なのは、デプロイ作業がどんなステップで成り立っていて、各ステップで何を目的とし、そのために最低限どんな設定が必要なのか、理解することだ。

そういうわけで、自分で考えて調べて実行したので、結果をまとめることにした。

方針

以下の6ステップに分割して作業を進めた。概ね、各ステップがCapistranoの一プラグインに対応している。つまり一つずつプラグインを追加していくイメージである。

  1. ssh/gitによるファイルの配置
  2. rbenvの動作確認
  3. bundlerによるgemインストール
  4. Railsの設定
  5. pumaの起動
  6. nginx と pumaの連携

可能な限り、次の2つの指針をもって進めた。

  • デフォルト値が妥当な場合は、設定を書かない
  • 1回きりとわかっている手順は、手動で行う

デフォルト値が妥当な場合は、設定を書かない

そのステップで本当に必要な設定が、一目でわかるようにするためである。

もちろん、人によって構成によって必要な設定は異なる。異なるからこそ、他人の設定ファイルをコピペするのではなく、公式ドキュメントを読んで一つずつ妥当性を判断すべきである。全部まとめてやるのは大変だが、一度に1プラグインなら難しくない。

1回きりとわかっている手順は手動で行う

Capistranoは独自のタスクを定義できるから、ついカッコよく自動化してみたくなる。しかしそれは「早すぎる最適化」というものだ。結局今回は、カスタムタスクは作らなかった。

環境情報とドキュメント化の範囲

サーバOS: Debian 9 stretch
クライアントOS: Ubuntu 18.04.2 LTS on WSL 1

  • Ruby 2.6.3 on rbenv
  • Rails 5.2.3
  • puma 3.12.1
  • Capistrano 3.11.0
  • SQLite

各ソフトウェアのインストールには触れない。ユーザを追加したりSSH鍵をセットアップしたりといった手順にも触れない。またプラグインを追加した後のbundle installなど、自明な手順はしばしば省略する。

使用するサーバはweb/app/dbを兼ねる1台のみ、productionオンリー、しかもDBはsqlite。オモチャのようなアプリであるが、手順を学ぶ題材にはちょうど良かった。

デプロイ手順の説明

前置きが長くなった。具体的な作業に入る。Capstranoのインストールから。

Gemfile
# Use Capistrano for deployment
group :development do
  gem 'capistrano'
end

bundle installの後、cap install を実行して設定ファイルを作る。今回はproductionの分しか作らない。

bundle exec cap install STAGES=production

Capistranoの設定は、Railsアプリと共通のGemfileconfig/以下に書き込んでいく。しかし事実上、「独立したデプロイ用のプログラムを作る」と考えた方がいい。

例えばCapistrano関係の設定を変更しても、その都度 git commit / push する必要はない。Capistranoは実行時のRailsアプリとは無関係だ。

1. ssh/gitによるファイルの配置

目的

  • 素のCapistranoで、ssh/gitを用いたファイル配置のみを行う。

前提条件

  • サーバにSSH接続できること
  • 接続したユーザで配置先のディレクトリに書き込めること
  • サーバからGitリポジトリに接続できること

方針通り、最低限の設定のみ記す。必要なのはまずアプリケーションの名前と、gitリポジトリのURLと、サーバ上での配置先。

config/deploy.rb
set :application, 'myapp'
set :repo_url, 'git@github.com:myapp.git'
set :deploy_to, "/var/www/apps/myapp"

それからサーバのアドレスとユーザ名。Capistranoは実行ユーザの ~/.ssh/config を(完全ではないが)認識するので、sshコマンドで普通に接続できるなら、改めてSSH関連の設定はしなくていい。

config/deploy/production.rb
server "myserver", user: "tkyk", roles: %w{app db web}

デプロイ実行。

bundle exec cap production deploy

指定した配置先にCapistranoが管理するディレクトリ構造が作られ、gitからファイルがチェックアウトされる。サーバにログインして確認しておこう。

2. rbenvの動作確認

目的

  • 使用したいバージョンのRubyがrbenv経由で使える(Capistranoが認識する)ことを確認する。

前提条件

  • rbenvおよび使用するバージョンのRubyがインストールされていること

capistrano-rbenvプラグインを追加・有効化する。

Gemfile
group :development do
  #...
  gem 'capistrano-rbenv'
end
Capfile
require "capistrano/rbenv"

今回、rbenvはシステムレベルで(/usr/local/rbenv に)インストールしており、Rubyのバージョンは.ruby-version で指定する。この場合、設定は次のようになる。

config/deploy.rb
set :rbenv_type, :system
set :rbenv_ruby, File.read('.ruby-version').strip

rbenvの構成違いは、

  • rbenv_type
  • rbenv_ruby
  • rbenv_prefix

という3つの設定を組み合わせて対応する。たとえばrbenvをユーザレベルでインストールして、かつ.ruby-versionを使用しないなら、次のような設定になるはず(動作確認はしてない)。

config/deploy.rb
set :rbenv_type, :user
set :rbenv_ruby, '2.6.3'
set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec"

deployの実行。

bundle exec cap production deploy

この段階では実質的に何も実行しないが、設定を間違えたりして必要なバージョンが見つからない場合、警告が出てデプロイが中止される。

3. bundlerによるgemのインストール

目的

  • Rails他、アプリケーションが必要とするgemをbundlerでインストールする

前提条件

  • 使用するバージョンのRubyにおいてbundlerがインストールされていること

capistrano-bundlerプラグインを追加。

Gemfile
group :development do
  #...
  gem 'capistrano-bundler'
end
Capfile
require "capistrano/bundler

アプリケーションで使用するgemはリリース間で共有したいので、プラグインの標準の規約に従い、.bundle をlinked_dirsに追加する。ここに追加したディレクトリは shared ディレクトリ下に配置され、各リリースからはシンボリックリンクで参照される。

config/deploy.rb
append :linked_dirs, '.bundle'

他の設定はデフォルト値で問題なかったが、並列数だけはサーバスペックに合わせて設定すると良いと思う。デフォルトは4。

config/deploy.rb
set :bundle_jobs, 2

デプロイ実行。bundle installが走るので初回はかなり時間がかかるだろう。

bundle exec cap production deploy

4. Railsの設定

このステップはやや複雑で、おそらく構成による差異も大きいはず。

目的

  • Railsがリリース間で共有するリソースを定義する
  • データベースのmigrationを行う
  • assetコンパイルを行う

前提条件

  • 特になし

まずはcapistrano-railsプラグインを追加し、migrationおよびassetコンパイルを有効化する。

Gemfile
group :development do
  #...
  gem 'capistrano-rails'
end
Capfile
require "capistrano/rails/assets"
require "capistrano/rails/migrations"

第1の目的。railsがリリース間で共有するリソースを定義する。次のファイル・ディレクトリは、基本的にどんなRailsアプリでも該当するだろう。

config/deploy.rb
append :linked_files, "config/master.key"
append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets"

他にも tmp/storage とか public/uploads とか、アプリケーションによって必要なものを追加する。

こうしたリソースのうち、ディレクトリは自動で作られるが、ファイルはそうではない。よってconfig/master.key は予めサーバのshared以下にコピーしておこう。

scp config/master.key myserver:/var/www/apps/myapp/shared/config/

第2の目的。データベースのmigration。

独立したデータベースサーバがあるなら、database.ymlは普通、リポジトリに追加しないはず。その場合はlinked_filesにdatabase.ymlを追加し、何らかの手段で shared/config/database.yml を設置する。

一方、今回の私のアプリではsqliteを使うため、データベースファイル *.sqlite3 自体をリリース間で共有したい。そのため、database.ymlを次のように書き換え、1段階ディレクトリ階層を深くして……

config/database.yml
production:
  <<: *default
  database: db/production/production.sqlite3

このディレクトリを共有リソースとした(database.ymlはリポジトリ内で管理する)。

config/deploy.rb
append :linked_dirs, 'db/production'

第3の目的、assetコンパイルについては、特に設定する箇所がなかったのでデフォルトのまま。

デプロイ実行。migration, assetコンパイルが実行され、諸々シンボリックが作られる。

bundle exec cap production deploy

5. pumaの起動

目的

  • pumaを起動できるようにする
  • pumaプロセスのステータスをローカルから確認できるようにする
  • デプロイ完了時にpumaプロセスを再起動させる

前提条件

  • 特になし

capistrano-pumaプラグインのインストールと有効化。もちろんpuma自体まだインストールしていないなら、それも。

Gemfile
# Rails 5.2.3でrails newしたら最初から入っていた
gem 'puma', '~> 3.11'

group :development do
  #...
  gem 'capistrano3-puma'
end
Capfile
require 'capistrano/puma'
install_plugin Capistrano::Puma

とりあえず動かしてみたいなら、プラグインに設定ファイルを自動生成してもらうのが簡単である。

bundle exec cap production puma:config

このコマンドで、サーバ上の shared/puma.rb に設定ファイルが作られる。

(すでにサーバ上にpuma gemをインストール済みなら)以下のようなコマンドで、起動や終了、ステータスを確認できる。

bundle exec cap puma:start
bundle exec cap puma:stop
bundle exec cap puma:status

デプロイ実行。完了時に再起動されることを確認しよう。

bundle exec cap production deploy

6. nginx と puma の連携

目的

  • nginxからUNIXドメインソケットを通してpumaにアクセスできるようにする

前提条件

  • nginxがインストールされていること

capistrano-pumaのnginx用プラグインを有効化。

config/deploy.rb
install_plugin Capistrano::Puma::Nginx

これまた、とりあえず動かしたいなら、設定ファイルは自動生成すると良い。

bundle exec cap production puma:nginx_config

これで /etc/nginx/sites-available/myapp_pruduction のようなパスに設定ファイルが作られ、sites-enabled からはシンボリックリンクが作られる(当然、ディレクトリへの書き込み権限が必要。もし書き込み権限を与えたくないなら rails g capistrano:nginx_puma:config でローカルに設定ファイルを生成できる)。

あとは必要に応じて設定ファイルを修整、例えばnginxのデフォルトサイト(sites-enabled/default)を削除したりして、nginxをリロードすれば完了。

sudo nginx -t && sudo systemctl reload nginx

前ステップでpumaが起動しているなら再デプロイは不要なはず。httpアクセスして動作確認しよう。

終わりに

pumaプラグインが設定ファイルを自動生成してくれるおかげで、動作させるだけならとても簡単だった。もちろん実運用においては、それら設定ファイルの管理ポリシーが必要だが。

一連の過程において唯一引っかかったのは、以下の問題だった。

capistrano3-pumaのアップデートで起きたバグ解決

結局これも、デフォルトが妥当なら上書きしない、という方針で回避できた。

(とはいえcapistrano-rbenvの場合、デフォルト値が何なのか公式ドキュメントに書いてないのが困りものである。例示されている設定はデフォルト値ではない)

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

rails でflashメッセージを作成(toastrを使う)

はじめに

忘れそうなのでメモしました。

gemインストール

gem "toastr-rails"
$ bundle install
assets/javascripts/application.js
//= require jquery3
//= require popper
//= require bootstrap-sprockets
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require toastr #これを追加します。
//= require_tree .
assets/stylesheets/application.scss
@import "toastr"; #これを追加します。

もし、sassを使っていなければ、

assets/stylesheets/application.css
*= require toastr

これで準備完了です。

使う前にちゃんと使えるかテストして見ます。
application.html.erbに以下のように追加します。

application.html.erb
<script type="text/javascript">
  toastr.success("hello")
</script>

これを追加してページにアクセスすると以下のようなアラートが右上に出現します。

スクリーンショット 2019-07-04 16.50.54.png

色はこの4色みたいです。以下参考

application.html.erb
<script type="text/javascript">
  toastr.success("hello")
  toastr.warning("hello")
  toastr.error("hello")
  toastr.info("hello")
</script>

スクリーンショット 2019-07-04 18.22.13.png

flashメッセージに適応する

まず、前提としてflashのシンボルは :alert と :notice の2つです。(deviseを使ってたのでその2つになりました)ホントは :error と :successのほうが都合がいいのですが。。。
flashメッセージがある時適宜flashを使いたいのでapplication.html.erbに記述します。

layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>InstagramClone</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  </head>

  <body>
    <%= render 'layouts/header' if user_signed_in? %>
    <%= yield %>
    <%= render "layouts/footer" %>

<!--ここから下を追加しています。----------------------- -->
    <% if flash.any? %>
      <script type="text/javascript">
        <% flash.each do |key, value| %>
          <% key = "success" if key == "notice" %>
          <% key = "error" if key == "alert" %>
          toastr['<%= key %>']('<%= value %>');
        <% end %>
      </script>
    <% end %>
<!--ここまでです----------------------------------- -->
  </body>
</html>

まず、if flash.any? flashがあるかないかを判定してます。その次に見た目が奇妙なのですが、
scriptタグでflashのeach分を囲ってます。

でtoastrで扱うメソッドが :info :success :error :warning の4つです。しかし、flashのシンボルが :alert と :notice になっているため、変換して上げる必要があります。
その記述が以下の2つになります。

<% key = "success" if key == "notice" %>
<% key = "error" if key == "alert" %>

:notice を :success に :alert を :error に変換してます。
最後に値をtoastrに入れて終わりです。

おまけ

こんな書き方も出来るみたいです。

layouts/application.html.erb
<% if flash.any? %>
  <script type="text/javascript">
    <% flash.each do |key, value| %>
      <% type = key.to_s.gsub('alert', 'error').gsub('notice', 'success') %>
      toastr.<%= type %>('<%= value %>')
    <% end %>
  </script>
<% end %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Form_forでプルダウンメニューの項目をDBから選択する

概要

select_tagによる、決め打ちの選択肢 ↓
<%= f.select :subject, [["1", "1"], ["2", "2"]] , class:"form-control" %>
ではなく、DBのテーブルから項目を引っ張ってきたい時 :fist_tone4:

今回の例

都市を入力するページで、国をプルダウンで選択させたい。

Countries (国のテーブル) ※数が増えることがないのでdb/*seed.rbに記載済み :flag_ca: :flag_jp:

id name
1 Australia
2 Canada
3 Japan

has_many :cities
国は都市をたくさん持っています。

Cities (都市のテーブル):tokyo_tower: :shinto_shrine: :metro:

id name country_id
1 Tokyo 3
2 Osaka 3
3 Kyoto 3

belongs_to :countries

collection_select を使う

app/views/cities/new.html.erb
<%= form_for @city do |c| %>
<%= c.label :country_id %>
<%= c.collection_select :country_id, Country.all, :id, :country_name %>

collection_selectの文法

<%= f.collection_select <属性名>, <プルダウンメニュー表示用の配列データ>, <valueとして扱うカラム名>,  <表示用のカラム名>, <オプション> %>

オプション

:prompt

prompt オプションを使うと、プルダウンメニューが選択されていない時に設定した文字列の行が先頭に追加される。

<%= form_for @city, url: admins_cities_new_path do |c| %>
       <%= c.label :country_id %>
       <%= c.collection_select :country_id, Country.all, :id, :country_name,
                               :prompt => "国を選択" %>

これで選択していない時は、 国を選択が表示されます :fist_tone4:

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

Git bashでrails consoleが開けないときの解決策

はじめに

git.png

WindowsのGit bashでrails cを行うと Switch to inspect mode.と出てきてしまい後は何も出ません。

一応入力することは出来ても真っ黒な画面なので気持ち悪いです:sweat_smile:

解決策

Windowsのプロンプトでは、駄目でしたが、VS code内でGit bashを開くとrails cを開くと何故かは分かりませんが、うまく開くことが出来ました。

git4.png

まとめ

今回偶然うまく行って良かったのですが、ただ、windowsのプロンプトの方は駄目で、VS codeでやるとうまく行った理由が分からないのでそこが気になります。

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

【Rails】RSpecでFactoryBotをcreateしようとしたらErrorが吐き出されたので調べた

RSpecを書いたところ、FactoryBotからuserをcreateするところでエラーが発生してしまうので、原因を調べてみた。

事象

spec/factories/user.rb
FactoryBot.define do
  factory :user do
    name { "テストユーザー" }
    email { "test_user@test.co.jp" }
    password { "12345678" }
  end
end
user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  let(:user) { user :user }

  describe "バリデーション" do

    it "nameが空だとinvalid" do
      user.name = ""
      expect(user).not_to be_valid
    end
  end
end

rspecを実行したところ、エラーが発生。
ちなみに今回、Userモデルのvalidatesは:name, presence: trueだけとする。

Failures:

  1) User バリデーション nameが空だとinvalid
     Failure/Error: let(:user) { create :user }

     ActionView::Template::Error:
       Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true
     # ./spec/models/client_spec.rb:4:in `block (2 levels) in <top (required)>'
     # ./spec/models/client_spec.rb:9:in `block (3 levels) in <top (required)>'
     # ------------------
     # --- Caused by: ---
     # ArgumentError:
     #   Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true
     #   ./spec/models/client_spec.rb:4:in `block (2 levels) in <top (required)>'

Finished in 0.52519 seconds (files took 2.18 seconds to load)
1 example, 1 failure

何やらMissing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true というエラーが吐き出されている。

結論

今回はdeviseを使っているのだが、メール認証機能のconfirmableを有効にしている。

user.rb
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :confirmable

メール認証機能とは→アカウント新規作成時に登録したアドレスにメールが送られ、確認用リンクをクリックするとログイン画面からログイン可能となる。
このリンクをクリックしたタイミングで、Userのconfirmed_atカラムにその時刻がsaveされる。
どうやらconfirmableが有効の場合、このconfirmed_atがpresentかnilかを見分けて、そのユーザーがログイン可/不可を条件分けしているらしい。
(詳細までは調べられてません)

従って、FactoryBotのファイルにconfirmed_atを書き加えると

spec/factories/user.rb
FactoryBot.define do
  factory :user do
    name { "テストユーザー" }
    email { "test_user@test.co.jp" }
    password { "12345678" }
    confirmed_at { Date.today }
  end
end
.

Finished in 0.2486 seconds (files took 4.21 seconds to load)
1 example, 0 failures

無事、rspecが通りました。

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

【Rails】FactoryBotをcreateしようとしたらErrorが吐き出されたので調べてみた

RSpecを書いたところ、FactoryBotからuserをcreateするところでエラーが発生してしまうので、原因を調べてみた。

事象

spec/factories/user.rb
FactoryBot.define do
  factory :user do
    name { "テストユーザー" }
    email { "test_user@test.co.jp" }
    password { "12345678" }
  end
end
user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  let(:user) { user :user }

  describe "バリデーション" do

    it "nameが空だとinvalid" do
      user.name = ""
      expect(user).not_to be_valid
    end
  end
end

rspecを実行したところ、エラーが発生。
ちなみに今回、Userモデルのvalidatesは:name, presence: trueだけとする。

Failures:

  1) User バリデーション nameが空だとinvalid
     Failure/Error: let(:user) { create :user }

     ActionView::Template::Error:
       Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true
     # ./spec/models/client_spec.rb:4:in `block (2 levels) in <top (required)>'
     # ./spec/models/client_spec.rb:9:in `block (3 levels) in <top (required)>'
     # ------------------
     # --- Caused by: ---
     # ArgumentError:
     #   Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true
     #   ./spec/models/client_spec.rb:4:in `block (2 levels) in <top (required)>'

Finished in 0.52519 seconds (files took 2.18 seconds to load)
1 example, 1 failure

何やらMissing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true というエラーが吐き出されている。

結論

今回はdeviseを使っているのだが、メール認証機能のconfirmableを有効にしている。

user.rb
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :confirmable

メール認証機能とは→アカウント新規作成時に登録したアドレスにメールが送られ、確認用リンクをクリックするとログイン画面からログイン可能となる。
このリンクをクリックしたタイミングで、Userのconfirmed_atカラムにその時刻がsaveされる。
どうやらconfirmableが有効の場合、このconfirmed_atがpresentかnilかを見分けて、そのユーザーがログイン可/不可を条件分けしているらしい。
(詳細までは調べられてません)

従って、FactoryBotのファイルにconfirmed_atを書き加えると

spec/factories/user.rb
FactoryBot.define do
  factory :user do
    name { "テストユーザー" }
    email { "test_user@test.co.jp" }
    password { "12345678" }
    confirmed_at { Date.today }
  end
end
.

Finished in 0.2486 seconds (files took 4.21 seconds to load)
1 example, 0 failures

無事、rspecが通りました。

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

TECH ~Day11~

学習内容
・フォーム
・pry-rails
・binding-pry

・フォーム
ユーザーが情報を入力し、その情報をサーバーに送信するためのもの。HTMLコードの中にform要素を作成し、その中にフォームを構成する部品のinput要素やtextarea要素を配置することで作成できる。

Railsでフォームから情報を発信するには「form_tag」をはじめとするヘルパーメソッドを使う。

・pry-rails
Rails向けのデバックツール。
バグの有無、処理を止めて、ソースコードが正しいのかチェックするツール。

・binding-pry
これは、binding.pryという文字列をソースコードの中に記述することで、binding.pryという文字列が存在する部分でRailsの処理を止めることができるというものです。

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

TECH ~Day9~

学習内容
・erbファイルでrubyコードを使うとき
・改行の適応
・ヘルパーメソッド

・erbファイルでrubyコードを使うとき
Railsの見た目用のファイルである「erb」ファイルの中でRubyのコードを利用する場合、「<%= %>」という記述で囲う。
<%= %>を利用するにはビューの拡張子が、「erb」になっている必要がある。

・改行の適応
railsは「simple_format」というメソッドを使わなければ改行が適応されない。
(復習)
 ①改行→

 ②文字列を→

で括る

・ヘルパーメソッド
simple_formatのようにRailsではviewでHTML加工をするために、予めのメソッドが用意されている。これらをまとめて「ヘルパーメソッド」と呼ぶ。他にはform_tag(フォームを出現)やlink_to(aタグを出現)などがある。

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

TECH ~Day10~

学習内容
・erbファイルでrubyコードを使うとき
・改行の適応
・ヘルパーメソッド

・erbファイルでrubyコードを使うとき
Railsの見た目用のファイルである「erb」ファイルの中でRubyのコードを利用する場合、「<%= %>」という記述で囲う。
<%= %>を利用するにはビューの拡張子が、「erb」になっている必要がある。

・改行の適応
railsは「simple_format」というメソッドを使わなければ改行が適応されない。
(復習)
 ①改行→<br/>
 ②文字列を→<p>で括る

・ヘルパーメソッド
simple_formatのようにRailsではviewでHTML加工をするために、予めのメソッドが用意されている。これらをまとめて「ヘルパーメソッド」と呼ぶ。他にはform_tag(フォームを出現)やlink_to(aタグを出現)などがある。

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

TECH ~Day9~

学習内容
・一つのデータをテーブルから取り出す
・content カラムの値を取り出す
・全てのデータをテーブルから取り出す
・配列のデータから投稿内容を取り出す

・一つのデータをテーブルから取り出す
書き方
 $ rails console
  post = Post.first
!Post.firstの書き方でpostsテーブルにある最初のデータを取得できる。

・contentカラムの値を取り出す
書き方
 $ rails console
  post = Post.first
  post.content
 ="~~~~~"
!  post = Post.first
  post.content
この二つの流れによってPost.firstで得たデータの投稿内容を取得できる。

・全てのデータをテーブルから取り出す
先ほど一つのデータの取り出し方で書いた「Post.first」のfirstの部分を「all」にすることで全てのデータをテーブルから取り出すことができる。

書き方
 posts = Post.all

・配列のデータから投稿内容を取り出す
Post.all[0]はPost.firstと同じ内容のデータ。
これを「Post.all[0].content」と書くことで投稿内容を取得できる。

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

google-api-client,optimist(trollop)を使うと出てくる3つのエラーとその解決法

概要

Youtube APIを使用し動画情報を 取得し一覧表示するサイトを作成中にハマってしまったエラーが3つありましたので、エラーパターンと解決法を書き残しておきます。
Gemのバージョンにも関わってきますし、デプロイしているサーバの環境にも依存しますので、このエラー現象と解決方法は2019年7月段階のものだということを念頭に置いてください。

環境

  • Ruby 2.4.1
  • rails 5.2.3
  • google-api-client 0.8.6
  • optimist 3.0.0(旧trollop)
  • サーバ
    • heroku

エラーパターン1:oprimist(trollop)のエラー

herokuのlog
2019-06-18T11:44:23.578660+00:00 app[web.1]: Error: unknown argument '-p'.
2019-06-18T11:44:23.578668+00:00 app[web.1]: Try --help for help.

このパターンはoptimist::optionsメソッドにコマンドライン引数がぶっこまれていることが原因です。
heroku上だと-pと-eオプションを付けた状態でrails sされます。
参考(optimistのソース):https://github.com/ManageIQ/optimist

ローカル環境でサーバを起動する際にrails s だけで起動している場合は起きないのにheroku上だと上記のエラーが出てきます。ためしにローカル環境でrails s -p ポート番号(3000) -e development で起動して実行するとheroku上で動かした時と同じエラーが出ることが確認できます。
もしくはbyebugでoptimist::optionsをコールする直前のARGVの値を参照してみると確認できると思います。

エラーパターン1:解決法

1.ARGVの値をローカル変数に退避
2.ARGVを空にする
3.optimist::optionsをコール
4.ARGVの値を元にもどす

修正コード
arg_array_save = []
arg_array_save = ARGV.shift(4)
opts = Optimist::options do
     ・・・中略・・・
arg_array_save.each do |arg_push|
  ARGV.push(arg_push)
end

こんな感じになります。ARGVはメソッド内で値を直接操作すること(代入等)ができないので、shift,push等のメソッドを使用していじっています。
もっといい方法があればぜひコメントで教えて頂きたいです。

エラーパターン2:ArgumentError

herokuのlog
2019-06-24T14:54:19.983862+00:00 app[web.1]: F, [2019-06-24T14:54:19.983799 #4] FATAL -- : [d9d33d3a-e4a5-4ba3-a0f6-7286855ae09a] ArgumentError (header field value cannot include CR/LF):

このパターンは上記のパターン1を解決した後にでてくる可能性があります。
原因はgoogle-api-clientのバージョンとrubyのバージョンがうまく対応できてないみたいです。gem内のソースコードレベルの原因は解明できてません。gem内の解析めんどくさい

解決法は2つありますが、解決法パターン1推奨です。

エラーパターン2:解決法パターン1

Rubyのバージョンを 2.4.1にする。
なぜかわかりませんが2.4.6から2.4.1にバージョンを落とすと解決しました。エラーメッセ的には改行コードだと思うので、その辺の処理がバージョンによって違うのでしょうか。
わかる人いたら教えてください。

エラーパターン2:解決法パターン2

google-api-client を最新のバージョンにする。
2019年7月現在だと最新は0.30.4です。
Gemを最新にするとエラーパターン2のエラーメッセージは消えるのですが、後述するエラーパターン3の状態に陥ってしまうので、この解決法はあまりお勧めできないです。
GitHubのReadmeを全部翻訳してバージョンによる変更点を全部理解すると解決できるかもしれないです。

エラーパターン3:LoadError

herokuのlog
2019-06-20T16:12:55.170597+00:00 app[web.1]: /app/vendor/bundle/ruby/2.4.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:33:in `require': No such file to load -- google/api_client.rb (LoadError)

ファイルが読み込めてないみたいですが、heroku内のファイルとかいじれるんでしょうか。

エラーパターン3:解決法

boot.rbのbootsnapをOFFにする。

これでエラーメッセージ自体は消えますが、また同じようなエラーが今度はactivesupportで出てきます。
この解決ルートは永遠と上記の解決法を繰り返す羽目になりそうだったので、やめました。
ですので、エラーパターン3の明確な解決法はわかってない状態です。

まとめ

  • エラーパターン3の最終的な解決法は不明。エラーパターン2でrubyのバージョンを変えて解決する方が現実的。
  • Gemのバージョンアップなどで仕様が変わるのでReadmeを読まないとわからない。
  • 英語を勉強する必要がある。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミング初心者はまずRailsチュートリアルをやるべきなのか?

Railsチュートリアルは初心者がやるべきか?

プログラミング初心者の流れとして、最近はProgateやドットインストールをやって基礎を学んで、そして次の段階でRailsチュートリアルをやろう!というようなものが結構多いように感じます。
Railsチュートリアルは、手を動かしものを作りながら学習を進められるとてもいい教材です。しかし、これをプログラミングを始めたばかりの初心者が取り組むべきなのでしょうか?

個人的な意見

私の個人的な意見としては、NOです。特に独学の場合はあまりお勧めできないと思っています。
先ほど書いたようにRailsチュートリアルはとてもいい教材です。しかしながら、初心者向けかというとちょっと違うかな、と思います。

なぜNO?

なぜ初心者向けではないと思うか、その最たる理由は初心者のレベル感は大きく異なるということです。
初心者といってもできることはかなり違ってくると思っています。
例えば、ProgateでRubyのコースを終えた方はプログラミング初心者と言えるでしょう。また一方で、自分でとりあえずは開発環境を作って、Progateの知識をつかって何か書いてみた!みたいな人もプログラミング初心者ということもできるでしょう。
つまり同じプログラミング初心者といってもできることの幅が大きく違うのです。そもそも初心者の線引きは人それぞれですしね。

じゃあいつ、どうやってやるべきなの?

さて、NOとはいったものの、永久に基礎練を続けていては意味がありません。ではいつやるべきなのか。
ベストなタイミングは挑戦を何度もしてみて、行けそうだなと感じたときですが、これだと元も子もないので、ちょっとした自分なりの基準を置きたいと思います。

まずは何を勉強したうえでやるべきか。

ここはProgateが個人的にも好きな教材なので、それを例にして書いていきます。
Progateでこれはやっておいておくべき、というものは

  • Html&CSS
  • Ruby
  • Rails
  • Command line
  • SQL

です。このすべてをやって、少し足りないかな、という感じです。
さらに、ここに加えてWebの知識を付けておくべきだと私は思います。というのも、Railsは簡単にアプリケーションが作れてしまうため、何がどう動いているのかわからなくても、なんとなくものを作れてしまいます。しかし、なんとなくでやっているので知識として定着しなかったり、他の言語への知識の転用ができなかったりします。
なので、どんな形でもいいのでWebの知識をつけておくとベストです。

おすすめのやり方

上に書いた勉強をしたうえでいよいよRailsチュートリアルに入っていきます。
そしてその時のおすすめのやり方ですが、

  • Heroku(デプロイ)
  • Bitbucket(バージョン管理)
  • テスト

このすべてを飛ばしてまずは一周することです。
この部分がRailsではとても難しい部分になると思います。
特にテストで詰まると進まなくなってしまうので、まずは何か作り切る!というところに焦点をおいて、作ることに関係のない、上の三つの難しい部分は飛ばしてしまいましょう。
そして二週目でここの部分も余裕があれば触り、勉強していくと詰まらずにすすめると思います!

終わりに

私はプログラミングを始めたとき、一緒に勉強する仲間がいました。しかし、Railsチュートリアルで自分以外が挫折してしまい、気づけば一人になってしまっていました。
適切なタイミング、適切な知識で勉強すればRailsチュートリアルはとてもいい教材です。しかし、その一方で、多くのプログラミング初心者を挫折させてしまっているものでもあります。(TwitterでRailsチュートリアルを始めて気づけば消える方の多い事!)
一人でもそんな人を減らせるように願います。

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

Rails6 のちょい足しな新機能を試す46(multidb connects_to 編)

はじめに

Rails 6 に追加されそうな新機能を試す第46段。 今回は、 multidb connects_to 編です。
Rails 6 では、 複数DBに対応したため、DB接続先を切り変えることができるようになっています。

Ruby 2.6.3, Rails 6.0.0.rc1 で確認しました。Rails 6.0.0.rc1 は gem install rails --prerelease でインストールできます。

$ rails --version
Rails 6.0.0.rc1

今回の準備

今回は、 Rails6 のちょい足しな新機能を試す35(multidb migration --database オプション編)
をベースに作業を進めます。

library 側にも users テーブルを追加する

backbone 側に users テーブルが存在しますが、 library 側にも users テーブルを追加します。

$ bin/rails g migration CreateUser name --db=library

db:migrate を実行する

bin/rails g db:migrate

User モデルを作る

User モデルを作ります。

app/models/user.rb
class User < ApplicationRecord
end

Book モデルを作る

Book モデルを作ります。
books テーブルは、 backbone にはなく、 library 側にありますので、DB接続先を connects_to を使って明示的に設定します。

app/models/book.rb
class Book < ApplicationRecord
  connects_to database: { writing: :library, reading: :library }
end

Rails 6.0.0rc1 では、 :writing:reading の両方を指定する必要があるようです。

  connects_to database: :library

だとエラーになりました。

今回は、Book クラスに直接 DB 接続先を記載しましたが、もし、Bookモデルの他にも、 library 側に接続する接続するモデルがあれば、 ApplicationRecord から派生させた抽象クラスを作成して、抽象クラスの中で、 connects_to を使い、 Book クラスは、その抽象クラスから派生させた方が良いでしょう。(Rails Guide ではそういう書き方になってます。)

seed データを作る

backbonelibrary 側双方に users テーブルが存在するため、両方のDBに接続してデータを登録するように、 ActiveRecord::Base.connected_to を使います。

books テーブルは、 library 側に存在しますが、 Book クラスで接続先を library に指定しているため、 ActiveRecord::Base.connected_to を使う必要がありません。

db/seeds.rb
User.create(name: 'Taro')

ActiveRecord::Base.connected_to(database: :library) do
  User.create(name: 'Hanako')
end

Book.create(title: 'Programming Ruby')

seed データを登録する

$ bin/rails db:seed

controller と View を作る

登録した seed データを表示するため、 controller と view を作ります。

bin/rails g controller Dashboard index

User モデルにメソッドを追加する

library の users テーブルを検索できるように User モデルに all_in_library メソッドを追加します。
ActiveRecord::Base.connected_to を使って library 側に接続先を変更してから all メソッドを呼び出します。

app/models/user.rb
class User < ApplicationRecord
  def self.all_in_library
    ActiveRecord::Base.connected_to(database: :library) do
      all
    end
  end
end

DashboardController#index メソッドを修正する

登録したデータを取得するように index メソッドを修正します。

app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
  def index
    @books = Book.all
    @users = User.all.to_a
    @users_in_library = User.all_in_library
  end
end

ここで、 メソッドの2行目を @user = User.all.to_a としている理由は後述します。

データを表示する View を作成する

各変数の値を表示するための View を作成します。

app/views/dashboard/index.html.erb
<h1>Dashboard#index</h1>

<table>
  <thead>
    <tr>
      <th>
        valiable
      </th>
      <th>
        value
      </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        @books
      </td>
      <td>
        <%= @books.map(&:title) %>
      </td>
    </tr>
    <tr>
      <td>
        @users
      </td>
      <td>
        <%= @users.map(&:name) %>
      </td>
    </tr>
    <tr>
      <td>
        @users_in_library
      </td>
      <td>
        <%= @users_in_library.map(&:name) %>
      </td>
    </tr>
  </tbody>
</table>

rails server を起動してブラウザで表示する

$ bin/rails s

http://localhost/dashboard/index にアクセスすると値が表示されます。
index_ok.png

User.all ではなく User.all.to_a な理由

User.all にすると、 Taro の値が表示されず Hanako になってしまいます。

index_ng.png

これは、 all メソッドが ActiveRecord::Relation を返すだけで、 実際にSQLを発行しないためです。 このため、 all にすると、 View を rendering するときに、SQLが発行されてしまい、接続先が library に設定された状態で SQLが発行されてしまうようです。
これを防ぐために、DB の接続が library に変わる前に SQLを発行するため、意図的に to_a をつけました。

app/controllers/dashboard_controller.rb
  def index
    @books = Book.all
    @users = User.all # => ActiveRecord::Relation が返され SQLは実行されない。
    @users_in_library = User.all_in_library # 接続先が library に変わる

    # このあと index View をレンダリングする際に、全てのSQLが発行される。
  end

User.all の場合、View のレンダリングの際にすべての SQL が発行されていることがログからもわかります。

  Rendering dashboard/index.html.erb within layouts/application
  Book Load (0.3ms)  SELECT "books".* FROM "books"
  ↳ app/views/dashboard/index.html.erb:20:in `map'
  User Load (0.3ms)  SELECT "users".* FROM "users"
  ↳ app/views/dashboard/index.html.erb:28:in `map'
  User Load (0.5ms)  SELECT "users".* FROM "users"
  ↳ app/views/dashboard/index.html.erb:36:in `map'

User.all.to_a とした場合、controller の中で SQL が発行されていることがわかります。

  User Load (0.4ms)  SELECT "users".* FROM "users"
  ↳ app/controllers/dashboard_controller.rb:4:in `index'
  Rendering dashboard/index.html.erb within layouts/application
  Book Load (0.3ms)  SELECT "books".* FROM "books"
  ↳ app/views/dashboard/index.html.erb:20:in `map'
  User Load (0.4ms)  SELECT "users".* FROM "users"
  ↳ app/views/dashboard/index.html.erb:36:in `map'

今回、試したコードが特殊な例かも知れませんが、挙動が少し紛らわしいので、今後のバージョンで修正されるかも知れませんね。

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails6_0_0rc1/tree/try046_multidb_connection_switching

参考情報

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

React+ReduxにRedux-Sagaを導入して非同期処理(axios)をさせる手順

ソースコード
https://github.com/tontoko/react-redux-saga-rails

↓こちらの記事を参考にまずReact+Rails(API)の環境を整えました。

Ruby on Rails+ReactでCRUDを実装してみた
https://qiita.com/yoshimo123/items/9aa8dae1d40d523d7e5d

非常に分かりやすいです。

Reduxの導入

sudo npm install redux react-redux --save

ActionCreator

crud-front/src/Actions/actions.js
export default {
    create: (data) => {
        return { type: 'CREATE', data }
    }, 
    update: (id, data) => {
        return { type: 'UPDATE', id, data }
    },
    delete: (id) => {
        return { type: 'DELETE', id }
    },
    init: () => {
        return { type: 'INIT' }
    },
}

Reducer

crud-front/src/Reducers/productsReducer.js
// 中味はまだ空

const initialState = {
    products: [],
    isFetching: false,
}

export default function productReducer(state = initialState, action) {
    switch (action.type) {
        case 'CREATE':
            return Object.assign({}, state, {
            })
        case 'UPDATE':
            return Object.assign({}, state, {
            })
        case 'DELETE':
            return Object.assign({}, state, {
            })
        case 'INIT':    
            return Object.assign({}, state, {
            })
        default:
            return state
    }
}
crud-front/src/Reducers/reducers.js
// Reducer達を一つに纏める
// 今回は必要ないけど。。

import {combineReducers} from 'redux'
import productsReducer from './productsReducer'

export default combineReducers({
    products: productsReducer,

});

index.js

crud-front/src/index.js
// -- 省略 --
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import Reducers from './Reducers/reducers'

const store = createStore(
    Reducers,
)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

MainContainer.js

crud-front/src/Components/MainContainer.js
// -- 省略 --
import { connect } from 'react-redux'
import Actions from '../Actions/actions'
import { bindActionCreators } from 'redux'

const mapStateToProps = state => {
    return state
}

const mapDispatchToProps = dispatch => {
    return {
        init: () => dispatch(Actions.init()),
        create: (data) => dispatch(Actions.create(data)),
        update: (id, product) => dispatch(Actions.update(id, product)),
        delete: (id) => dispatch(Actions.delete(id)),
    }
}

//// こっちでもいいかな
// const mapDispatchToProps = dispatch => {
//     return bindActionCreators(Actions, dispatch)
// }

export default connect(mapStateToProps, mapDispatchToProps)(MainContainer)

非同期処理をどこに書くべきか

Redux導入の雛形はできたものの、ここで問題が起きる。

reduxで非同期処理をするいくつかの方法(redux-thunk、redux-saga)
https://qiita.com/muiscript/items/63386fd65c7e9f06f5d4

Actionをプレーンに保てテストもしやすい、というRedux-Sagaを試してみます。

Redux-Sagaの導入

Redux-Sagaの概要については以下の記事もわかりやすいです。

redux-sagaで非同期処理と戦う
https://qiita.com/kuy/items/716affc808ebb3e1e8ac

【React】 redux-saga でAPIを叩く
https://k-tomoo.hatenablog.com/entry/2018/03/12/151045

まずはインストールします。

sudo npm install redux-saga --save

Saga部分をInit処理を例にとって書いていきます。

crud-front/src/Saga/Init.js
import axios from "axios"
import { put, call, takeEvery } from 'redux-saga/effects';

const initAjax = () => axios.get('http://localhost:3001/products')
    .then((res) => {
        const data = res.data
        console.log(data)
        return { data }
    })
    .catch((error) => {
        return { error }
    })

function* initProduct() {
    // 3.
    const { data, error } = yield call(initAjax);
    console.log(data)

    if (data) {
        // 4.
        yield put({ type: "INIT_SUCCEEDED", data });
    } else {
        // todo: エラーハンドリング
        // 今回はエラー処理は省きます
    }
}

// 1.& 2.
export default [takeEvery("INIT", initProduct)];

パッとみてもよくわからないと思うので順に見ていきます。

  1. takeEveryの第一引数で指定されたアクションがどこかで呼ばれる

  2. そのアクションの完了を待って第二引数が呼ばれる

  3. yield call()で中の関数が実行され、Promiseオブジェクトが帰ってくるまで待つ

  4. yield putで新たにActionをdispatchする

ざっくりこのような流れで非同期処理を実現しています。

function* などを見て戸惑った方は以下の記事がわかりやすいです。
https://qiita.com/kura07/items/cf168a7ea20e8c2554c6
https://qiita.com/kura07/items/d1a57ea64ef5c3de8528

Redux-Sagaを組み込む

crud-front/src/index.js
import createSagaMiddleware from 'redux-saga'
import { all } from 'redux-saga/effects'

import Init from './Saga/Init'
import Create from './Saga/Create'
import Update from './Saga/Update'
import Delete from './Saga/Delete'

// ここで一つにまとめます
function* rootSaga() {
    yield all([
        ...Init,
        ...Create,
        ...Update,
        ...Delete,
    ])
}

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
    Reducers,
    applyMiddleware(sagaMiddleware)
)

sagaMiddleware.run(rootSaga)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

呼び出し用、成功した処理用、エラー処理用と分けてやる必要があります。(エラー分は今回は省略)

crud-front/src/Actions/actions.js
export default {
    create: (data) => {
        return { type: 'CREATE', data }
    }, 
    createSuccess: (data) => {
        return { type: 'CREATE_SUCCEEDED', data }
    },
    update: (id, data) => {
        return { type: 'UPDATE', id, data }
    },
    updateSuccess: (id, data) => {
        return { type: 'UPDATE_SUCCEEDED', id, data }
    },
    delete: (id) => {
        return { type: 'DELETE', id }
    },
    deleteSuccess: (id) => {
        return { type: 'DELETE_SUCCEEDED', id }
    },
    init: () => {
        return { type: 'INIT' }
    },
    initSuccess: (data) => {
        return { type: "INIT_SUCCEEDED", data}
    },
}
crud-front/src/Reducers/productsReducer.js
import axios from "axios"

const initialState = {
    products: [],
    isFetching: false,
}

export default function productReducer(state = initialState, action) {
    switch (action.type) {
        case 'CREATE':
            return Object.assign({}, state, {
                isFetching: true,
            })
        case 'CREATE_SUCCEEDED':
            return Object.assign({}, state, {
                products: [...state.products, action.data],
                isFetching: false,
            })
        case 'UPDATE':
            return Object.assign({}, state, {
                isFetching: true,
            })
        case 'UPDATE_SUCCEEDED':
            const updateIndex = state.products.findIndex(x => x.id === action.id)
            const updatedProductsState = state.products
            updatedProductsState.splice(updateIndex, 1, action.data)
            return Object.assign({}, state, {
                products: updatedProductsState,
                isFetching: false,
            })
        case 'DELETE':
            return Object.assign({}, state, {
                isFetching: true,
            })
        case 'DELETE_SUCCEEDED':
            const deleteIndex = state.products.findIndex(x => x.id === action.id)
            const deletedProductsState = state.products
            deletedProductsState.splice(deleteIndex, 1)
            return Object.assign({}, state, {
                products: deletedProductsState,
                isFetching: false,
            })
        case 'INIT':    
            return Object.assign({}, state, {
                products: [],
                isFetching: true,
            })
        case 'INIT_SUCCEEDED':
            return Object.assign({}, state, {
                products: action.data,
                isFetching: false,
            })
        default:
            return state
    }
}

ここまでで導入は完了です。
後は例としてinit()を呼んでやります。

crud-front/src/Components/MainContainer.js
class MainContainer extends React.Component {
// -- いろいろ省略 --
    componentDidMount() {
        this.props.init()
    }

    render() {
        if (this.props.isFetching === true) {
            return (<div />)
        } else {
            return (
                <div className='app-main'>
                    <FormContainer createProduct={this.props.create} />
                    <ProductsContainer deleteProduct={this.props.delete} updateProduct={this.props.update} />
                </div>
            );
        }
    }
}
// -- いろいろ省略 --
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】namespace / 指定ディレクトリ以下にコントローラーを作成する

1つのアプリケーション内に user photographer admin
3つのログイン口があるマッチングアプリを作成中 :lemon: 学習の記録 :writing_hand_tone4:

概要

コントローラーやビューを生成する時、
それぞれ同じ名前になってしまうコントローラー名やビューの名前がありますよね。
例えば、user photographer admin ぜんぶContacts.controller(お問い合わせ用)を持っています。

そこでディレクトリを指定してコントローラーを生成してあげたら、
adminsディレクトリ下にあるものはadmins/contactsというURLとして呼び出してくれると思ったわけですが・・

ここでルーティング内の重複はどうなるんだ〜となりました。
そこでnamespace(名前空間)という便利なものを知ったのでメモします:sunny:

Namespace(名前空間)とは

名前の重複があるときに、衝突を防ぐために使用します。

前提

今回はgem deviseでログイン機能を実装したため、
すでに$rails g devise:controllers adminsコマンドでadminsディレクトリがあります。
そこにdevise関連ではないコントローラーも追加していきたい!という感じ。

とりあえずコントローラーを生成してみよう

rails generate controller admins/Contacts edit update

これだけ:relieved::v_tone4:

ルーティングは?

上のコマンドでコントローラーを生成した時点で、
namespace :admins do --- endという枠組みが作成されて、
namespace枠内に、GETやPOSTなど自分が追加したアクションがきちんとルーティングされています:clap_tone3: 今回はResourcesに書き換えました。

config/routes.rb
Rails.application.routes.draw do

  namespace :admins do
    resources :contacts, only:[:edit, :update]
  end
  # アドミンのcontacts

  resources :contacts, only:[:new, :create]
  # ユーザーのcontacts

  devise_for :photographers
  devise_for :admins
  devise_for :users

end

これで名前が一緒でも、
「このcontactsはadminね。」 「このcontactsはuserね。」という感じで
判別してくれるわけです :relaxed: :fist_tone3:

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

【Rails】undefined method ***_pathの解決策

学習の記録 :writing_hand_tone4:

***/edit.html.erb
<%= form_for @*** do |f| %>
  # 省略
<% end %>
NoMethodError undefined method `***_path'

が出てしまう現象が発生。
ルーティングを確認してもパスは合っている・・なぜ :sweat:

原因

form_forが自動的に生成してくれるパスは複数形のみらしいです。

$ rails routes
  # resourcesが生成してくれたroutes
  edit_admins_単数形 GET    /admins/複数形/new(.:format)

  # form_forが欲しかったroutes
  edit_admins_複数形 POST   /admins/複数形/new(.:format)

解決法

urlを指定して、こっちに飛ぶんだよ〜と教えてあげます :fist_tone4:

***/edit.html.erb
<%= form_for @hoge, url: ***_index_path do |f| %>
  # 省略
<% end %>

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

RSpecのテストを一旦コメントアウトしたいときは、xをつけてpendingにしよう

コメントアウトすると、RSpecの実行結果からコメントアウトしていることに気づけない

pasta_spec.rb
describe Pasta, type: :model do
  describe '#boil' do

    context '14mm' do
      # it { expect(Pasta.new(size: 14).boil(min: 8)).to eq TASTE::NICE }
    end
    context '18mm' do
      # it { expect(Pasta.new(size: 18).boil(min: 8)).to eq TASTE::SOLID }
    end
  end

  # ..略(このあと、47個のテストがある)..
end
RSpec
$docker-compose exec web bundle exec rspec spec/models/pasta_spec.rb
...............................................

Finished in 1.55 seconds (files took 3.7 seconds to load)
47 examples, 0 failures

itにxをつけて、xitとすると、実行結果に明示的にpendingと出る

pasta_spec.rb
describe Pasta, type: :model do
  describe '#boil' do

    context '14mm' do
      xit { expect(Pasta.new(size: 14).boil(min: 8)).to eq TASTE::NICE }
    end
    context '18mm' do
      xit { expect(Pasta.new(size: 18).boil(min: 8)).to eq TASTE::SOLID }
    end
  end

  # ..略(このあと、47個のテストがある)..
end
RSpec
$docker-compose exec web bundle exec rspec spec/models/pasta_spec.rb
..**.............................................

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) Pasta #boil 14mm 8 min should eq nice
     # Temporarily skipped with xit
     # ./spec/models/pasta_spec.rb:19

  2) Pasta #boil 18mm 8 min should eq かたい
     # Temporarily skipped with xit
     # ./spec/models/pasta_spec.rb:24


49 examples, 0 failures, 2 pending
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[小ネタ]特定のcssを読み込みたい

たとえば、app/assets/stylesheets/util/color.css.scssというファイルがあったとして、それを(application.css.scssなど)別のscssから読み込みたいときは、こうする。

app/assets/stylesheets/application.css.scss
// = require util/color

/* 
 * こちらも可
 * = require util/color
 * /
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】 新規アプリケーション作成時の手順と設定

目的

忘れがちなのでまとめる。内容は学習の都度随時更新する。

1.アプリケーションの作成

ターミナルでrails newコマンドを実行することで、アプリケーションのディレクトリとファイルを新規作成できる。

例:rails バージョン5.0.7.2で開発、MySQLでデータベース管理を行うアプリケーションを作成する場合

$  rails _5.0.7.2_ new アプリケーション名 -d mysql   

2.データベースの作成

ターミナルでrake db:createコマンドを実行すると、config/database.ymlファイル内の記述内容に従って、データベースが作成される。

$  cd アプリケーション名               #作成するアプリケーションのディレクトリに移動
$  bundle exec rake db:create    #データベースを作成

rake db:createコマンドの前にbundle execコマンドをつけているのは、複数のアプリケーションにバージョンが異なる同名のgemがある場合、rakeコマンドを実行するとコンフリクトエラーが生じるため。

3.不要なファイルが作成されないように設定

rails gコマンドでコントローラを作成すると、同時にコントローラファイルに対応する各種ファイルが生成される。生成されないようにするにはconfig/apprication.rbを編集する。

config/apprication.rb
module PracticeSpace
  class Application < Rails::Application
    config.generators do |g|
      g.stylesheets false       #cssファイルの生成を行わない
      g.javascript false        #JavaScriptファイルの生成を行わない
      g.helper false            #helperファイルの生成を行わない
      g.test_framework false    #テストファイルの生成を行わない
    end
  end
end

4.Gitのトラッキングの対象外とするファイルやディレクトリの指定

.gitignoreでGitのトラッキングから外すファイルとディレクトリを指定する。

sample/.gitignore
test/controllers/*  #test/controllersディレクトリ内のすべてのファイルとディレクトリを無視する

参考にした記事

Railsドキュメント
[Git] .gitignoreの仕様詳解

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

railsでCSV出力をする方法+実務あるある(関連テーブルのカラム出力など)

社内用の顧客管理ツールを作った際に、バックオフィスのメンバーから「CSV出力機能がほしい!」という熱い要望がありました。

CSV出力機能はよくある機能ですが、実装後も細かな要望に答えて改修したので実務あるあるとしてまとめました。

前提のお話

前提としてtownは複数のuserを持ち、userは1つのtownと紐づきます。
特定のtownごとのuserをまとめてCSV出力するボタンを作りました。

models/town.rb
class Town < ApplicationRecord
  has_many :users
end
models/user.rb
class User < ApplicationRecord
  belongs_to :town
end
towns/index.html.erb
<div class="flex space-between">
   <div>
     <% @towns.each do |town| %> 
       <p><%= town.name %></p>
       <%= link_to "CSV出力", towns_csv_town_user_path(id: town.id) %>
     <% end %>
   </div>
</div>

userテーブル

id town_id last_name first_name address email
1 1 田中 太郎 〇〇町 tanaka@gmail.com
2 2 佐藤 花子 △△町 sato@gmail.com

townテーブル

id name population food tourist spot
1 名古屋 300000 味噌カツ 名古屋港水族館
2 札幌 200000 カニ 時計台
config/routes.rb
  resources :towns do
    get "csv_town_user", on: :member
  end

ボタンを押した時にtowns_controllerのcsv_town_userメソッドにtown.idを渡して、townごとのuserをCSV出力します。

基本のCSV出力

towns_controller.rb
require "csv"

class TownsController < ApplicationController

  def index
    @towns = Town.all
  end

  def csv_town_user
    head :no_content

    users = User.where(town_id: params[:id])
    town = Town.find(params[:id])
    #ファイル名を指定 ここはお好みで
    filename = town.name + Date.current.strftime("%Y%m%d")

    csv1 = CSV.generate do |csv|
      #カラム名を1行目として入れる
      csv << User.column_names

      users.each do |user|
        #各行の値を入れていく
        csv << user.attributes.values_at(*User.column_names)
      end
    end
    create_csv(filename, csv1)
  end

  private
    def create_csv(filename, csv1)
      #ファイル書き込み
      File.open("./#{filename}.csv", "w", encoding: "SJIS") do |file|
        file.write(csv1)
      end
      #send_fileを使ってCSVファイル作成後に自動でダウンロードされるようにする
      stat = File::stat("./#{filename}.csv")
      send_file("./#{filename}.csv", filename: "#{filename}.csv", length: stat.size)
    end
end

実務あるある1: 一部のカラムだけCSV出力したい

towns_controller.rb
require "csv"

class TownsController < ApplicationController
  #略

  def csv_town_user
    head :no_content

    users = User.where(town_id: params[:id])
    town = Town.find(params[:id])
    filename = town.name + Date.current.strftime("%Y%m%d")

    csv1 = CSV.generate do |csv|
      #ここでカラム指定
      columns = ["town_id", "last_name", "first_name", "address", "email" ]
      csv << columns
      users.each do |user|
        csv << user.attributes.values_at(*columns)
      end
    end
    create_csv(filename, csv1)
  end

  private
  #以下略
end

実務あるある2: 1行目はカラム名を日本語にしたい

towns_controller.rb
require "csv"

class TownsController < ApplicationController
  #略

  def csv_town_user
    head :no_content

    users = User.where(town_id: params[:id])
    town = Town.find(params[:id])
    filename = town.name + Date.current.strftime("%Y%m%d")

      #日本語のカラム名を用意
      columns_ja = ["都市ID", "名字", "名前", "住所", "メールアドレス"]
      columns = ["town_id", "last_name", "first_name", "address", "email" ]

    csv1 = CSV.generate do |csv|
      #1行目は日本語のカラム名
      csv << columns_ja
      users.each do |user|
        csv << user.attributes.values_at(*columns)
      end
    end
    create_csv(filename, csv1)
  end

  private
  #以下略
end

実務あるある3: テーブル結合して別テーブルの情報も表示したい

現在は各userの"town_id", "name", "address", "email" が出力されるが、"town_id"ではなく、townテーブルを結合してtownテーブルのnameカラムを出力したいとします。

towns_controller.rb
require "csv"

class TownsController < ApplicationController
  #略

  def csv_town_user
    head :no_content
    #ここでtownテーブルの情報を同時に引いてくる
    users = User.where(town_id: params[:id]).includes(:town)
    filename = town.name + Date.current.strftime("%Y%m%d")

      #都市IDではなく都市名を表示するようにする
      columns_ja = ["都市名", "名字", "名前", "住所", "メールアドレス"]
      columns = ["town_name", "last_name", "first_name", "address", "email" ]

    csv1 = CSV.generate do |csv|
      csv << columns_ja
       users.each do |user|
        user_attributes = user.attributes 
       #user.attributesオブジェクトにtown_nameというキー名でtownテーブルのnameカラムを追加する
       #includesで結合しても呼び出すときはuser.nameではなくuser.town.nameなので注意
        user_attributes["town_name"] = user.town.name
        csv << user_attributes.values_at(*columns)
      end
    end
    create_csv(filename, csv1)
  end

  private
  #以下略
end

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

お勉強を兼ねて、鳥貴族 注文ガチャ を作った(nuxt.js + rails + heroku + python + selenium)

背景

モダンな技術を勉強したかったので、それらを使ってwebアプリを何か作ろうというところから始まりました。作っていく中で勉強するのが一番手っ取り早いという考えもありました。

下記の「サイゼ1000円ガチャ」を見てメニューの注文ガチャいいなあと思ったので、居酒屋でも同じようなものがあったら面白そうと思い、今回の鳥貴族ガチャを作ることにしました。

サイゼリヤ1000円ガチャをつくってみた(Heroku + Flask + LINEbot) - Qiita

そんな感じで、興味のある技術を使って開発をしてみることが一番の目的でした。

成果物:鳥貴族 注文ガチャ

D4CE3051-88F9-48A1-BC98-28432D13BD51.jpeg

概要
- 食べ物と飲み物の数を入力する
- [ガチャを回すボタン]を押下
- 入力した個数分のメニューがそれぞれランダムで出力される

実物
鳥貴族 注文ガチャ - Heroku
https://ak-toriki-nuxt-frontend.herokuapp.com/

フロントエンドプロジェクト(Nuxt.js) - GitHub
https://github.com/lelouch99v/toriki-nuxt-frontend

バックエンドプロジェクト(Ruby on Rails) - GitHub
https://github.com/lelouch99v/toriki-backend

制作のポイント

使った技術

以下の技術を使いました。

  • Python
  • selenium webdriver
  • vue.js
  • nuxt.js
  • Ruby on Rails
  • postgreSQL
  • heroku

DBに入れるメニューをスクレイピングで取得

Pythonでのスクレイピングにハマっていたのでやってみました。

焼鳥、逸品料理、スピードメニュー、ドリンクの4つのカテゴリからそれぞれメニューを取得していきます。結果はcsvで出力します。

このcsvを使ってrailsのmigrateデータとしてメニューデータをDBに入れました。

トリキ スクレイピング - GitHub
https://github.com/lelouch99v/toriki-scraping/tree/master

(今回の開発で一番楽しかったのがここです)

React → Vue.js に変更

前提としてSPA + API の構成としたかったです。

業務でAngularは使っているので、ReactかVueを学びたいと思っていました。
当初Reactを選択したのですが、学習に時間がかかり出来上がるのが先になってしまいそうなのと、Nuxt.jsが気になっていたのであっさりとVueに変えました。
すごく入りやすくて学習していて楽しいです。

猫本も買いました。
302BA844-B09E-457D-BAEB-45FF2BA0A677.jpeg

今後の課題

ボタン押下時のインタラクション
ガチャ回すボタン押すとメニューがランダム表示されますが、とても味気ないです。派手なものにする必要はないですが、最低限以下は実現したいと思っています。
- ボタンを押した感出す
- メニューが表示されるまでにワンクッション置く(ワクワク感が足りないため)

各メニューのイメージ画像を見れるようにする
メニューの名称だけの表示ではなく、イメージ画像も見れるようにするといいと思いました。
そのまま画像を表示するのはスペースの問題などありそうなので、リンククリックでモーダル表示など一工夫は必要かもしれません。

あとはメニュー表示の見た目が質素なので、もう少し改善が必要ですね。

スマホで数字入力をドロップダウンリストで可能にする
スマホではキーボード入力よりも、以下のような選択式のリストで入力のほうがやりやすいと思います。
a.png

最大値は99まであれば十分かなと。。。(もっと少なくてもいいですね)

てかすでにあった

鳥貴族ガチャでぐぐったらすでに作っていた人がいました。しかもクオリティ高い。
完全なリサーチ不足です。

今回の目的は技術の勉強なので、もももんだいないんですけどね!

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