20201123のRailsに関する記事は28件です。

seed_fu: herokuにrailsアプリをデプロイしたら,初期データが反映されてなかった

困っていたこと

railsでwebアプリケーションを完成した。よっしゃherokuにデプロイしようと思いデプロイしたところ
表示してほしいユーザーデータとかの初期データが反映されていなかった

ぼくは初期データをseed_fuといったgemを使って、開発環境では下のコマンドで初期データを投入していた

rails db:seed_fu

結論

以下のコマンドを使えばseed_fuのデータは本番環境に反映される(herokuでは

heroku run rails db:seed_fu

以上です。なにか間違っている点などあれば、コメントお願いいたします

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

[初心者向け]怪奇!Herokuで画像投稿する際の謎のエラー

ローカルでは正常に動いていた。Herokuになると途端に
スクリーンショット 2020-11-23 23.43.06.png
こうなってしまいます。

エラーの正体を先にいうと、、


拡張子です。


ファイルの名前の最後についてるあれです。.pdf.png.mp3です
Herokuでは、"jpeg"が使えません。

※ちなみに"jpg"は使えます。

僕が投稿しようとしてエラーが出ていた画像は拡張子が全てjpegでした。

ログを見ても気付きにくい初心者には手強いエラーなので気をつけてください!:)

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

初期データをseeds.rbに記述して、ゲストログイン機能のエラーを解決してみた

はじめに

 ゲストログイン機能を実装したが、ユーザー登録以前に、2つの情報を登録し、ユーザーと紐づけていたので、それを解決するために、試行錯誤した結果、初期データの作成にたどり着いた。

seeds.rbとは

 Rails6.0では、(もう少し前からだとは思うが、)デフォルトで入っているファイル。dbディレクトリの配下にある。最初から、コメントアウトで色々記述されているが、説明なので、消してしまって構わない。

記述方法

 seeds.rbには、初期データとして作成しておきたい(テスト用などの目的)データを直接作る。

seeds.rb
User.create!(name: 'ゲスト', email: 'gest@sample.com')

モデル名.create!(カラム名: 値)が基本形。
create!の部分は他にも、いくつか使えるメソッドがある。(次回、紹介予定)
上記のように、書けば、いくつでも初期データを作成できる。モデル名の部分を他に変えれば、別のテーブルにも作成可能。

カラム名にidを用いることもできるので、いつもは自動で振り当てられるidについても、任意で作成可能。

ちなみに、eachメソッドやtimesメソッドを使って、繰り返し処理によって、大量の初期データを作成することも可能。

初期データ生成方法

ターミナルで、

rails db:seed

を実行。特にエラーが無ければ、特に反応なく、次の行にいき、待機状態となる。(success!みたいに、表示してくれれば安心なのに…)。テーブルで実際に保存されているか、確認するとよい。

最後に

 これで本番環境でも、生成のコマンドさえ実行すれば、無事に、ゲストログインもできるはず!

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

「N+1問題」とは??

1. 「N+1問題」とは??

アソシエーションを利用した際に、データベースへのアクセス回数が多くなってしまう問題を「N+1問題」といいます

例えば1つのツイート投稿(tweet)は1人のユーザー(user)とアソシエーションで結びついているとします。そして、データベースから全てのツイート投稿を取得するとアソシエーションによりユーザー情報も取得しようとします。
この時、次のコードだとターミナルではこのようなログを示します。

class TweetsController < ApplicationController
  def index
    @tweets = Tweet.all
  end
end

スクリーンショット 2020-11-23 22.07.24.png
水色の箇所がTweetsテーブルとUsersテーブルにアクセスしているログです。
Tweetsテーブルに対しては回で全ての情報を取得していますが、Usersテーブルには回アクセスしています。
(このときTweetsテーブルに存在するTweetのレコードは6つです。)
取得したTweet1つ1つに対して、アソシエーションを使ってユーザーのレコードを取得する処理を繰り返していることを示しています。つまり100個のツイート投稿を取得すると、100回Usersテーブルにアクセスすることになり、データ取得に時間がかかります。その結果、アプリケーションのパフォーマンスが著しく下がることになります。これが「N+1問題」です。これを解決するためには、includesメソッドを利用します。

2. includesメソッド

モデル名.includes(:アソシエーションで紐付くモデル名)

includesメソッドは、引数に指定された関連モデルを1度のアクセスでまとめて取得するメソッドです。このメソッドを用いて以下のようにコードを書き換えます。

class TweetsController < ApplicationController
  def index
    @tweets = Tweet.includes(:user)
  end
end

スクリーンショット 2020-11-23 22.07.13.png
結果、水色の箇所が2つに変わりました。Tweetsテーブルに対して回、Usersテーブルに対しても回のアクセスで全ての情報を取得できました。

3. さいごに

 私自身、今まで勉強する中で膨大なデータの使用を想定した開発をしていませんでした。そのため「N+1問題」を勉強して、アプリケーションのパフォーマンスを意識した開発する重要性を感じました。この記事を読んで、内容が良かったらLGTMお願いします!また、ご意見があれば是非コメントもお願いします。

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

[Rails]collection_selectについて勉強してみた![初心者]

はじめに

現在作成しているアプリの中で、collection_selectを使用する場面があり、備忘録のためにアウトプットします。
正直、このメソッドにたどり着くまでに、メチャクチャ時間がかかりました笑

collection_selectとは、モデルの情報を元に、セレクトボックスを生成できるメソッドです。
具体的に見ていきましょう!!

やりたかったこと

セレクトボックス内に、ユーザーの登録済住所を選択肢として用意したかったのですが、どのような記載方法が適切なのか、全然分からなくて、けっこう長い時間グーグル検索して辿り着いたのが、collection_selectです。
image.png
このセレクトボックスをクリックすると、
image.png
というように、登録している住所一覧が出るようにしたかったです!!

使い方

collection_select(オブジェクト名, メソッド名, 要素の配列, value属性の項目, テキストの項目 [, オプション or HTML属性 or イベント属性])
f.collection_select(メソッド名, オブジェクトの配列, value属性の項目, テキストの項目 [, オプション or HTML属性 or イベント属性])

使用例

<%= f.collection_select(:address_id, @addresses, :id, :order_address, prompt: "選択してください") %>

上のcollection_selectがHTMLでどうなっているのか確認すると、こうなっていました。

<select name="order[address_id]" id="order_address_id"><option value="">選択してください</option>
  <option value="1">1111111京都市田中</option>
  <option value="2">2222222アメリカ佐藤</option>
  <option value="3">3333333インド安井</option>
</select>

それぞれの引数について確認しましょう!!

第一引数 → :address_id

第一引数(プロパティ名)は、selectタグのid属性とname属性に関係しています。
今回で言うと、
name="order[〜〜〜]" id="order_〜〜〜"
という風に反映されています。

第二引数 → :@addresses

第二引数(オブジェクトの配列)は、コントローラで
@addresses = Address.where(customer_id: current_customer.id)
と定義しています。
つまり、現在ログインしているユーザー(cuurent_customer)の登録済住所を全て取得しているということです。

第三引数 → :id

第三引数(value属性の項目)は、<option value="〜〜〜">において。設定したい値のカラム名が入ります。
今回はidカラムの値を設定するので、上記記述となります。

第四引数 → :order_address

第四引数(テキストの項目)は、optionタグ内のテキスト<option>〜〜〜</option>に設定したい値のカラム名が入ります。

第五引数 → prompt: "選択してください")

最後はオプションですが、これを付け足すことにより、選択してくださいの文言が一番上に表示されるようになります。

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

README に記述するアプリ開発の背景

はじめに

個人開発したオリジナルアプリのREADMEを記述しています。このアプリを開発した背景や問題、課題を記載しました。問題と背景の切り分け、考え方の本質を書き記します。

目次

1.問題と課題の違い
2.オリジナルアプリの開発背景から設定した問題と課題

1.問題と課題の違い

辞書などにおける意味は下記の通り。言葉そのもの意味に差異はない。

問題:批判・論争・研究などの対象となる事柄。解決すべき事柄。課題。
課題:解決しなければならない問題。果たすべき仕事。

参考:
課題
問題

しかし、ビジネスにおいては明確な違いがある。主に下記のような意味で使われる。

問題:好ましくない状態
課題:問題を解決すべく行うこと

問題と課題の因果関係は上位に問題、下位に課題となる。基本的には1つの問題に対し、複数の課題がぶら下がる。逆算的に見れば複数ある下位の課題を全て解決すると、上位の問題も解決されることになる。

オリジナルアプリの開発背景から設定した問題と課題

背景
コロナ渦をきっかけとしてリモートワークが増えており、それに伴い自宅にいる時間も増えてる。普段の職場における労働と比べ、具体的には下記2点の傾向が増加していると考えた。

・一人で部屋にこもりがち。
・仕事とプライベートの切り替えが曖昧。これまで仕事をしない時間帯や場所で仕事をする。

問題
下記2点の理由から「メンタルヘルスが悪化する可能性がある」を問題として設定した(状態)。

・人との接点が減り孤独を感じやすくなる
・デスク周りの汚れや整理整頓に対し、他者から指摘されない。部屋が汚く無意識にストレスを感じる。

課題
メンタルヘルスが悪化するという問題に対し、下記2点を課題とした(やること)。
・対面に変わる、孤独を感じない仕組みの確立
・清潔が保たれている空間の確保
image.png

そして各課題をさらに細かく分類し、具体的な機能を7つ抽出した。逆算的にみれば抽出した7つの機能を満足することにより2つの課題は解決され、最終的には問題も解決できる。
image.png

以上

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

個人アプリ開発日記 #4

では前回書いた様々なメソッドが機能してくれるのか簡単なビューを使って確かめてみましょう、、!

まずは新規登録から

このような簡単なビューで新規登録

users/new.html

<% if current_user %>
  <h1>welcome to my app</h1>
<% else %>
  <%= link_to "ログイン", login_path%>
<% end %>


<h2><%= link_to "ユーザー一覧",users_path %></h2>
<h2><%= link_to "投稿一覧", drinks_path %></h2>
<% unless  logged_in? %>
    <div class="col-md-6 col-md-offset-3">
    <%= render 'shared/form' %>
  </div>
<% end %>



  <% if current_user %>
    <p>ログインしてます</p>
    <%= link_to @current_user.nickname, user_path(@current_user) %>
    <%= link_to "ログアウト",logout_path,method: :delete %>
    <%= link_to "投稿する", new_drink_path %>
    <%= link_to "退会する", user_path(current_user),method: :delete %>
  <% else %>
    <strong>ログインしてません</strong>
  <% end %>

前回定義したcurrent_userメソッドや、logged_in?メソッドを使ってみました
スクリーンショット 2020-11-23 18.36.21.png

これが今回のルート画面です

ログインしてみます

1606132818649@2x.jpg

しっかり前回書いたコードが機能してるみたいですね、、、!!

users/show.html.erb

<h1><%= current_user.nickname%></h1>
<%= link_to "toppage", root_path %>
<%= link_to "setting", edit_user_path(current_user)%>

  <p><%= current_user.nickname %>さんの投稿一覧</p>
  <% @drinks.each do |drink| %>
      <%= drink.price%>
      <span class="name"><%= drink.name %></span>
      <p><%= drink.explain %></p>
  <% end %>

drinkリソースの実装は次回で書きます

前回書いたusers#showあたりがしっかり機能していて、sessions_helperのcurrent_userも使えます
便利メソッドはビューでも呼び出せるので、ビューでも呼び出したいメソッドはhelperメソッドに定義した方がいいのかもしれません

次はuserの更新を見てみましょう

先ほどのユーザー詳細ページにsettingというところをクリックすると

1606133218088@2x.jpg

という画面に遷移します

コードはこんな感じ

users/edit.html.erb

<h1>Update <%= @user.nickname %>'s profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'shared/form' %>
  </div>
</div>
<%= render 'shared/form' %>

部分テンプレートを使用してるのでそちらもみてみましょう

shared/_form.html.erb

<%= form_with(model: @user, local: true) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>

  <%= f.label :nickname %>
  <%= f.text_field :nickname, class: 'form-control' %>


  <%= f.label :email %>
  <%= f.email_field :email, class: 'form-control' %>

  <%unless @current_user%>
    <%= f.label :password %>
    <%= f.password_field :password, class: 'form-control' %>

    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation, class: 'form-control' %>
  <% end %>
  <%= f.submit "submit"%>
<% end %>

ここは新規登録ページのユーザーに関する入力フォームと一緒です

ですが

<%unless @current_user%>
<%end%>

とパスワードに関するフォームを囲むことにより、現在ログインしてる本人ならパスワードを改めて入力せずに
プロフィールの更新ができます。

パスワードを入力せずに値を更新するためには

user.rb

 validates :password, presence: true, length: { minimum: 6 },allow_nil: true

allow_nil: true にしなければなりません

パスワードがなくても新規登録できちゃうじゃん!

と思うかもしれませんが、has_secure_passwordをuser.rbに書いてるので、
新規登録の時はhas_secure_passwordくんがパスワードあるかどうか確かめてくれるらしいです、
賢いですね。

とりあえず「そうくん」に名前を変更しときましょう

ログインしたことによるトップページの変化

トップ.jpg

では早速ログアウトしてみましょう

トップ2.jpg

ログインしてないバージョンのtoppageに戻ります

そしてもう一回ログイン

ログイン画面.jpg

ログイン.jpg

ユーザーの詳細ページにリダイレクトされました
ユーザーが投稿したコーヒーの感想がチラッと見えてますが、これは次回投稿します

ちなみにremember me on this computerにチェックしたので、ブラウザを閉じたりしてもログイン情報は保持される仕組みです

どのような仕組みかは前回解説しております

何はともあれ前回書いたコードがしっかり機能することが確かめられました、、、!
本当はrails consoleとかでもうちょい上手く確かめられると思うのですが、、、、、

次回はコーヒーの感想を投稿したり、削除したり、一覧表示したり、誰の投稿かを定義したり、drinkリソースに関することをやっていきたいと思います

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

Rails wheneverをdockerで実行する

動機

Railsの定期実行を行うwheneverを使おうとしたのですが、Mac上で直接実装すると環境変数や権限管理で大変だったのでdockerで行うことにしました

前提条件

実行環境は下記のようになります
・ ruby 2.6.3
・ Rails 5.2.4
・ MySQL 8.0.19 

Docker関係

Dockfiledocker-compose.ymlは以下のようになります
通常のdockerの設定とあまり変わらないのですが、今回はwheneverを利用するのでcornのインストールとcronをフォアグラウンド実行するための設定を追記しています。

FROM ruby:2.6.3
#rubyのバージョン指定

#gemのインストール
RUN apt-get update -qq && \
    apt-get install -y build-essential \ 
                       libpq-dev \        
                       nodejs

# cronインストール
RUN apt-get install -y cron 

RUN mkdir /my_app

WORKDIR /my_app

COPY Gemfile /my_app/Gemfile
COPY Gemfile.lock /my_app/Gemfile.lock

RUN gem install bundler
RUN bundle install

COPY . /my_app

# wheneverでcrontab書き込み
RUN bundle exec whenever --update-crontab 

# cronをフォアグラウンド実行
CMD ["cron", "-f"] 

docker-compose.yml
version: '2'
services:
  db:
    image: mysql:8.0.19
    command: 
      --default-authentication-plugin=mysql_native_password
    volumes:
      - ./mysql-confd:/etc/mysql/conf.d
      - mysql-data:/var/lib/mysql    #データの永続化のため
    ports:
      - "3306:3306"
    restart: always
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      # MYSQL_DATABASE: app_development
      MYSQL_USER: root
      # MYSQL_PASSWORD: password
      TZ: Asia/Tokyo
  app:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/my_app   
      - bundle:/usr/local/bundle   
    ports:
      - "3000:3000"
    links:
      - db

volumes:
  mysql-data:
  bundle:      #bundle installした後buildし直さなくてよくなる

whenever関係

config/schedule.rb
# wheneverにrailsを起動する必要があるためRails.rootを使用
require File.expand_path(File.dirname(__FILE__) + "/environment")

# 環境変数をうまい感じにやってくれる
ENV.each { |k, v| env(k, v) }

# ログを書き出すようファイル
set :output, error: 'log/crontab_error.log', standard: 'log/crontab.log'
set :environment, :development

#2分毎に`sample_task`の`scheduled_task`を実行する
every 2.minutes do
  rake 'sample_task:scheduled_task'
  # runner "Test.yakisoba", :environment => :development # runnnerの例
end

実行したいコマンド

/lib/tasks/sample_task.rb
namespace :sample_task
  desc "scheduled_task"
  task scheduled_task: :environment do
     ..... 実行したい関数
    end
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RSpec】Ruby on Rails チュートリアル「第3章」のテストを RSpec で書いてみた

はじめに

Ruby on Rails チュートリアルでは「minitest」を使用してテストが実施されていますが、実際の現場では主に「RSpec」を使用してテストを実施するとのことなので、チュートリアルのテストを実際の現場に近づけるために「RSpec」で実施するようにしました。まず手初めに「第3章」のテストを RSpec で書き換えてみたので、同じようにチュートリアルのテストを RSpec で実施してみたい人の参考になれば幸いです。

対象者

  • Ruby on Rails チュートリアルのテストを Rspec で実施予定、または実施してみたい人
  • Ruby on Rails チュートリアル「第3章」のテストを Rspec で書きたいけど、書き方が分からない人

テストコード

実際にテストコードを書き換えた結果が下記になります。

Minitest

static_pages_controller_test.rb
require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  def setup
    @base_title = "Ruby on Rails Tutorial Sample App"
  end

  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", "Home | #{@base_title}"
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
    assert_select "title", "Help | #{@base_title}"
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
    assert_select "title", "About | #{@base_title}"
  end
end

RSpec

static_pages_spec.rb
require "rails_helper"

RSpec.describe "StaticPages", type: :request do
  before do
    @base_title = "Ruby on Rails Tutorial Sample App"
  end

  describe "Home page" do
    it "should get home" do
      visit static_pages_home_url # /static_pages/home/ へアクセス
      expect(page.status_code).to eq(200) # HTTP ステータスコードが "200" か判定
      expect(page).to have_title "Home | #{@base_title}" # タイトルに 「Home | Ruby on Rails Tutorial Sample App」 が含まれているか判定
    end
  end

  describe "Help page" do
    it "should get help" do
      visit static_pages_help_url
      expect(page.status_code).to eq(200)
      expect(page).to have_title "Help | #{@base_title}"
    end
  end

  describe "About page" do
    it "should get about" do
      visit static_pages_about_url
      expect(page.status_code).to eq(200)
      expect(page).to have_title "About | #{@base_title}"
    end
  end
end

次回

「第4章」のテストコードを RSpec に書き換える予定です。

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

Rails 幹事向けアプリの作り方 複数レコードの同時登録編

最初に

この記事は、幹事向けのアプリを作成方法を記載します。
どんな構造を取るかアプリと言うと、2段階の複数登録を行うものです。

①複数のテーブルと複数レコードを同時登録(イベントと複数の日付の登録)

②①で登録した複数レコード(日付など)の中間テーブルレコードの一括登録(出欠状態など)

開発時に、②複数のレコードを登録する方法について
参考となる情報がなく苦労しました。
そこで、作り方を公開して、同じ悩みを持った方の
助けに慣れればと思い記載致します。

※UI部分については今回紹介していません。Gitにて確認お願いします。

目次

  1. 概要
  2. テーブルデータについて
  3. イベント(親)と複数の日程(子)、お店(子)の登録機能
  4. 参加者(親)と参加状況(子)の登録機能
  5. 参加者の参加状況の編集機能
  6. 参加状況の削除機能

1.アプリの概要

イベントを開催して、参加者の状況を管理。最終的にはイベントの日時・場所を決定するアプリです
ユーザーは2種類あり、「イベントの主催者」と「参加者」を想定しています。
Git:https://github.com/tsuyatsuya-april/ikang
HP:http://kyomo-ikang.com/events/1

機能

主催者
・イベントの概要・候補日・候補店を登録・編集・削除する機能
Image from Gyazo

参加者
・参加者の名前と参加状況の登録・編集・削除する機能
Image from Gyazo

2.テーブルデータについて

Userテーブル...主催者の名前・email・passwordを登録
Eventテーブル...イベントの名称と概要を登録
Scheduleテーブル...イベントの候補日を登録
Shopテーブル...イベントの開催場所候補を登録
Joinテーブル...参加者の名前を登録
DateAnswerテーブル...JoinとScheduleを親として、参加日程の状況を登録
ShopAnswerテーブル...JoinとShopを親として、開催場所の評価を登録

作成するアプリでは、主に2つのフォームで下記の関係での登録を行う
Event(親)、Schedule(子)、Shop(子)のイベント登録フォーム
Join(親)、DateAnswer(子)、ShopAnswer(子)の参加者登録フォーム

Image from Gyazo

3.イベント(親)と複数の日程(子)、お店(子)の登録機能

イベントの登録機能を下記の小項目に沿って説明を行う。
尚、ユーザーの登録は、メジャーなので割愛させていただきます。
また、コードの一部抜粋した形で説明させていただきます。

小項目

  1. 注目コード記載(Model,Controller,HTML,JS)
  2. fields_forメソッド
  3. name属性の修正

1.注目コード記載

model/event.rb
class Event < ApplicationRecord
  belongs_to :user
  has_many :shops, inverse_of: :event, dependent: :destroy
  accepts_nested_attributes_for :shops, allow_destroy: true
end

model/shop.rb
class Shop < ApplicationRecord
  belongs_to :event, inverse_of: :shops
  validates_presence_of :event
end
events_controller.rb
  def new
    @event = Event.new
    1.times { @event.shops.build } 
  end 

  def create
    @event = Event.new(event_params)
    @event.user_id = current_user.id
    if @event.save
      redirect_to event_path(@event.id)
    else
      render "new"
    end
  end

  private
  def event_params
    params.require(:event).permit(:name, :description, schedules_attributes: [:savedate, :savetime], shops_attributes: [:shop_name, :shop_url, :map_url, :comment])
  end

events/new.html
    <%= form_for @event,id:"event-new-form", local: true do |f| %>
      <%= render "share/error_messages", model: f.object %>
      <% 中略%>
          <div id="shop-add-btn">
            <%= link_to "お店追加", "#", class:"btn-flat-border" %>
          </div>
        </div>
        <%= f.fields_for :shops do |shop_fields| %>
          <div id="new-shop-top">
            <div id="new-shop">
              <div id="shop-name-box">
                <div id="shop-name"><p>店名(必須)</p></div>
                <%= shop_fields.text_field :shop_name, class:"shop-name-input"%>
              </div>
              <% 中略 %>
              <div id="shop-delete-box">
                <%= link_to "お店削除", "#", class:"btn-flat-border-red", id:"shop-delete" %>
              </div>
            </div>
          </div>
        <% end %>
      </div>
      <div class="submit-box">
        <%= f.submit "イベントの登録",class:"btn-flat-border-submit", id:"new-submit" %>
      </div>
    <% end %>
main.js
//お店の追加
   function newShopAdd(){
        nameNumberShopAdjust();
        const shopParent = document.getElementById("new-shop-top");
        const addShopBtn = document.getElementById("shop-add-btn");
        let currentShopLength = document.querySelectorAll("#new-shop").length;
        let nextNum = currentShopLength;
        let shopHtml = `
          <div id="new-shop">
            <div id="shop-name-box">
              <div id="shop-name"><p>店名(必須)</p></div>
              <input class="shop-name-input" type="text" name="event[shops_attributes][${nextNum}][shop_name]" id="event_shops_attributes_${nextNum}_shop_name">
            </div>
            ~<中略>~`;
        addShopBtn.onclick = function(){
          shopParent.insertAdjacentHTML("beforeend", shopHtml);
          shopDelete();
          newShopAdd();
        };

      }
   //お店の削除
      function shopDelete(){
        let shopParent = document.querySelectorAll("#new-shop");
        let shopDeleteBtn = document.querySelectorAll("#shop-delete");
        let shopParentLength = shopParent.length;
        for (let i=0; i < shopParentLength; i++){
          shopDeleteBtn[i].onclick = function(){
            let conformShopLength = document.querySelectorAll("#new-shop").length;
            if(conformShopLength != 1){  
              return shopParent[i].remove();
            }
          };
        };
      }
   //お店のname属性の値に入る数値の調整
   function nameNumberShopAdjust(){
        let saveShop = document.querySelectorAll(".shop-name-input");
        let saveShopLength = saveShop.length;
        for(let j=0; j<saveShopLength; j++){
          saveShop[j].removeAttribute("name");
          saveShop[j].setAttribute("name",`event[shops_attributes][${j}][shop_name]`);
        }
      }
    } 

2.fields_forメソッド

このメソッドは、公式ドキュメントにて下記のような説明をしている。
モデルを固定してフォームを生成
form_for内で異なるモデルを編集できるようになる。

つまり、今回でいうとEvent(親)とは別のShop(子)モデルの編集ができるようになるというものである。
ただし、fields_forを使う為にはいくつか準備が必要である。

アソシエーションの設定

model/parent.rb
親と子のモデルの双方向の関連付けができるようにする
has_many :子モデル(複数形), invers_of: :(親モデル), dependent: :destroy

子モデルが同時に登録できるようにする
accepts_nested_attributes_for :子モデル(複数形), allow_destroy: true(空白のフォームがあれば登録できないようにする)
model/child.rb
  親と双方向の関連付けしてバリデーション設定などができるようにする。(例 shops.eventなど)
  belongs_to :親モデル, inverse_of: :子モデル

コントローラの設定

controller/test.rb
def new
 @event = Event.new
 関連した子の要素を生成する時はbuild(newのエイリアス)を使用する
 1times { @event.shops.build }
 ちなみにtimesの数値の分だけ子の要素の登録フォームができる
end

通常に保存する場合と同じ
def create
  @event = 親モデル.new(event_params)
    if @event.save
      redirect_to 指定のパス
    else
      render アクション名
    end
end

private
def event_params
    params.require(:親モデル).permit(:親カラム1, :親カラム2, 
                  子モデル名(複数系)_attributes: [:子カラム1])
    .merge(user.id: :current_user.id)
end

ビューの設定

views/test.html
<%= form_for @event,id:url, local: true do |f| %>
  <%= f.text_field :親カラム名 %>
 ここで子モデルの登録ができるように設定を行う
  <%= f.fields_for :子モデル do |sf| %>
     <%= sf.text_field :子モデルカラム名 %> 
  <% end %>
  <%= f.submit "イベントの登録" %>
<% end %>

上記のビュー・モデル・コントローラーの設定を行えば、
フォームの送信ボタンを押した後にデータが下記のようなパラメータで送られます。

paramater.rb
通常の場合
親モデル名 = { :first => 1, :second => 2}

fields_forを使用した場合
親モデル名 = {:first => 1, :second => 2, 
子モデル名 => {:first => 1, :second => 2 }}

見にくいですが具体的な例が下記画像です。
Image from Gyazo

親モデルの中に子モデルがネストされてデータが受け渡され保存されるということになります。
これで複数モデルの複数レコードの保存の下地が整いました。

3. name属性の修正

2では子モデルの複数のレコードが保存できる設定の説明を行ってきた。
ただし、javascriptを用いてお店と日程フォームの増減が行えるように設定をしています。
この時に保存が上手く行かなくなることがあったので、その原因と解消方法について記載します。

問題
javascriptでフォームの追加を実行した後に全てのフォームが保存されない時があった。

前提条件
fields_forを使った時のフォームのname属性がhtml上で下記のように変換されて表示される

test.html
fields_for内のinputタグの中にあるname属性の名前
event[schedules_attributes][0][savedate]
event[schedules_attributes][1][savedate]
event[schedules_attributes][3][savedate]

公式化
親モデル名[子モデル名(複数系)_attributes][要素番号][子モデルの対象カラム名]

仮説検証
Image from Gyazo

解消方法
javascriptを使ってデータの送信前にname属性の要素番号が被らないように調整する。

main.js
   //(送信ボタンを押した時に要素番号を調整するメソッドを実行)
   function nameNumberShopAdjust(){
     //お店のフォームのセレクタを取得
        let saveShop = document.querySelectorAll(".shop-name-input");
     //要素の数がいくらあるかを取得
        let saveShopLength = saveShop.length;
     //要素数分だけループを実行
        for(let j=0; j<saveShopLength; j++){
      //フォームの既存で設定されたname属性を削除する
          saveShop[j].removeAttribute("name");
      //フォームのname属性について、現在のループ回数を要素番号として付け直す。
          saveShop[j].setAttribute("name",`event[shops_attributes][${j}][shop_name]`);
        }
      }
    } 

ここの部分に関しては、ドキュメントに記載されておらず挙動を読んで
仮説検証をたてたものですが上記方法で解決致しました。

4.参加者(親)と参加状況(子)の登録機能

大項目3で指定した日程とお店について、参加状況を登録する方法について記載する。
join(親)、shop(親)、shop_answer(子)の中間テーブル、
join(親)、schedule(親)、date_answer(子)の中間テーブル
上記2つの登録を主に行う。
尚、解説はお店の方のみさせていただく

小項目

  1. 注目コード記載(Model,Controller,HTM, routes)
  2. viewのname属性の調整
  3. 中間テーブルの親_idの格納
  4. event/showページで子のjoinコントローラにパラメータを渡す方法
  5. joinコントローラーのupdateアクション

1. 注目コード記載(Model,Controller,HTM, routes)

model/join.rb
  has_many :shop_answers, dependent: :destroy
  accepts_nested_attributes_for :shop_answers, allow_destroy: true
model/shop.rb
  has_many :shop_answesr, dependent: :destroy
model/shop_answer.rb
  belongs_to :join
  validates_presence_of :join
  belongs_to :shop
events_controller.rb
  def show
    if params[:join_id]
      set_join
    else
      @join = Join.new
      1.times { @join.shop_answers.build }
    end
  end
joins_controller.rb
  def create
    @join = Join.new(join_params)
    if @join.save
      redirect_to event_path(params[:event_id])
    else
      render "events/show"
    end
  end
  private
  def join_params
    params.require(:join).permit(:nickname, :email, 
      shop_answers_attributes: [:shop_id, :status, :vote])
      .merge(event_id: params[:event_id])
  end
events/show.html
<%= form_with model: @join, url:event_joins_path(@event.id), id:"join-form",  local: true do |f| %>
   <div id="join-box">
     <div id="join-name-label">
       <label>ユーザー名</label>
     </div>
     <div class="join-users">
      <%= f.text_field :nickname, id:"join-user",placeholder:"ニックネームを入力ください"%>
     </div>

     <div id="shop-answer">
       <h1 id="shop-answer-title">店舗選択</h1>
         <div class="circle-text">説明: お店を◯×△で評価,一番良いと思う店に一番ボタンで投票下さい</div>
           <table id="shop-answer-table">
             <tbody> 
               <% @event.shops.each do |es| %>
                  <%= f.fields_for :shop_answers do |shop_fields| %>
                    <tr id="join-shops" class="bottom-line">
                      <th class="shop-label">
                        <div>
                          <%= link_to es.shop_name, es.shop_url, target: :_blank %>
                        </div>
                      </th>
                      <td class="shop-vote-balance">
                        <%= shop_fields.hidden_field :shop_id,class:"shop-id", value: es.id %>
                        <%= shop_fields.hidden_field :status, id:"shops-status" %>
                        <%= shop_fields.hidden_field :vote, id:"shops-vote" %>
                        <div class="change-status">
                          <h1 class="btn btn--orange btn--circle btn--circle-a btn--shadow " id="shop-yes"></h1>
                          <h1 class="btn btn--orange btn--circle btn--circle-a btn--shadow choice" id="shop-delta"></h1>
                          <h1 class="btn btn--orange btn--circle btn--circle-a btn--shadow cross-vote" id="shop-no">×</h1>
                         </div>
                         <div id="shop-change-vote">
                           <h1 class="btn btn--orange btn--circle btn--circle-a btn--shadow" id="shop-vote">一番</h1>
                         </div>
                       </td>
                    </tr>
                 <% end %>
              <% end %>
            </tbody>
         </table>
      </div>
       <div id="join-submit-box">
        <%= f.submit "回答",class:"btn-flat-border", id:"join-submit-inputbox", :onclick => "return check_name()" %>
       </div>
  <% end %>
routes.rb
Rails.application.routes.draw do
  root to: "events#index"
  resources :events do
    resources :schedules
    resources :shops
    resources :joins
    resources :comments
    resources :date_decisions
    resources :shop_decisions
  end
end

2. viewのname属性の調整

大項目3で登録した複数データに対して、複数の中間テーブルの保存を行うには下記のコードの構造をとる。

events/show.html
<%= form_with model: @join, url:event_joins_path(@event.id), id:"join-form",  local: true do |f| %>
  親モデルのnicknameカラムの保存フォーム
  <%= f.text_field :nickname, id:"join-user",placeholder:"ニックネームを入力ください"%>

  イベントページに紐づいているお店のレコードを全て出力する。
  <% @event.shops.each do |es| %>

  fields_forで複数レコード登録できるようにする。
    <%= f.fields_for :shop_answers do |shop_fields| %>

    中間テーブルの親IDを登録するフォーム。事前にid番号をvalueに格納する
      <%= shop_fields.hidden_field :shop_id, value: es.id %>

    javascriptを使って、数字1(=◯),2(=△),3(=×)が格納されるようにしている
      <%= shop_fields.hidden_field :status, id:"shops-status" %>

    javascriptを使って投票数0,1を格納する。
      <%= shop_fields.hidden_field :vote, id:"shops-vote" %>

    onclickでユーザーのニックネームが格納されていない場合にフォームを送信できないようにする
      <%= f.submit "回答",:onclick => "return check_name()" %>
    <% end %>
  <% end %>
<% end %>               

Image from Gyazo
解決方法
大項目3と同様な形でjavascriptを用いて、
出力されたお店の分、name属性の要素番号を付け直す
これで出力されたお店のレコード分だけ、中間テーブルのレコードが保存できるようになった。

3. 中間テーブルの親_idの格納

中間テーブルであるshop_answerテーブルに親IDを紐付ける方法を記載する。
・1点目のjoin_idはfields_forメソッドを使用しているので親子関係となり自動的にIDが割り振られる。
・2点目のshop_idは下記の通りeachメソッドを用いて、各ID番号をshop_idのフォームに格納している。

events/show.html
<% @event.shops.each do |es| %>
  <%= f.fields_for :shop_answers do |shop_fields| %>

    先ほども記述したがes.idでshop(親)のID番号をshop_answer_idに渡している。
    <%= shop_fields.hidden_field :shop_id, value: es.id %>
  <% end %>
<% end %>               

以上で中間テーブルの保存ができる状態になった

4. events/showページで子のjoinコントローラにパラメータを渡す方法

初学者にとってevents/showページで登録を行う場合、
events_controllerにパラメータをpostするという印象が強い。
しかし今回はjoins_controllerにパラメータをpostして登録を行うので
その場合のurl指定方法を記載する。

まず下記コマンドを実行しURLの確認を行う

test.rb
 rails routes

次に確認したURLの内joinの登録であるURLをform_withメソッドの中に適応させる

events/show.html
 <%= form_with model: @join, url:event_joins_path(@event.id), id:"join-form",  local: true do |f| %>

以上を行うことで、events/showページからjoins_controllerにデータを受け渡すことができるようになる。

5.参加者の参加状況の編集機能

events/show.htmlにて参加者毎の編集フォームを表示する方法について記載を行う。
複数の情報を出力する上で

小項目

  1. 同一ページ内で参加者IDの情報をコントローラに受け渡す方法
  2. fields_forを用いた場合の編集フォームのviewを表示について。
  3. objectメソッド、親データの店名やURLを引き出す方法

1. 同一ページ内で参加者IDの情報をコントローラに受け渡す方法

まず、events/show.html上に参加者の編集フォームを出力するには、
コントローラに各参加者のjoin_idの数値を送り、データを抽出する必要がある。

その為に、link_toメソッドを用いて、join_idをパラメータとしてコントローラに引き渡す手法を用いた。

events/show.html
<% if @event.joins %>
 登録されている参加者全てを出力する
  <% @event.joins.each do |event_join| %>
  ループ中の参加者のID、join.idをjoin_idというキーに格納してコントローラに渡している
    <%= link_to event_join.nickname, event_path(@event.id,join_id: event_join.id) %>
  <% end %>
<% end %>

コントローラではjoin_idというパラメータの有無によって
編集フォームの出力か登録ページの出力かの分岐を行っている。

events_controller.rb
def show
  @joins = Join.all
  if params[:join_id]
    set_join
  else
    @join = Join.new
    1.times { @join.shop_answers.build }
  end
end

private
def set_join   
    @join = Join.find(params[:join_id])
end

編集フォームの場合は、決まった値をフォームに格納した状態で表示される。
登録フォームの場合は、新規にデータを作成できる状態で表示される
これでビューに表示できる条件が整った。

2. fields_forを用いた場合の編集フォームのviewを表示について

登録時には、shop.each doとfields_forを用いたが編集時には、fields_forのみを用いれば、選択したJoinレコードに紐づくshop_answerレコードが全て出力される。

events/show.html
<% unless params[:join_id] %>
新規登録の時の処理
<% else %>
編集時の処理
  <%= form_with model: @join, url:event_join_path(@event.id, @join.id), id:"join-edit-form" , local: true do |f| %>
   ユーザー名の編集
    <%= f.text_field :nickname,id:"join-edit-user", placeholder:"ニックネームを入力ください"%>
   fields_forを用いて、選択したJoinレコードに紐づく全てのshop_answerレコードを出力
    <%= f.fields_for :shop_answers, @join.shop_answers do |shop_edit_fields| %>
   編集ページでは新たに、joinのidカラムを格納するフォームを用意する。それ以外は登録と同じ。
      <%= shop_edit_fields.hidden_field :id,class:"shop-answers-id" %>
      <%= shop_edit_fields.hidden_field :shop_id,class:"shop-edit-id" %>   
      <%= shop_edit_fields.hidden_field :status, id:"shops-edit-status" %>
      <%= shop_edit_fields.hidden_field :vote, id:"shops-edit-vote" %>  
    <% end % >
    <%= f.submit "更新",class:"btn-flat-border", id:"join-edit-submit-inputbox", :onclick => "return check_name()" %>
  <% end %>
<% end %>

同じ躓きをする方もいると思うので私が失敗した時の場合も下記の画像にて記載します。
Image from Gyazo

3. objectメソッド、親テーブルの店名やURLを引き出す方法

小項目2では、フォームが正しく表示されたが実は1つ問題がある。
それは、fields_forでは、
「中間テーブルの子モデルに対し、親モデルのデータが引き出せない」ことである。
私のアプリでは店名やURLが表示されず、どのデータに紐づいているか判断出来なくなる。

これに対して、shops.each do を用いて、店名を引き出せば良いと考えたこともあった。
しかし、結果は小項目2の最後の画像の通り、フォームが正しく表記されない。

そこで、objectメソッドを用いることにした。
このメソッドを変数の後につけることで、変数に格納しているデータを取得できる。

test.HTML
  shop_answer = { id => 1, join_id => 1, shop_id => 1, status => 1}
  shop = { id => 1, shop_name => "吉野家", shop_url => "yoshinoya" }

  <%= f.fields_for :shop_answers, @join.shop_answers do |shop_edit_fields| %>
   snum = shop_id = 1
    <% snum = shop_edit_fields.object.shop_id%>
      sn = Shop.find(1)
     <% sn = Shop.find(snum) %>
     これでfields_for内でshop_answerの親モデルのお店情報が出力できるようになった
     sn.shop_name = 吉野家
   sn.shop_url = yoshinoya

objectメソッド導入前と導入後のイメージ
Image from Gyazo

5. joinコントローラーのupdateアクション

fields_forを用いた時は、paramsの表記方法が異なる。
登録時には子モデルのID番号が必要なかったが
更新時には小モデルのID番号が必要となる

また、:_destroyをつけることで親モデルに紐づく子モデルのデータを削除することができる。
例えば、編集時に子モデルのフォームを空白に変更して更新するとデータが削除されるなど。

joins_controller.rb
  def update
    if @join.update(join_update_params)
      redirect_to event_path(params[:event_id])
    else
      render "events/show"
    end
  end

  private
  通常時
  def join_params
    params.require(:join).permit(:nickname, :email, 
      date_answers_attributes: [:schedule_id, :status], 
      shop_answers_attributes: [:shop_id, :status, :vote])
      .merge(event_id: params[:event_id])
  end
 
 更新時
  def join_update_params
    params.require(:join).permit(:nickname, :email, 
      date_answers_attributes: [:id,:schedule_id, :status, :_destroy], 
      shop_answers_attributes: [:id,:shop_id, :status, :vote, :_destroy])
  end

6. 参加状況の削除機能

アソシエーションの設定で親レコードの削除に伴い紐づく子のレコードが削除されるようにする必要がある。
allow_destroy: trueは
親要素が削除された時、関連付けている情報もまとめて削除するためのオプションです。

model/join.rb
  has_many :date_answers, dependent: :destroy
  accepts_nested_attributes_for :date_answers, allow_destroy: true
model/shop.rb
  has_many :shop_answer, dependent: :destroy
model/shop_answer.rb
  belongs_to :join
  validates_presence_of :join
  belongs_to :shop

以上のアソシエーションをつけることで通常時と変わりなく、削除を実行することができる。
今回はビューとコントローラーの記述を割愛させていただきます。

最後に

ここまで読んで下さってありがとうございます。
はじめて個人で作成したアプリなのでアラが目立つかとは存じますが
ご容赦下さいますようお願いします。

また、何かご質問があればコメントお願いします。
お答えできる限りはお答え致します。

参考

https://qiita.com/kouuuki/items/5daf2b5f34273d8457f7

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

GitHub Actions を使った Rails アプリの自動デプロイを10分で理解する

こんにちは、駆け出しエンジニアのよしこ @k2_yoshikoukiです

CI/CDってカッコいいですよね。なんかこう、カッコいいんですよね(語彙力)

しかし「どうやって実装したらいいのかワケワカメ\(^o^)/」というエンジニアの方は多いと思います。でも実際はとても簡単なんだよということを知ってほしかったので、10分で読める記事で CI/CD のうちのCD(自動デプロイ)を実装していきたいと思います。

実作業時間は詰まらなかったら30分かからないくらいです。

ゴール

CI/CD をやってみたいがやったことがないし何から手を付けたら良いか分からないエンジニア向けに、Rails アプリ (6系) で自動デプロイを最速で実装する。

結果、GitHub Actions を使ったCI/CDの大枠を把握できて自力で実装できるようになる

実装フローを見ていく

  1. Rails アプリを作る
  2. Heroku に手動デプロイして動作確認
  3. GitHub Actions の初期設定
    • ./.github/workflows/{好きな名前}.yml の作成
    • 必要な鍵などの取得・GitHubへ設定

すでにアプリケーションがある方は 3 からです。たった2ステップで自動デプロイが実装できます。早速やっていきましょう

手順

Railsアプリの準備

  • rails new sample-rails-with-gha

    ~/tmp/sample-rails-with-gha
    ❯ rails -v
    Rails 6.0.3.4
    
    ~/tmp/sample-rails-with-gha
    ❯ ruby -v
    ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin19]
    # 後から2.7.2にアップデートします(デバックのため)
    
    bundle install
    rails server
    

    image.png

リポジトリの準備

  • GitHub上にリポジトリを作る
    https://github.com/new
  • ローカルにリポジトリを登録

    git remote add origin https://github.com/yoshikouki/sample-rails-with-gha.git
    

    image.png

    • アップロード

      git add -A
      git commit -m "rails new"
      git push -u origin master
      

デプロイの初期設定と初回デプロイ

  • 今回はデプロイ先に Heroku を使う(簡単なので)

    ~/tmp/sample-rails-with-gha master
    ❯ heroku --version
    
    zsh: command not found: heroku
    
  • Heroku 入ってなかった・・・

    brew tap heroku/brew && brew install heroku
    # https://devcenter.heroku.com/articles/heroku-cli
    
  • Heroku アカウントを作っておく
    https://signup.heroku.com/

  • Heroku にログイン(ターミナル上)

    heroku login --interactive
    
  • Heroku 用のアプリケーションを作る(アップロードする場所を作る)

    heroku create
    # URL を控えておく
    # https://sleepy-beyond-32826.herokuapp.com/
    # `$ heroku open` でも開ける
    

    image.png

  • Heroku は sqlite3 に対応していないため、./Gemfile を操作する

    image.png

  • Heroku へデプロイする

    git push heroku master
    
  • あらら

    image.png

    • エラーログ
    $ heroku logs
    2020-11-22T14:39:05.610528+00:00 app[web.1]: I, [2020-11-22T14:39:05.610419 #4]  INFO -- : [a6b71e5b-f13a-4224-a857-9b791da3ecc4] Started GET "/" for 133.32.232.41 at 2020-11-22 14:39:05 +0000
    2020-11-22T14:39:05.611222+00:00 app[web.1]: F, [2020-11-22T14:39:05.611152 #4] FATAL -- : [a6b71e5b-f13a-4224-a857-9b791da3ecc4]
    2020-11-22T14:39:05.611223+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] ActionController::RoutingError (No route matches [GET] "/"):
    2020-11-22T14:39:05.611224+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4]
    2020-11-22T14:39:05.611225+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call'
    2020-11-22T14:39:05.611225+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
    2020-11-22T14:39:05.611226+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] railties (6.0.3.4) lib/rails/rack/logger.rb:37:in `call_app'
    2020-11-22T14:39:05.611226+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] railties (6.0.3.4) lib/rails/rack/logger.rb:26:in `block in call'
    2020-11-22T14:39:05.611226+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] activesupport (6.0.3.4) lib/active_support/tagged_logging.rb:80:in `block in tagged'
    2020-11-22T14:39:05.611227+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] activesupport (6.0.3.4) lib/active_support/tagged_logging.rb:28:in `tagged'
    2020-11-22T14:39:05.611227+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] activesupport (6.0.3.4) lib/active_support/tagged_logging.rb:80:in `tagged'
    2020-11-22T14:39:05.611228+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] railties (6.0.3.4) lib/rails/rack/logger.rb:26:in `call'
    2020-11-22T14:39:05.611228+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/remote_ip.rb:81:in `call'
    2020-11-22T14:39:05.611228+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/request_id.rb:27:in `call'
    2020-11-22T14:39:05.611229+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] rack (2.2.3) lib/rack/method_override.rb:24:in `call'
    2020-11-22T14:39:05.611229+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] rack (2.2.3) lib/rack/runtime.rb:22:in `call'
    2020-11-22T14:39:05.611230+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] activesupport (6.0.3.4) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'
    2020-11-22T14:39:05.611230+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/executor.rb:14:in `call'
    2020-11-22T14:39:05.611231+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/static.rb:126:in `call'
    2020-11-22T14:39:05.611231+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] rack (2.2.3) lib/rack/sendfile.rb:110:in `call'
    2020-11-22T14:39:05.611232+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/host_authorization.rb:76:in `call'
    2020-11-22T14:39:05.611232+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] railties (6.0.3.4) lib/rails/engine.rb:527:in `call'
    2020-11-22T14:39:05.611232+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/configuration.rb:228:in `call'
    2020-11-22T14:39:05.611233+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/server.rb:713:in `handle_request'
    2020-11-22T14:39:05.611233+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/server.rb:472:in `process_client'
    2020-11-22T14:39:05.611233+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/server.rb:328:in `block in run'
    2020-11-22T14:39:05.611234+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/thread_pool.rb:134:in `block in spawn_thread'
    2020-11-22T14:39:05.612663+00:00 heroku[router]: at=info method=GET path="/" host=sleepy-beyond-32826.herokuapp.com request_id=a6b71e5b-f13a-4224-a857-9b791da3ecc4 fwd="133.32.232.41" dyno=web.1 connect=1ms service=4ms status=404 bytes=1902 protocol=https
    2020-11-22T14:39:05.858618+00:00 heroku[router]: at=info method=GET path="/favicon.ico" host=sleepy-beyond-32826.herokuapp.com request_id=831fe7f9-bc01-496d-a9ac-380c9c89dd3c fwd="133.32.232.41" dyno=web.1 connect=1ms service=2ms status=200 bytes=143 protocol=https
    
    • なるほどねえ(分かっていない) ActionController::RoutingError (No route matches [GET] "/"):
        # ./config/routes
        Rails.application.routes.draw do
          # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
        end
    
    • ないものは作ればいいんだよ(ドヤ顔)

      rails generate controller Home index
      
    • ルーティング ./config/routes もイジる

      image.png

    • よしよし(手動デプロイで動作を確認)

      image.png

GitHub Actions を設定していく

  • 空の設定ファイルを作る

    mkdir -p ./.github/workflows
    tocuh -p ./.github/workflows/sample_cd.yml
    
  • 設定していく

    • 設定ファイル

      name: Deploy to sample-rails-with-gha
      
      on:
        # master ブランチにプッシュされた場合に動作する
        push:
          branches:
            - master
      
      jobs:
        deploy:
          runs-on: ubuntu-latest
          steps:
            # GitHub リポジトリからコードを引っ張ってくる
            - name: Checkout
              uses: actions/checkout@v2
            # 前回のコンテナキャッシュがあれば使用する。なければキャッシュを作る
            - name: Cache multiple paths
              uses: actions/cache@v2
              with:
                path: vendor/bundle
                key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
                restore-keys: |
                  ${{ runner.os }}-gems-
            # Ruby をインストールする
            - name: Set up Ruby 2.7
              uses: actions/setup-ruby@v1
              with:
                ruby-version: 2.7
            # バンドラーをインストールし、初期化する
            - name: Bundle install
              run: |
                gem install bundler
                bundle config path vendor/bundle
                bundle install --jobs 4 --retry 3
            # ヘロクにデプロイする
            - uses: akhileshns/heroku-deploy@v3.6.8 # This is the action
              with:
                heroku_api_key: ${{secrets.HEROKU_API_KEY}}
                heroku_app_name: "sleepy-beyond-32826"
                heroku_email: "yoshikouki@gmail.com"
      
    • 簡単な設定ファイルなので説明はファイル内のコメントを参考にしてください。

      • アクションという Ruby でいう Gem のような機能を使用しています。
    • ドキュメント

    • secrets.HEROKU_API_KEY が必要なので Heroku で取得する

    • 取得した Key をGitHub で使用できるように設定する

      • sample-rails-with-gha のリポジトリ > Settings > Secrets
      • GitHub Actions で使用する非公開環境変数の設定画面になる > New repository secret
      • Name に HEROKU_API_KEY Value に先程Herokuで取得したキーをセット > Add secret
  • 準備は整ったので、master/main ブランチにプッシュしてみる

    デバッグ

    • actions/cache のドキュメントは bundle がインストールしてある前提だったので gem install bundler を追加した
    • Gemfile に記述されている ruby-version と GitHub Actions のコンテナ内のRubyを一致させること

      uses: actions/setup-ruby@v1
      with:
        ruby-version: 2.7
      
      • GitHub Actions setup-ruby ではマイナーバージョンまでしか指定できないみたいなので、手元Rubyのメンテナンス (ビルド) バージョンをあげることになるかもしれない
      • マイナーバージョンが上がるわけではないので、素直に上げておけばいいと思う

そして...

image.png

  • 最後の push を済ませれば・・・

    image.png

  • 感極まる?

    image.png

まとめ

以上で Rails アプリ (6系) で自動デプロイまでを最速で実装しました。

以後は、master (main) ブランチへ push するだけで heroku へ自動デプロイされます。これで2〜3コマンド分の実行の手間が省けたことになりますし、あなたはもう完全に CI/CD を習得したエンジニアです。

(実際のところ、今回はCI部分を何も扱っていませんし、デプロイに関してもデータベースを触っていないので中途半端な状態ではあるのですが、最低限は実装できたので良しとしましょう。してください。)

参考URL

https://github.co.jp/features/actions

https://docs.github.com/ja/free-pro-team@latest/actions

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

【Rails】formの検索ボタンを虫眼鏡のアイコンにする

Ruby on RailsでFont-Awesomeを初めて使ったの投稿しました。

Font Awesome HP
https://fontawesome.com/icons?d=gallery

ポイント:使いたいアイコンのUnicodeの前に \u をつける

1 . CDNでfont-awesomeを読み込む

<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

2 . submitの記述の後を下記に変更。

<%= f.submit "\uF002" %>

3 . cssで下記を追加

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

【Rails / FontAwesome】formの検索ボタンを虫眼鏡のアイコンにする

Ruby on RailsでFont-Awesomeを初めて使ったの投稿しました。

Font Awesome HP
https://fontawesome.com/icons?d=gallery

ポイント:使いたいアイコンのUnicodeの前に \u をつける

1 . CDNでfont-awesomeを読み込む

<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

2 . submitの記述の後を下記に変更。

<%= f.submit "\uF002" %>

3 . cssで下記を追加

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

【Rails】form_withの使い方について解説【初心者向け】

はじめに

Rails開発をする中で、form_withを使う機会は非常に多いですが、

  • 結局form_withとは何なのか?
  • form_withにはどんな役割があるのか?
  • form_withはどのように用いたらいいのか?

といった悩みを抱えている方は多いと思います。

本記事はそのような方達に向けて、書いた記事となります。

ぜひ最後までご覧下さい。

使用技術

Ruby on Rails 5.1〜
Ruby 2.6〜

筆者の自己紹介

  • テックキャンプ86期生(2020/9/21~)
  • 執筆時点で学習開始から約9週間経過
  • 約6週間で最終課題(メルカリクローンアプリの作成)を終了

本記事の内容

1.form_withとはそもそも何者なのか?
2.form_withはどのような役割を果たしているのか?
3.form_withの具体的な記述法
4.まとめ
5.最後に
6.参照サイト

1.form_withとはそもそも何者なのか?

form_withは①ヘルパーメソッドの一種で、ビューファイル上で②フォーム入力欄を作成できるメソッドのことを言います。

①ヘルパーメソッド

ヘルパーメソッドとはRails内部で定義されているメソッドのことで、ビューファイルにおける記述をシンプルにするためのものです。

ビューファイルではERBタグ(本記事ではビューファイルにテンプレートエンジンとしてerbを使用)を使えば、直接Rubyの記述を書くことはできますが、DRYの視点から見た場合そのようなコードは綺麗なコードとは言えません。

そういったことを防ぐためにヘルパーメソッドは使用されます。

ヘルパーメソッドを使用しない場合
<%= form action="/posts" method="post" %>
  <%= input type="text" name="content" %>
  <%= input type="submit" value="投稿する" %>
<%= /form %>
ヘルパーメソッドを使用した場合
<%= form_with url: "/posts", method: :post, local: true do |form| %>
  <%= form.text_field :content %>
  <%= form.submit '投稿する' %>
<% end %>



またヘルパーメソッドはform_withの他にも様々なものが存在していて、有名なものでいえばlink_toメソッドが存在します。

※他のヘルパーメソッドが気になる方は下記で詳細を確認してみてください
ActionView::Helpers

②フォーム入力欄を作成できる

またヘルパーメソッドはビューファイル上で用いられ、HTMLの代わりのような役割を果たします。

具体的にはHTMLタグを出現させたり、テキストの加工を行うことが可能です。

form_withの場合は、HTMLにおけるinputタグにあたるものを生成できます。

つまりフォーム入力欄が作成できることになります。

HTMLのフォーム記述
<form action="/posts" method="post">
  <input type="text" name="content">
  <input type="submit" value="投稿する">
</form>
ヘルパーメソッドを用いたフォーム記述
<%= form_with url: "/posts", method: :post, local: true do |form| %>
<%= form.text_field :content %>
<%= form.submit '投稿する' %>
<% end %>

Image from Gyazo

③まとめ

  • ヘルパーメソッドとはRails内部で定義されているメソッドのことで、ビューファイルでの記述をシンプルにするためのもの
  • form_withはヘルパーメソッドの一種である
  • form_withはHTMLでinputタグにあたるものを作成し、フォーム入力欄を作成することができる

2. form_withはどのような役割を果たしているのか?

次にform_withメソッドは実際どのような役割を果たしているのか?について考えていきます。

結論からいうと、form_withメソッドは

ユーザーからサーバー側に情報を送信する

という役割を果たしています。

これだけでは少しわかりづらいので、実際のアプリケーションであるTwitterを例に考えてみましょう

Twitterでform_withがどういう役割を果たしているのか?考えてみる

Twitterには様々な機能がありますが、その中でも今回は投稿機能に着目して考えてみます

Twitterの投稿機能では

①ユーザーが投稿フォームに値を入力し、ボタンをクリックする
②その投稿がサーバー側に送られる
③サーバーに送られた投稿内容が投稿一覧表示画面に反映される

という流れで処理が行われています。

この際にform_withがどのような動きをしているのか?を考えてみましょう。


①ユーザーが投稿フォームに値を入力し、ボタンをクリックする

まず最初に、form_withで作成された投稿フォームにユーザーが値を入力し、送信(投稿)ボタンをクリックします。

作成した投稿フォーム
<%= form_with url: "/posts", method: :post, local: true do |form| %>
<%= form.text_field :content %>
<%= form.submit '投稿する' %>
<% end %>

Image from Gyazo

②その投稿がサーバー側に送られる

次に、送信(投稿)ボタンを押したため、先ほど作成したツイートの内容が送信されます。

ここで大半の人が疑問に思っていることが

「ツイートが送信されるのは感覚的にはわかるが、どこへ送信されているのか?」
「ツイートはどのようにして送信されているのか?」

だと思います。

なので、この送信される過程についてもう少し詳しく見ていきましょう。

この過程は大きく分けると、次の3つに分かれます。

  1. リクエストが送信される
  2. ルーティングによって、「どのコントローラーのどのアクションを行うのか?」が決められる
  3. 2で指定されたコントローラーへツイートの内容が渡される

1. リクエストが送信される

まず送信ボタンをクリックすると、使用しているブラウザからサーバー側にリクエストが送られます。

リクエストに関してわからない人がいれば、「HTTPリクエスト」と検索してみてください。
簡単にいうと、ユーザーからサーバーへの「〜して欲しい!」という要望みたいなものです。

そしてこの時リクエストには パラメーターと呼ばれるものが含まれます

※ パラメーター

パラメーターとはリクエストに含まれてサーバーの外部から渡されるデータのことを指します。

わかりやすく言い換えると、外から入ってくる値のことです。


少し身近な例に例えて考えてみましょう。

例えば、自動販売機を想像してみてください。

自販機は100円を入れたら、「100」と料金表示の欄に表示されます。また1000円札を入れれば、「1000」と表示されます。

この時、自販機の料金表示機能をプログラムと見立てた場合に、自販機に入金したお金というのは自販機側からは外から入ってくるものであるので、パラメータになります。

これがパラメータのイメージです。

そしてパラメータにはいくつか種類があります。

例えば、URLに含まれるパラメータはURLパラメータと呼ばれたりします。これは http://tweets.jp/tweets/1/1ように、URLの末尾に文字列が含まれているものを指します。

2. ルーティングによって、「どのコントローラーのどのアクションを行うか」が決定される

次に送信されたリクエストは、サーバー側のルーターに遷移します。

このルーターでは受け取ったリクエストを見て、「どのコントローラのなんの処理を行うか」を決定します。

このように、「このリクエストに対してはこのコントローラーのこの処理を行う」みたいなルールのことをルーティングと言います。

ここでは詳細の説明は省きます。

また「コントローラーやアクション」などの詳しい説明に関しても、今回は省かせていただきます。
RailsにおけるMVCの理解は最低限できているものとして本記事は進ませていただきますので、ご注意ください。

3. 2で指定されたコントローラーへツイート内容(パラメーター)が渡される

最後に、2で指定されたコントローラーへリクエストに含まれるパラメーターが渡されます

ただこの時、パラメーターには様々な情報があるため、そのまま渡されると非常にわかりづらくなってしまいます。

そこで、パラメータはparamsと呼ばれるものに一度格納されてからコントローラーへ渡されます。

※ params

paramsとはハッシュのような構造を取るパラメーターを格納するためのものです。

実際には以下のような形をとっています。

Image from Gyazo

しかし、これだと少しわかりづらい方もいらっしゃると思うので、「paramsはパラメータを格納するための箱のようなもの」と考えると、イメージしやすいかもしれません。

以下のようなイメージです。


1~3をまとめると

フォームに入力された値は、リクエストの中に含まれるパラメータとしてリクエストと共にサーバー側のルーターに送信され、そこでparamsに格納されてサーバー側のコントローラーへ送信される

ということになります。

③サーバーに送られた投稿内容が投稿一覧表示画面に反映される

次にサーバーに送られた投稿内容は、コントローラーによって処理が行われます。

今回だと、投稿一覧画面に投稿内容を反映させるために、送られてきた投稿内容を保存するという処理が行われます。

具体的には

tweets_controller.rb
  def create
    Tweet.create(tweet_params)
  end

のような処理が行われ、投稿一覧画面に反映されます。


Twitterの投稿機能の場合は、このように処理が行われます。

ではこの時、form_withはどのような役割を果たしていたのか?考えてみましょう。

form_withは、上の一連の処理の中では①で登場しました。

つまり、サーバーに投稿内容を送信するという役割を果たしていることがここでわかるかと思います。

3. form_withの具体的な記述法

form_withの表記の仕方は大きく2つに分かれます。

①基本形

基本形
<%= form_with url: "パス" do |form| %>
  フォーム内容
<% end %>

urlにはリクエスト先のパス(URLと考えてもらって大丈夫です)が入ります。

そして、この基本形の場合はオプションをつけることが可能です。

※オプションについて

オプションとはメソッドに補足的に付けられるもので、メソッドの情報を補足する働きがあります。

基本形の場合は以下のようなオプションをつけることが可能です。

  • methodオプション
    form_withで送信されるHTTPリクエストのデフォルトはPOSTであり、methodオプションはそのHTTPリクエストを変更したい場合に用います

  • localオプション
    form_withから送られたリクエストはデフォルトではHTTP通信が行われていないため、それをHTTP通信にする場合localオプションを用います

オプションを付けた場合
<%= form_with url: "/posts", method: :post, local: true do |form| %>
 フォーム内容
<% end %>

②応用形

①のようにurlにリクエスト先を指定したり、methodオプションでHTTPメソッドを指定したりするのが面倒な場合はそれらを省略して、以下のように記述することができます

応用形
<%= form_with model: モデルクラスのインスタンス do |form| %>
  フォーム内容
<% end %>

そして、この応用形には他にも様々なメリットがあるためform_withを用いる場合はこちらがよく使われる傾向にあります。

4. まとめ

  • form_withはヘルパーメソッドの一種で、ビューファイル上でフォーム入力欄を作成できるメソッドのことを言います。
  • form_withメソッドはユーザーからサーバー側に情報を送信するという役割を果たしている
  • form_withの表記は大きく分けると、urlオプションとmomdelオプションを使う場合の2種類が存在する

5. 最後に

本記事の内容がみなさんの参考になれば嬉しいです。

最後までご覧いただきありがとうございました。

6. 参照サイト

https://qiita.com/snskOgata/items/44d32a06045e6a52d11c
https://qiita.com/tomoharutt/items/46e6358acc8b45cd7db1
https://qiita.com/hayulu/items/5bf26656d7433d406ede

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

【Rails6】Active Job + Sidekiqを動かしてみた

Railsの非同期処理をActive Job + Sidekiqで実装したのでメモを残します。

※RailsアプリケーションはDocker環境で構築済みの前提です。環境構築はこちら

※Active Jobとバックエンドの比較はこちら

環境

  • Ruby 2.7.2
  • Rails 6.0.3.4
  • MySQL 8.0.20
  • Redis 6.0.9
  • Sidekiq 6.1.2
  • Docker version 19.03.13

1. Redisの導入

まずredisコンテナを用意します。
ポート番号はdocker-compose.override.ymlで指定していますが、下記で設定して問題ないと思います。

docker-compose.yml
version: '3.7'

services:
  db:
    image: mysql:8.0.20
    volumes:
      - mysql:/var/lib/mysql:delegated
    command: --default-authentication-plugin=mysql_native_password
    env_file: .env

  web:
    build:
      context: .
      dockerfile: Dockerfile
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    tty: true
    stdin_open: true
    env_file: .env
    depends_on:
      - db
      - chrome
      - redis
    volumes:
      - .:/app:cached
      - bundle:/usr/local/bundle:delegated
      - node_modules:/app/node_modules

  chrome:
    image: selenium/standalone-chrome:3.141.59
    volumes:
      - /dev/shm:/dev/shm

  redis:
    image: redis:6.0.9
    env_file: .env
    command: redis-server --appendonly yes
    volumes:
      - redis:/data

volumes:
  mysql:
  bundle:
  node_modules:
  redis:
docker-compose.override.yml
version: '3.7'

services:
  db:
    ports:
      - 3306:3306
  web:
    ports:
      - 3000:3000
  chrome:
    ports:
      - 4444:4444
  redis:
    ports:
      - 6379:6379

Redisの設定ファイルを追加します。
host名に注意してください。

config/redis.yml
default: &default
  db:
    sidekiq: 0
    # cache:   1
    # session: 2

development:
  <<: *default
  host: redis

test:
  <<: *default
  host: redis

2. Sidekiqの設定

まずはGemを追加します。
ruby:Gemfile
gem 'sidekiq'

bundle installを実行し、設定ファイルを追加します。
詳しくは、こちらを参照してください。

config/sidekiq.yml
:verbose: false
:max_retries: 1
:concurrency: 10
:pidfile: ./tmp/pids/sidekiq.pid
:logfile: ./log/sidekiq.log
:queues:
  - development_default

続いてSidekiqとRedisの接続情報も追加します。

config/initializers/sidekiq.rb
redis_config = YAML.load_file('config/redis.yml')[Rails.env]
redis_config['db'] = redis_config['db']['sidekiq']

Sidekiq.configure_server do |config|
  config.redis = {
    url: "redis://#{redis_config['host']}/#{redis_config['db']}"
  }
end

Sidekiq.configure_client do |config|
  config.redis = {
    url: "redis://#{redis_config['host']}/#{redis_config['db']}"
  }
end

ダッシュボードのルーティングも設定します。

config/routes.rb
require 'sidekiq/web'

Rails.application.routes.draw do
  mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq
end

3. Active Jobの設定

Active Jobで非同期処理を実装していきます。

config/application.rb
require "active_job/railtie"

module App
  class Application < Rails::Application
    config.active_job.queue_adapter = :sidekiq
    config.active_job.queue_name_prefix = Rails.env # これは任意
  end
end

4. ジョブの作成と動作確認

ようやくジョブを作成します。

rails g job sample
app/jobs/sample_job.rb
class SampleJob < ApplicationJob
  queue_as :default

  def perform
    puts '--------------------------------'
    puts '------------  Test  ------------'
    puts '--------------------------------'
  end
end

続いて動作確認を行います。
dockerコンテナを起動し、RailsとRedisが動いていることを確認します。
コンテナ内でsidekiqを起動します。

$ bundle exec sidekiq -C config/sidekiq.yml

2020-11-23T07:06:57.513Z pid=74 tid=9om INFO: Booting Sidekiq 6.1.2 with redis options {:url=>"redis://redis/0"}


               m,
               `$b
          .ss,  $$:         .,d$
          `$$P,d$P'    .,md$P"'
           ,$$$$$b/md$$$P^'
         .d$$$$$$/$$$P'
         $$^' `"/$$$'       ____  _     _      _    _
         $:     ,$$:       / ___|(_) __| | ___| | _(_) __ _
         `b     :$$        \___ \| |/ _` |/ _ \ |/ / |/ _` |
                $$:         ___) | | (_| |  __/   <| | (_| |
                $$         |____/|_|\__,_|\___|_|\_\_|\__, |
              .d$$                                       |_|


2020-11-23T07:06:57.938Z pid=74 tid=9om INFO: Booted Rails 6.0.3.4 application in development environment
2020-11-23T07:06:57.939Z pid=74 tid=9om INFO: Running in ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]
2020-11-23T07:06:57.939Z pid=74 tid=9om INFO: See LICENSE and the LGPL-3.0 for licensing details.
2020-11-23T07:06:57.939Z pid=74 tid=9om INFO: Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org
2020-11-23T07:06:57.946Z pid=74 tid=9om INFO: Starting processing, hit Ctrl-C to stop

別タブのターミナルでRailsコンソールからSampleJobをキューイングしてみます。

$ rails c
Loading development environment (Rails 6.0.3.4)
> SampleJob.set(wait: 5.second).perform_later

Enqueued SampleJob (Job ID: fdcf5c60-3542-4b26-bfd2-9662ffafada9) to Sidekiq(development_default) at 2020-11-23 07:08:49 UTC
=> #<SampleJob:0x00005567e535e4c0
 @arguments=[],
 @exception_executions={},
 @executions=0,
 @job_id="fdcf5c60-3542-4b26-bfd2-9662ffafada9",
 @priority=nil,
 @provider_job_id="7c42f602bd499e75efecad26",
 @queue_name="development_default",
 @scheduled_at=1606115329.3860393>
>

キューイングすると先ほど起動したSidekiq側でジョブが実行されたことが確認できました。

2020-11-23T07:08:52.800Z pid=74 tid=b6y class=SampleJob jid=7c42f602bd499e75efecad26 INFO: start
--------------------------------
------------  Test  ------------
--------------------------------
2020-11-23T07:08:53.067Z pid=74 tid=b6y class=SampleJob jid=7c42f602bd499e75efecad26 elapsed=0.267 INFO: done
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【自分メモ】モデル/コントローラー/ビューの役割

モデル…コントローラーで定義したインスタンス変数にデータを入れるため、コントローラーからモデルへデータを取ってくるよう指示が出る(モデル名.all)→指示を受けたモデルはデータベースからデータを取得後、コントローラーのインスタンス変数へ入れる。
またコントローラーから指示を受けて新しいオブジェクトを作成する(モデル名.new)。

コントローラー…インスタンス変数に入れるデータをモデルに持ってくるよう指示を出したり、そのインスタンス変数をビューに渡したりする。

ビュー…コントローラーから受け取ったインスタンス変数を元にhtmlファイルを作成する。

※自分の考えをメモとして残していますので、間違っていたり修正した方が良い点がございましたら、ドシドシつっこみいただけると嬉しいです!

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

【自分メモ】ストロングパラメータ

外部からの不正なアクセスを受け付けないようにするものです。

例えば、新規投稿したデータをデータベースに保存する際、新規投稿ではない別のデータを保存するように書き換える人がいました。そうすることで、本来なら公開されないはずの情報を操作されてしまい、不正に情報が流出してしまうきっかけになります。
そのような不正なアクセスを受けなくするため、ストロングパラメータにて「カラムを登録したモデル(オブジェクト)しかデータは持って来れませんよ〜」と記入し、加えてprivate下に置くことで特定のコントローラーでしか呼び出せないようにしました。(また、private下に定義することでストロングパラメータはアクションとして認識されなくなります)

また、ストロングパラメータはマスアサインメント脆弱性というセキュリティ上の問題を解決するために設定するものになります。

※自分の考えをメモとして残していますので、間違っていたり修正した方が良い点がございましたら、ドシドシつっこみいただけると嬉しいです!

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

Rails + Docker でhello worldを表示するまでを簡単に

アプリ作成時に毎回調べている気がするので、備忘録的にまとめてみました。
下記のapp名の箇所には適宜アプリ名を入力して下さい。

※間違いがありましたら変更しますのでコメント頂けると嬉しいです^^

新規ディレクトリ作成 〜 hello world!!まで

まず、アプリの土台となるディレクトリを作ります。
さらに、touchコマンドで、2つの空ファイルを作成します。

$ mkdir app名 && cd app名
$ touch Gemfile Gemfile.lock

私は、VScodeを使用して開発しているので
code コマンドを使用して起動しています。
ちなみに、code コマンドは起動と作成をしてくれます。

Gemfileを編集します。

$ code Gemfile
Gemfile
source 'https://rubygems.org'
gem 'rails', '~>5.2'

Dockerfileを作成して編集します。

$ code Dockerfile
Dockerfile
FROM ruby:2.5
RUN apt-get update
RUN apt-get install -y \ 
    build-essential \
    libpq-dev \
    nodejs \
    postgresql-client \
    yarn \
    vim

WORKDIR /app名
COPY Gemfile Gemfile.lock /app名/
RUN bundle install

docker-compose.ymlファイルを作成して編集します。

$ code docker-compose.yml
docker-compose.yml
version: "3"

volumes:
  db-data:

services:
  web:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - ".:/app名"
    environment:
      - "DATABASE_PASSWORD=postgres"
    tty: true
    stdin_open: true
    depends_on:
      - db
    links:
      - db

  db:
    image: postgres
    volumes:
      - "db-data:/var/lib/postgresql/data"
    environment:
      - "POSTGRES_HOST_AUTH_METHOD=trust"
      - "POSTGRES_USER=postgres"
      - "POSTGRES_PASSWORD=postgres"

コンテナの起動を行い、webコンテナに入って、rails new します。

$ docker-compose up --build -d
$ docker-compose exec web bash
$ rails new . --force --database=postgresql

rails new で作成された database.yml ファイルに追記します。

database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  host: db  #追記
  user: postgres  #追記
  port: 5432  #追記
  password: <%= ENV.fetch("DATABASE_PASSWORD") %>  #追記
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
$ rails db:migrate
$ rails s -b 0.0.0.0

Chromeの検索バーに、localhost:3000 と入力してアクセスするとhello worldが表示されているかと思います!

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

【自分用】マイグレーション関連【ロールバック、カラムの追加、型の変更】

完全に自分用の書き殴りです。

誤り等ございましたら、コメント下さい!

ロールバック

  • rails db:migrate:down VERSION=20190611235049
    • VERSIONで指定した箇所まで戻れる
  • rails db:rollback STEP=3
    • 現在の migration ファイルから遡って、STEPで指定した回数戻れる

カラムの追加

  1. rails g migration Addカラム名Toテーブル名 @@@:integer @@@:string
  2. rails db:migrate
  • Add 以下はキャメルケースで記載
  • カラム名のところは何を書いてもいいが、追加したいカラム名の複数形に統一しておく
  • テーブル名にはカラムを追加するテーブル名の複数形を書く
  • @@@はカラム名を単数形で記入

以下は user テーブルにふりがなを追加したときの例。

class AddKanaToUser < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :kana, :string
  end
end

カラム型の変更

rails g migration change_data_<カラム名>_to_<テーブル名の複数形>

class ChangeColumnToUsers < ActiveRecord::Migration[5.2]
  def change
  end
end

上記のように空の migration ファイルが作成されるので、下記のように編集

class ChangeColumnToUser < ActiveRecord::Migration[5.2]
  def up
    change_column :users, :detail, :text
  end

  def down
    change_column :users, :detail, :string
  end
end

注意点

カラムの追加カラム型の変更では、方法がやや異なっていて、migration ファイルを触る時は、追加(up)するだけでなく、削除(down)するときのことも考えなければならない。

つまり、カラムの追加では、change で書いても rollback 時など down する際にもよしなにしてくれるが、カラム型の変更で change を使ってしまうと、up 時は良くても、down 時に rollback できないというようなことが起こってしまう。ということ。

簡単なことだが、ここを理解したときに、migration ファイルに対する怖さが一気に消えた。

おまけ

これからは上記の様に書くとして、これまでに書いてしまった箇所を rollback するさいはどうすればいいのか...

これは簡単で、今ある migration ファイルの change と記載している箇所を、up に上書き変更して、その下に down 時の動作を追記すればよい。down 時の動作は変更前のカラム型のこと。

なんとなく、migration ファイルを上書きするのは怖かったが、これで問題なく動作した。

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

Docker×Rails6(メモ)

事前準備

環境構築

Docker 関連ファイルを用意

FROM ruby:2.6.3-alpine

ENV LANG=ja_JP.UTF-8
ENV TZ=Asia/Tokyo
ENV ROOT=/myapp \
    GEM_HOME=/bundle \
    BUNDLE_PATH=$GEM_HOME
ENV BUNDLE_BIN=$BUNDLE_PATH/bin
ENV PATH /app/bin:$BUNDLE_BIN:$PATH


WORKDIR $ROOT

RUN apk update && \
    apk upgrade && \
    apk add --no-cache \
        gcc \
        g++ \
        libc-dev \
        libxml2-dev \
        linux-headers \
        make \
        nodejs \
        postgresql \
        postgresql-dev \
        tzdata \
        imagemagick \
        yarn && \
    apk add --virtual build-packs --no-cache \
        build-base \
        curl-dev

COPY Gemfile $ROOT
COPY Gemfile.lock $ROOT

RUN bundle install -j4
# 不要ファイル削除
RUN rm -rf /usr/local/bundle/cache/* /usr/local/share/.cache/* /var/cache/* /tmp/* && \
apk del build-packs

COPY . $ROOT

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["sh", "/usr/bin/entrypoint.sh"]
EXPOSE 3000
docker-compose.yml
version: "3.8"

services:
  db:
    image: postgres:11.0-alpine
    volumes:
      - postgres:/var/lib/postgresql/data:cached
    ports:
      - "5432:5432"
    environment:
      PGDATA: /var/lib/postgresql/data/pgdata
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=ja_JP.UTF-8"
      TZ: Asia/Tokyo
  app:
    build: .
    command: ash -c "rm -f tmp/pids/server.pid && ./bin/rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp:cached
      - rails_cache:/myapp/tmp/cache
      - node_modules:/myapp/node_modules:cached
      - bundle:/bundle:cached
    tmpfs:
      - /tmp
    tty: true
    stdin_open: true
    ports:
      - "3000:3000"
    environment:
      RAILS_ENV: development
      NODE_ENV: development
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_USER: postgres
      DATABASE_PASSWORD: password
      WEBPACKER_DEV_SERVER_HOST: webpacker
    depends_on:
      - db
      - webpacker

  webpacker:
    build: .
    command: ./bin/webpack-dev-server
    volumes:
      - .:/myapp:cached
      - node_modules:/myapp/node_modules:cached
    environment:
      RAILS_ENV: development
      NODE_ENV: development
      WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
    tty: false
    stdin_open: false
    ports:
      - "3035:3035"

volumes:
  rails_cache:
  node_modules:
  postgres:
  bundle:
source 'https://rubygems.org'
gem 'rails',      '6.0.3'
gem 'devise'

# to upload images
gem 'carrierwave', '~> 2.0'
gem "mini_magick"
Gemfile.lock(empry)
```entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

cmd

docker-compose run app rails new . --force --no-deps --database=postgresql --skip-bundle

Gemfile を編集する

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.3'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.0.3', '>= 6.0.3.4'
# Use postgresql as the database for Active Record
gem 'pg', '>= 0.18', '< 2.0'
# Use Puma as the app server
gem 'puma', '~> 4.1'
# Use SCSS for stylesheets
gem 'sass-rails', '>= 6'
# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
gem 'webpacker', '~> 4.0'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.7'

gem 'devise'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Active Storage variant
# gem 'image_processing', '~> 1.2'

# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.4.2', require: false

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
  gem 'web-console', '>= 3.3.0'
  gem 'listen', '~> 3.2'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

group :test do
  # Adds support for Capybara system testing and selenium driver
  gem 'capybara', '>= 2.15'
  gem 'selenium-webdriver'
  # Easy installation and use of web drivers to run system tests with browsers
  gem 'webdrivers'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

# to upload images
gem 'carrierwave', '~> 2.0'

gem "mini_magick"

その後のコマンド

docker-compose run app bundle update
docker-compose run app rails webpacker:install

database.yml を修正

config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  username: postgres
  password: password
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
...

Troubleshooting

up 時に check_yarn_integrity関連 というエラーが出たら webpacker.yml を修正

config/webpacker.yml
...
development:
  <<: *default
  compile: true 

  # Verifies that correct packages and versions are installed by inspecting package.json, yarn.lock, and node_modules
  check_yarn_integrity: false # true -> falseに変更
...
docker-compose build
docker-compose up -d
docker-compose run app rake db:create

また up 時に "webpack-dev-server" not found が出たら

docker-compose run app yarn add webpack-dev-server
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails Tutorial 拡張機能のメッセージ機能を作ってみた(その3):DMの作成

Rails Tutorialの第14章にある、メッセージ機能を作る件の続きです。

前回までで表示する画面ができました。作成する画面を作ります。

DMを作成

DMを作成する機能を作ります。micropostを作成するところを参考にします。tutorialのリスト 13.36: を参考にコントローラーにcreateアクションを作ります。

app/controllers/dms_controller.rb
class DmsController < ApplicationController
  before_action :logged_in_user, only: [:index, :create, :destroy]

  def index
      @user = current_user
      @dms = @user.sent_dms.paginate(page: params[:page])
  end

  def create
    @dm = current_user.sent_dms.build(dm_params)
    if @dm.save
      flash[:success] = "DM sent!"
    else
      render 'dms/index'
    end
  end

  private
    def dm_params
      params.require(:dm).permit(:content,:receiver)
    end
end
app/views/dms/index.html.erb
<section class="micropost_form">
  <%= form_for(@dm) do |f| %>
    <%= render 'shared/error_messages', object: f.object %>
    <div class="field">
      <%= f.label :receiver_name %>
      <%= f.text_field :receiver_name, class: 'form-control' %>

      <%= f.text_area :content, placeholder: "new DM..." %>
    </div>
    <%= f.submit "Send", class: "btn btn-primary" %>
  <% end %>   
</section>

rails serverで画面を表示してみます。
エラーになりました。

 Showing /home/ubuntu/environment/sample_app/app/views/dms/index.html.erb where line #13 raised:
First argument in form cannot contain nil or be empty

@dmがnilなのでエラーになったと考えました。リスト 13.40を参考に修正します。

app/controllers/dms_controller.rb
  <%= form_for(@dm) do |f| %>

  def index
      @user = current_user
      @dms = @user.sent_dms.paginate(page: params[:page])
      @dm = current_user.sent_dms.build if logged_in?

別のエラーになりました。

ActionView::Template::Error (undefined method `receiver_name' for #<Dm:0x00007ff01420e4a8>
Did you mean?  receiver_id

formについて理解が足りないようで、読み直します。
7.2.1でform_forの引数はActive Recordのオブジェクトを取り込むとあります。
一方で、8.1.2ではform_forにActive RecordにはないSessionを扱っています。
リスト8.7で検索をしているところを参考に使えそうです。
form_forの引数にハッシュを使っていることが分かりました。
form_forを修正します。
修正前: <%= form_for(@dm) do |f| %>
修正後: <%= form_for(:dm) do |f| %>

app/views/dms/index.html.erb
<section class="micropost_form">
  <%= form_for(:dm, url:new_dm_path) do |f| %>

同じエラーです。

ActionView::Template::Error (undefined method `receiver_name' for #<Dm:0x00007f962c6ab110>
      <%= f.text_field :receiver_name, class: 'form-control' %>

メソッドがないというので、モデルにある列名ならよいのかと考え、試しに変えてみます。
変更前:receiver_name
変更後:receiver_id

      <%= f.text_field :receiver_id, class: 'form-control' %>

画面が表示されました。生成されたページのソースを見てみます。

 <form action="/dms/new" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="&#x2713;" /><input type="hidden" name="authenticity_token" value="rSSWCwPnDGlcWrtuhvyk4BZdbOzImu6+XGx9ZVGKqJcUdwsYZdqCxsBOVAPsSHYj0L6plKtBiqAigicYUtCyYA==" />
    <div class="field">
      <label for="dm_receiver_name">Receiver name</label>
      <input class="form-control" type="text" name="dm[receiver_id]" id="dm_receiver_id" />

      <textarea placeholder="new DM..." name="dm[content]" id="dm_content">

form_forの引数がActiveRecordと関連していて、列名かどうかをチェックする動きをしています。
試しにdmをdm1と変えてみたところ、列名でなくてもエラーは起きなくなりました。

app/views/dms/index.html.erb
  <%= form_for(:dm1, url:new_dm_path) do |f| %>
    <%#= render 'shared/error_messages', object: f.object %>
    <div class="field">
      <%= f.label :receiver_name %>
      <%= f.text_field :receiver_name, class: 'form-control' %>     
      <%= f.text_area :content, placeholder: "new DM..." %>
    </div>
    <%= f.submit "Send", class: "btn btn-primary" %>
  <% end %>

試しにPOSTを画面で実行

receiver_nameをキーとしてユーザーを見つけ、DMをデータベースに追加します。
リスト12.5を参考にします。
Sendボタンをクリックします。

エラーが起きました。

No route matches [POST] "/dms/new"

ページのソースを調べると

 <form action="/dms/new"

となっています。
urlに指定するのはPOSTのpathだと分かりましたので、修正します。

  <%= form_for(:dm1, url:dms_path) do |f| %>

別のエラーが起きました。

Param is missing or the value is empty: dm
      params.require(:dm).permit(:content,:receiver)
app/controllers/dms_controller.rb:28:in `dm_params'

パラメーターを調べます。

{"utf8"=>"✓", "authenticity_token"=>"QnULF0hViVk9ty/LggtG1fcwzTYNvfp8HMJ6nvq2Z/H9qG/1nSzPgR1+97W7osFg250OdLAO1q8vplnjjIbvNQ==", "dm1"=>{"receiver_name"=>"gege", "content"=>"agege"}, "commit"=>"Send"}

dmをdm1に変更する必要がありそうなので、修正します。
変更前:params.require(:dm).permit(:content,:receiver)
変更後:params.require(:dm1).permit(:content,:receiver)

別のエラーが起きました。

'nil' is not an ActiveModel-compatible object. It must implement :to_partial_path.
 <%= render @dms %>  

@dmsがnilとは?、@dmsを設定していないのでエラーになったと考えます。

createでリストを表示するところに、indexと同じ行をコピーします。重複感があるのですが、まとめるのは後廻しにします。その結果、メッセージが正常に表示されました。

dm6.png

受信者がいないケースのテスト

テストを作ります。
リスト8.9を参考にします。

test/integration/dms_test.rb
  test "send dm with invalid receiver" do
    log_in_as(@user)
    get dms_path
    post dms_path, params: { dm1: {receiver_name: "" }}
    assert_template 'dms/index'
    assert_not flash.empty?
  end

リスト13.36を参考に、DMを送るアクションを作ります。

app/controllers/dms_controller.rb
 def create
    @user = current_user
    @dms = @user.sent_dms.paginate(page: params[:page])
    # receiverを探す
    @receiver = User.find_by(name: params[:dm1][:receiver_name])
    if @receiver
      @dm = @user.sent_dms.build 
      @dm.receiver_id = @receiver.id
      @dm.content = params[:dm1][:content]
      @dm.save
      flash[:success] = "DM sent!"
      redirect_to dms_path
    else
      flash.now[:danger] = 'Receiver not found'
      render 'index'
    end

DMを送ってみます。無事表示されました。
dm7.png

contentがブランクのテスト

contentをブランクにしてDMを送ってみます。
画面にはエラーが表示されません。コンソールのログを調べます。

   (0.1ms)  rollback transaction
Redirected to https://7eca7b943b584a2296afeb1d7ceb9db2.vfs.cloud9.us-east-2.amazonaws.com/dms
Completed 302 Found in 10ms (ActiveRecord: 0.7ms)
NoMethodError in Dms#index
undefined method `errors' for nil:NilClass
<% if object.errors.any? %>

8章と12章をもう一度読みます。
receiverがnot foundのときを再テストしてみます。
エラーが表示されました。

@dmをbuildしていないので、nilになっているのではと考え、buildしている行をreceiverを探す判定の前に移しました。

app/controllers/dms_controller.rb
 def create
    @user = current_user
    @dms = @user.sent_dms.paginate(page: params[:page])
    @dm = @user.sent_dms.build 
    # receiverを探す
    @receiver = User.find_by(name: params[:dm1][:receiver_name])
    if @receiver
      @dm.receiver_id = @receiver.id
      @dm.content = params[:dm1][:content]
      if @dm.save
        flash[:success] = "DM sent!"
        redirect_to dms_path
      else
        render 'index'
      end
    else
      flash.now[:danger] = 'Receiver not found'
      render 'index'
    end
 end

not foundのメッセージが表示されるようになりました。
dm8.png

無効・有効な送信のテスト

無効・有効な送信のテストを作ります。
リスト 13.55を参考にします。

test/integration/dms_test.rb
  test "DM interface" do
    log_in_as(@user)
    get dms_path
    # 無効な送信
    assert_no_difference 'Dm.count' do
      post dms_path, params: { dm1: {receiver_name: @receiver.name,
                                     content: "" } }
    assert_select 'div#error_explanation'
    end
    # 有効な送信
    content = "Dm test content1"
    assert_difference 'Dm.count', 1 do
      post dms_path, params: { dm1: {receiver_name: @receiver.name,
                                     content: content } }
    end
    assert_redirected_to dms_path
    follow_redirect!
    assert_match content, response.body
  end

自分が受信者のDMを表示

自分が受信者のDMも表示するように変更します。
「13.3.3 フィードの原型」を読みます。

Micropostとfeedの関係のように、DMに対してchatとchat_itemsを作ることにします。

app/models/user.rb
  # 試作 chat
  def chat
    Dm.where("sender_id = ?", id)
  end
app/controllers/dms_controller.rb
  def index
      @user = current_user
      @dms = @user.sent_dms.paginate(page: params[:page])
      @dm = current_user.sent_dms.build 
      @chat_items = @user.chat.paginate(page: params[:page])
  end
app/views/dms/index.html.erb
<% if @user.sent_dms.any? %>
    <h3>DMs (<%= @user.sent_dms.count %>)</h3>
    <ol class= "microposts">
      <%= render @chat_items %>  
      <%#= render @dms %>  
    </ol>
    <%= will_paginate @chat_items %>
    <%#= will_paginate @dms %>
<% end %>  

receiverをブランクにして送信してみたところエラーになりました。

undefined method `any?' for nil:NilClass
<% if @chat_items.any? %>

chat_itemsを作る行をindexと同じようにcreateにも入れます。

app/controllers/dms_controller.rb
  def index
      @user = current_user
      @dm = @user.sent_dms.build 
      @chat_items = @user.chat.paginate(page: params[:page])
  end

  def create
    @user = current_user
    @dm = @user.sent_dms.build 
    @chat_items = @user.chat.paginate(page: params[:page])
    # receiverを探す
    @receiver = User.find_by(name: params[:dm1][:receiver_name])
    if @receiver
      @dm.receiver_id = @receiver.id
      @dm.content = params[:dm1][:content]
      if @dm.save
        flash[:success] = "DM sent!"
        redirect_to dms_path
      else
        render 'index'
      end
    else
      flash.now[:danger] = 'Receiver not found'
      render 'index'
    end

テストしてすべてGreenです。

所要時間

11/15から11/22までの6.5時間です。

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

【Docker環境】ActiveSupport::MessageEncryptor::InvalidMessage の対処法

ポートフォリオをAWSにデプロイした後、ローカルで開発中にタイトルのエラーが出て解決に時間がかかったので、対処法を備忘録として投稿します。

credential.yml.encの再作成

master.keyを使ってcredential.yml.encを開く仕様になっているが、開けないためエラーを吐いている状態。そのため、credential.yml.encを作り直す必要がある。

$ EDITOR=vim bin/rails credetials:edit

このコマンドは暗号化されたファイルをmaster.keyで複合して指定のエディタで編集し、その結果を再び暗号化して保存する。また、master.keyがなければ新しく作り、credentials.yml.encがなければ新しく作ってくれる。そのため、既存のcredential.yml.encを削除し、上記コマンドで解決する。

しかし、筆者のローカル環境はDockerコンテナ上にあるので、少々特殊なコマンドが必要。
まずは、サーバーにログインしvimをインストール。

EC2サーバー上
$ apt-get install -y vim
ローカル環境
$ docker-compose run -e EDITOR=vim web rails credentials:edit

Starting live_share_db_1 ... done
File encrypted and saved.

これで、エラーを吐かなくなりました^^

参考

https://qiita.com/at-946/items/8630ddd411d1e6a651c6
https://qiita.com/zenfumi/items/4a7cbab59f0f7ede0d6e

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

Vue(Nuxt)+Rails APIで、ネストした配列で画像とJSONのパラメーターを送る方法

なにこれ

Vue(Nuxt)で、子テーブルへ画像データとJSONを配列にしてパラメーターで送りたい!って時にかなりつまづいたので、備忘録として書き残しておきます。

この記事で得られること

1.Vue(Nuxt)で画像+JSONの形式でPOSTする方法
2.ネストさせた配列で、画像とJSON形式のパラメーターを送る方法

大事なポイント

1.FormData型の変数を宣言して、その変数にappendしていく。

test.js
      const req = new FormData()
      req.append(`name`, this.product.name)

2.ネストした配列で送りたい時はこうする。配列を明示してその中にオブジェクトを生成&appendする。

test.js
          req.append(`product_sub_attributes[][image]`, subs[i].image)

3.axiosでリクエストを投げる時、'Content-Type': 'multipart/form-data'にすること。

test.js
        const response = await axios.post('/products', req, {
          headers: {
            'Content-Type': 'multipart/form-data'
          }
        })

備考

  • バックエンドはRails APIモードを仕様しています
  • 画像アップ方法は、フロントはvue-cropeer、バックエンドはcarrierwaveを使用していますが、まだ理解しきれてない箇所があるのでそちらも後日記事にします。 なので、vue-cropperとcarrierwaveの説明は割愛します。今はJSON配列で送れるんだなーと知っていただければ幸いです。
  • バックエンドはみんな大好きgem 'active_model_serializers’を使用しています。
  • 一部vuetify、bootstrap、axiosを使っています。

注意事項

今回はタイトルの方法を紹介するのがメインになるので、かなり端折っています。
全部解説すると膨大な量になってしまうので。
雰囲気を掴んでいただければ、と思っているのでご了承ください。
既存のコードをQiita用に抽出して載せているので、一部間違いがあるかも知れません。

前提条件

テーブル構造はこんな感じです。
productsテーブルproducts_subsテーブルがあって、1:Nの関係です。
両方とも画像投稿用のimageカラムを持っています。
登場する全てのカラムはstring型です。

product.rb
class Product < ApplicationRecord
  has_many :products_subs
  accepts_nested_attributes_for :products_subs
end
product_sub.rb
class ProductSub < ApplicationRecord
  mount_uploader :image, ProductSubUploader

  belongs_to :product

  validates :name, presence: true
  validates :image, presence: true
end

やりたいこと

productsテーブルへcreateしたい時に、子テーブルのproducts_subsへ同時にデータを保存させたい。
そのためにはネストして送る必要がある。

早速解説します。

画像アップのおおまかな流れ

プレビュー表示用のimgタグと画像アップ用のinputタグを用意して、
imgタグをクリックすると$refsでinputを参照してクリックしたことにします。

配列なので$refs.subImageに[i]をつけて、番号を参照しています。
inputタグはクリックされるとsetSubImageメソッドを呼び出します。

setSubImageで画像を読み込んだ後にvue-cropperに投げて、
らcropSubImageメソッドでsubImageCropperを参照してプレビューを表示する変数に突っ込んだり、画像格納用のオブジェクトに代入したりしてます。
余談だけど、このvue-cropperさんの動きが理解できてなくて、たまに$refsで参照できないバグが生まれます。笑

test.vue
<template v-for="(sub, i) in product.productsSubAttributes">

  <div>
    <v-textarea
      v-model="sub.name"
    >
    </v-textarea>
    <img
      :src="sub.imageSrc ? sub.ImageSrc : '' "
      @click.prevent="$refs.subImage[i].click()"
    >
    <input
      ref="subImage"
      class="d-none"
      type="file"
      name="image"
      accept="image/*"
      @change="setSubImage($event)"
    />
    <v-card>
      <vue-cropper
        ref=“subImageCropper"
        :src="imgSrc"
      />
      <button @click="cropSubImage(sub), i)">
        保存
      <button>
    </v-card>
  </div>
</template>

<script>
export default {
  methods: {
    setSubImage(e) {
      const reader = new FileReader()
      reader.onload = (e) => {
        this.imgSrc = e.target.result
      }
      reader.readAsDataURL(e.target.files[0])
    },
    async cropSubImage(sub, i) {
      // imageSrcがプレビュー表示用のプロパティです。
      sub.imageSrc = this.$refs.subImageCropper[0].getCroppedCanvas().toDataURL()
        this.$refs.subImageCropper[0].getCroppedCanvas().toBlob((blob) => {
          sub.image = blob
        })
      }
    }
  }
}
</script>

本題の画像とJSONオブジェクトをパラメーターで送る方法

今回はaxiosを使います。
その際に、configを設定してmultipart/form-data
という形式に変換します。

【axios】HTTPリクエストメソッド別の引数一覧表(エイリアスを使用した場合)
この記事がよくまとまってたのでリンク貼っておきます。

axios公式ドキュメント

multipart/form-dataとは、画像ファイル(Blob型)を送信できるようにするHTTPリクエストメソッドです。
application/jsonだと画像データが送れないです。

パラメーターで送るための方法

細かく解説するためにコメント式にしました。

test.js
    async onClickCreate() {
      // FormData型の変数reqを定義して、そちらにappendしていきます。
      const req = new FormData()
      // 今回は使用しませんが、こんな感じでバックエンドのカラム名に合わせて
      // オブジェクトを生成してappendすることで、パラメーターを送れます。
      req.append('name', this.product.name)
      // 定数subにアップした画像や文字列のデータを代入して、for文で回します。
      // for文を使っている理由は複数ありますが、今回は省略。状況によってはforEachでも代用可能です。
      const subs = this.product.productSubAttributes
      for(let i = 0; i < subs.length; i++){
        // 送りたいパラメーターに[]をつけることで、0から順番に配列で送ることが出来ます。
        // ただ、配列にインデックスを指定する方法が分からないです。
        req.append(`product_sub_attributes[][name]`, subs[i].name)
        // subs[i].imageがBlob型である=先程vue-cropeerで整形したデータなので、直接カラムに代入します。
        if (subs[i].image instanceof Blob) {
          req.append(`product_sub_attributes[][image]`, subs[i].image)
        // 画像がない場合、番号を指定できないので、空文字を代入しないと順番が狂います。
        // どういうことかと言うと、配列内のオブジェクトが2個以上あった場合、0番目から探して空いているカラムに代入されます。
        // ちなみにここでかなりハマりました。
        } else {
          req.append(`product_sub_attributes[][image]`, '')
        }
      }
      try {
        // 先程代入していった変数reqをパラメーターで送ります。
        // 余談ですが、Railsの場合全てキー名をスネークケースにする必要があります。
        const response = await axios.post('/products', req, {
          headers: {
            'Content-Type': 'multipart/form-data'
          }
        })
      } catch (error) {
        console.error(error.response)
      }
    }

ちなみにバックエンドはこんな感じ

products_controller.rb
 def product_params
    params.permit(
      :id,
      :name,
      {
        product_sub_attributes: [
          :id,
          :name,
          :image,
        ]
      }
    )
  end

おわり

こんな感じでVue(Nuxt)からパラメーターで画像を配列+JSONで送ることが出来ます。

自分が実装しようとした時にこの方法にたどり着くまで苦労したので、

個人的な感想

やり方(How)を覚えるのは良いことなんですけど、
裏側のなぜこの方法なのか(How)の方を理解していきたいです。

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

プルダウンを使おうとするとエラーになる件

ActiveRecord::StatementInvalid

image.png

他のf.text_areaは使えるのに、、テーブルがないよと言ってますね。
いちよマイグレの確認upにされてるし、カラムも間違えてないのに。。。
datebase.ymlいじっても変化なし。

ActiveHash::Base

ActiveHash::Baseは、あるモデル内(クラス内)でActiveHashを用いる際に必要となるクラスです。ActiveHashのGemに定義されています。
また、ActiveHash::Baseを継承することで、ActiveRecordと同じようなメソッドを使用できる

model/category.rb
class Category < ApplicationRecord

  include ActiveHash::Associations
  has_many :events
end

継承をすっかり忘れていました。⬇️

model/category.rb
class Category < ActiveHash::Base

  include ActiveHash::Associations
  has_many :events
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ルーティングにおける、getとpostの使い分け (Ruby on Rails)

(学習備忘録)

Ruby on Railsのルーティングにおける、getとpostの使い分け

「get」
データベースを変更しないアクション

「post」
データベースを変更するアクション

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

スクール4週目 学習環境の大切さ

スクール4週目では、問題をときながら一つずつ実装していくChatAppを完了することができました。

学習環境の大切さ :satellite:

プログラミング学習時に皆さんは音楽を聴きますか?集中力を高める為や、気分を上げる為に聴く方は多いと思います。私は学習中、常にイヤホンで自然音を流しています。

自分はこれまで自然音を流しながら学習する習慣はなく無音で学習しては周りの雑音が気になり、集中が途切れることが多々ありました。ところが、自然音を流しながら学習をするとプログラミングに没頭できることを実感しています。

これは、一定のテンポで自然音を流すことでリラックス効果あるからだと思います。また、ポモドーロ・テクニックを活用して25分勉強して5分休憩するを繰り返すことで、集中力を維持できます。ぜひ、一度は実践をしてみてください。

応用カリキュラム :bulb:

・ユーザー管理機能実装
・グループ作成編集機能実装
・メッセージ送信機能実装
・テスト
・非同期通信機能実装
・自動更新機能実装

ユーザー管理機能実装 :man_tone1:
gemのdeviseを使用してのユーザー管理を実装

①deviseのインストール
②Userモデルを作成する
③deviseのビューファイルの追加
④サインアップ機能の追加
⑤ユーザー情報編集機能の追加
⑥ログアウト機能の追加

グループ作成編集機能実装 :couple:
グループの新規登録画面で、グループ名と所属メンバーを入力して登録できる機能を実装。
①groupsコントローラーを作成(ルーティングの設定も合わせて実施)
②アイコンをクリックするとグループの新規登録画面へ移動するようリンクを設定
③groupモデルとgroup_userモデル(多対多のリレーションを組む為の中間モデル)を作成
④多対多のアソシエーションとバリデーションを設定
⑤binding.pryにより送信されるデータを確認
⑥配列の保存を許可するためのストロングパラメータの設定

そして、グループの新規登録画面の実装とほぼ同じ手順でグループ編集機能を実装。
また、ビューを部分テンプレートを使用し整えます。

メッセージ送信機能実装 :calling:
ここらへんまでくるといよいよ完成形が見えてきて画像を投稿できた時は少し感動しました。この手順の中では⑧、⑨、⑩は全くのノーヒントで、力が試されます。基礎カリキュラムを見返したのと、検索でここは乗り切りました。
①Messageモデルを作成する
②ルーティングを設定する
③該当するアクションをコントローラに定義する
④画像送信の為のgem、Carrierwaveを導入
⑤メッセージ送信機能を実装する
⑥グループにメッセージを表示する
⑦サイドバーに最新のメッセージを表示する
⑧ヘッダーを修正しグループ名、メンバーが表示されるようにする
⑨グループ編集ページへのリンクを設置する
⑩グループ編集後のリダイレクト先を変更する

テスト :skull_crossbones:
Webアプリケーション作成のおいて重要なテストの概念、必要性、原則、書き方をRSpecを利用し学びました。まずは基礎的な書き方を学習、その後はfactory_botというgemを利用。そしてダミーのデータを作成するためのgem、Fakerの利用方法を学びました。
ひと通り学んだ後はChat-spaceで実施。factory_botでスッキリまとめられるかが課題です。
①単体テスト‥ひとつのプログラムのまとまりに関して、それ単体が正常に動くか確かめるテスト
②結合テスト‥複数のプログラムが連動して行われる処理が意図した通りに行われるかを確かめるテスト

非同期通信機能実装 :gear:
非同期通信機能とは、リクエスト後にレスポンスが帰ってきた際、ブラウザが再読み込みされること無く通信が行われる通信方法。Ajaxと呼ばれます。また、ここではJSONというデータ交換を行う為の記述形式を使用。
学習の流れとしては基礎カリキュラムで作成したアプリであるPictweetのコメント機能の非同期通信化を行いながら学び、その後Chat-spaceへ実装します。
Chat-space実装時にビューが崩れたのでclass名を確認し修正。ここらへんからうまくいかない時は検証ツールが大活躍です。
①APIを作成
②jQueryが使えるように設定し、jsファイルを作成する
③フォームが送信されたら、イベントが発火するようにする
④イベントが発火したときにAjaxを使用して、messages#createが動くようにする
⑤messages#createでメッセージを保存し、respond_toを使用してHTMLとJSONの場合で処理を分ける
⑥jbuilderを使用し、作成したメッセージをJSON形式で返す
⑦返ってきたJSONをdoneメソッドで受取り、HTMLを作成、それをメッセージ画面の一番下に追加する
⑧メッセージを送信したとき、メッセージ画面を最下部にスクロールするようにする
⑨連続で送信ボタンを押せるようにする
⑩非同期に失敗した場合の処理も準備する(エラー表示)

自動更新機能実装 :flashlight:
現行では自分でメッセージを投稿した場合は非同期通信機能により画面に表示されますが、別のユーザーの投稿メッセージはリロードしなければ表示されません。そこで、この自動更新機能を実装し自動で画面が更新されるようにします。
①表示されているメッセージのidが確認出来るようにhamlにカスタムデータ属性として追加
②新規メッセージ確認用のアクションをWebAPIにて実装
③投稿内容をレスポンスできるようにする
④取得した投稿データを表示できるようにする
⑤7秒毎にリクエストするよう実装
⑥メッセージを取得したら画面がスクロールするようにする
⑦自動更新が必要ない画面では行わないようにする

そして無事LGTMを貰いとりあえず実装は完了。まだ、理解しきれていないところがあるので、余力と時間があればPicTweetとChatAppの2周目をしたいなと思います。

振り返り・感想 :triangular_flag_on_post:

PicTweetとChatAppを作成して大事だと思ったことは、とりあえず何か作ってみることで覚えていくという点です。ひたすらコードを暗記していくよりもまずは一つ簡単でも良いのでアプリ作ることから始めた方が良いと思いました。

ポートフォリオもまさにそうで、とりあえず作って過程で追加実装をしていく考え方、手を動かすことの大事さを感じました。それでも、ユーザ目線で要件定義することも念頭に入れてポートフォリオのアイデアも言語化して行きたいと思います。

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

【Rails】2つの(複数)deviseを導入

はじめに

「ユーザーと業者」、「管理者とユーザー」、「先生と生徒」など、登録情報が2つ以上(複数)必要な場合、deviseも同様に2つ以上(複数)必要です。
2つの(複数)deviseの導入方法を紹介します。

目次

  1. deviseのインストール
  2. モデルを作成
  3. ルーティングの設定
  4. コントローラーの作成

開発環境

ruby 2.6.5
rails 6.0.0
devise 4.7.3

実装

それでは実装していきます〜

1. deviseのインストール

deviseのgemを導入します。

Gemfile.
# 中略
gem 'devise'

記述したら、bundle installを実行しましょう。

ターミナル.
bundle install

Gemをインストールした後はrails sをcontrol + Cで一度停止し、サーバーを再起動する必要があります。

続いて、アプリケーション内でdeviseを使えるようにするため、下記のコマンドを実行します。

ターミナル.
rails g devise:install
ターミナル.
create  config/initializers/devise.rb
create  config/locales/devise.en.yml
.........#省略

deviseがインストールされました。

次に以下の二行をコメントインして変更します。

config/initializers/devise.rb
  # ==> Scopes configuration
  # Turn scoped views on. Before rendering "sessions/new", it will first check for
  # "users/sessions/new". It's turned off by default because it's slower if you
  # are using only default views.
  config.scoped_views = true  # ←複数のmodelで個別のログイン画面を使う。 これをtrueにします。


  # Configure the default scope given to Warden. By default it's the first
  # devise role declared in your routes (usually :user).
  # config.default_scope = :user

  # Set this configuration to false if you want /users/sign_out to sign out
  # only the current scope. By default, Devise signs out all scopes.
  config.sign_out_all_scopes = false  #複数のモデルを扱う際、いずれかがログアウトした時に全てログアウトする。 これをfalseにします。

2. モデルを作成

deviseが2つなので当然2つのモデルを作成します。

rails g devise user
rails g devise admin

管理者アカウントになる admin に対して勝手に sign_up されたくないので Admin モデルを修正します。

:registerable不要部分をコメントアウトします。

app/models/admin.rb
class Admin < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, #:registerable, ←ここをコメントアウトします。
         :recoverable, :rememberable, :validatable
end

3. ルーティングの設定

deviseを導入するとデフォルトでルーティングを設定してくれてますが、rails routesした時かぶって見にくいので再設定します。
スクリーンショット 2020-11-17 14.02.33.png
deviseがずらっと並んでみにくい、、、、
下記に編集します。

routes.rb
Rails.application.routes.draw do
  devise_for :admins
  devise_for :users
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
#ここまでが自動生成されてます。

#下記に編集します。
  devise_for :admins, controllers: {
    sessions:      'admins/sessions',
    passwords:     'admins/passwords',
    registrations: 'admins/registrations'
  }
  devise_for :users, controllers: {
    sessions:      'users/sessions',
    passwords:     'users/passwords',
    registrations: 'users/registrations'
  }
  root to: 'home#index'
end

adminsとsuersに別れて見やすくなります。

スクリーンショット 2020-11-17 14.06.49.png

4. コントローラーの作成

コントローラーも2つ作成します。

ターミナル.
rails g devise:controllers users
rails g devise:views admins
ターミナル.
rails g devise:views users
rails g devise:controllers admins

localhost:3000/users/sign_inlocalhost:3000/admins/sign_inのそれぞれページが表示されることを確認できたら完成です!!

まとめ

2deviseの導入方法でした。

最後に

私はプログラミング初学者ですが、自分と同じ様にエンジニアを目指す方々の助けになればと思い、記事を投稿しております。
それではまた次回お会いしましょう〜

参考

https://qiita.com/Yama-to/items/54ab4ce08e126ef7dade
https://ccbaxy.xyz/blog/2020/03/20/ruby33/

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

コントローラーのテスト時に画像をアップロードする方法

RSpecでコントローラーの単体テストを行っていた時にエラーが発生したため、解決方法を記録として残します。

開発環境

  • rails (6.0.3.3)
  • rspec-rails (4.0.1)

コード内容

店舗情報の登録に関するテストを行いました。

laundries.spec.rb
context "管理者の場合" do
   before do
     @user = FactoryBot.create(:user, admin: true)
   end

   it "店舗情報を追加できること" do
     laundry_params = FactoryBot.attributes_for(:laundry)
      sign_in @user
      expect {
       post laundries_path, params: { laundry: laundry_params }
      }.to change { Laundry.count }.by(1)
   end

内容としては、
1.店舗情報を投稿するためにユーザーを生成する。
2.FactoryBot.attributes_forでテスト用の属性値を生成してlaundry_paramsに代入。
3.ログインした後に情報を送り、Laundryモデルのカウントが1上がることを確認する。

テストを実行したところ、以下のようなエラーが表示されました。

expected `Laundry.count` to have changed by 1, but was changed by 0

どうやら、データが正しく登録されていないみたいです...。

原因

画像がパラメーターに含まれていなかった。

とりあえずログを確認。

test.log
Processing by LaundriesController#create as HTML
  Parameters: {"laundry"=>{"name"=>"コインランドリー名古屋店", "address"=>"愛知県名古屋市1-1", "opening_date"=>"2012-01-13", "open_time"=>"7:00", "close_time"=>"23:00", "shoe_washing"=>"true", "futon_washing"=>"true", "dryer"=>"true", "washing_machine"=>"true"}}
  Rendering laundries/new.html.erb within layouts/application

パラメーターに値は含まれているがnew.html.erbがレンダリングされている...
登録に失敗した場合new.html.erbをレンダリングするようにしてあるため、エラーが生じていることはわかるが何が原因なのかはわからず。
「もしかしてパラメーターのキー名が間違っていてコントローラーで正しく受け取れていないのでは?」と思いストロングパラメーターを確認したところ...

contoroller.rb
  def laundry_params
    params.require(:laundry).permit(:name, :address, :opening_date, :open_time, :close_time, :shoe_washing, :futon_washing, :dryer, :washing_machine, :image)
  end

:image(permitメソッドの最後)・・・これだ!!!これが抜けているから画像がないぞ!とエラーになっていることが判明。

原因が判明したのはいいが、FactoryBotではデータを生成した後に画像をアタッチするようにしていたためどのようにしてパラメーターに含めればいいのかわからず苦戦しました。

FabtoryBot
FactoryBot.define do
  factory :laundry do
    name { 'コインランドリー名古屋店' }
    address { '愛知県名古屋市1-1' }
    opening_date { '2012-01-13' }
    open_time { '7:00' }
    close_time { '23:00' }
    shoe_washing { true }
    futon_washing { true }
    dryer { true }
    washing_machine { true }

    after(:build) do |item|
      item.image.attach(io: File.open('public/images/test_image.png'), filename: 'test_image.png')
    end
  end
end

解決方法

fixture_file_uploadメソッドを使う

fixture_file_uploadメソッドはRSpecに用意されているメソッドです。
以下の記事を参考にさせていただきました。
Active Storage 導入環境下での単体テスト

以下が、コードのbefore/afterです。
エラー発生時のテストコード

it "店舗情報を追加できること" do
  laundry_params = FactoryBot.attributes_for(:laundry)
   sign_in @user
   expect {
    post laundries_path, params: { laundry: laundry_params }
   }.to change { Laundry.count }.by(1)
end

エラー解決時のテストコード

it "店舗情報を追加できること" do
 #laundry_paramsにimageを追加
  laundry_params = FactoryBot.attributes_for(:laundry, image: fixture_file_upload("/files/test_image.png"))
  sign_in @user
  expect {
   post laundries_path, params: { laundry: laundry_params }
  }.to change { Laundry.count }.by(1)
end

無事にテストが実行されログにもimageが含まれていました!

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