20190929のRailsに関する記事は17件です。

Rails 5.2 + Docker, RAILS_MASTER_KEYをイメージ作成時に動的に入れる方法

ブログから移動したものです。

下記のような形でDockerfileを作成する。

FROM ruby:2.6.0-alpine3.8

ENV RAILS_ENV=production
ENV APP_ROOT /usr/src/app
ARG RAILS_MASTER_KEY
ENV RAILS_MASTER_KEY ${RAILS_MASTER_KEY}

WORKDIR $APP_ROOT

RUN apk add --no-cache alpine-sdk \
    nodejs-current \
    nodejs-npm \
    yarn \
    mysql-client \
    mysql-dev \
    python2 \
    tzdata

COPY Gemfile $APP_ROOT
COPY Gemfile.lock $APP_ROOT

RUN bundle install --jobs=4

COPY . $APP_ROOT

RUN bin/yarn install
RUN bin/rails webpacker:compile

VOLUME $APP_ROOT/public
VOLUME $APP_ROOT/tmp

下記の部分がポイントとなっている。

ARG RAILS_MASTER_KEY
ENV RAILS_MASTER_KEY ${RAILS_MASTER_KEY}

下記のような形でCI上で環境変数をARGとして渡してやれば(CI側の環境変数にRAILS_MASTER_KEYが設定されている前提)、上記のARGにRAILS_MASTER_KEYが設定され、ENVのRAILS_MASTER_KEYにARGのRAILS_MASTER_KEYを設定することができる。これにより、イメージ作成時にRAILS_MASTER_KEYを設定した状態でビルドすることができる。

docker build --build-arg RAILS_MASTER_KEY=${RAILS_MASTER_KEY} -t app . -f Dockerfile.production

参考:
* https://qiita.com/NaokiIshimura/items/2a179f2ab910992c4d39
* https://qiita.com/nacika_ins/items/cf8ceb20711bd077f770

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

【Rails】kaminariで作ったページネーションの1ページあたりの表示件数を変更する方法

kaminariで作ったページネーションは、デフォルトだと25件まで表示する設定になっています。

1ページあたりの表示件数を変更するには、config/initializers/kaminari_config.rbを編集すればOK。

今回は1ページあたり10件の表示に変更することにしましょう。

config/initializers/kaminari_config.rbを開くと、コメントアウトされている項目がたくさんあると思います。

image.png

表示件数を変える場合は、1番上の# config.default_per_page = 25の#を取って、以下のように変更してください。

config/initializers/kaminari_config.rb
config.default_per_page = 10

変更したら、rails sでサーバーを再起動してください。

表示件数が10件になっているはずです。

ちなみに、他の項目は以下のような意味です。

【default_per_page】

  • 1ページあたりの表示件数

【max_per_page】

  • 1ページあたりの表示件数
  • デフォルトはnil = 無制限

【window】

  • 左右何ページ分のリンクを表示するかの設定
  • 3に設定して現在10ページ目にいる場合、7・8・9ページと11・12・13ページが表示される

【outer_window】

  • 最初のページ・最終ページからそれぞれ何ページ分のリンクを表示するかの設定
  • 3と設定して全20ページの場合、1・2・3ページおよび18・19・20ページが表示される

【left】

  • 先頭ページから何ページ分のリンクを表示するかの設定

【right】

  • 最終ページから何ページ分のリンクを表示するかの設定

【page_method_name】

  • ページ番号を指定するスコープの名前

【param_name】

  • ページ番号を渡すのに使うリクエストパラメータの名前

以上です。

以上です。

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

【Rails】kaminariのページネーションを中央寄せする方法

application.scssに、以下のように記述しましょう。

これでページネーションが中央寄せになります。

application.scss
.pagination {
  justify-content: center;
}

kaminariでは、ul要素に「.pagination」というclassが自動で付与されます。

そのため、.paginationクラスのcssを変更すればOKです。

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

【Rails】kaminariのページネーションを日本語化する方法

kaminariを日本語化する方法

kaminariを日本語化する方法はたったの2ステップです。

ステップ1:ステップ1:config/application.rbを編集

config/application.rbを開いて、以下のコードを書いてください。

config/application.rb
config.i18n.default_locale = :ja

image.png

ステップ2:config/lacales/ja.ymlを作成

次にconfig/lacales/ja.ymlといファイルを作りましょう。

ファイルを作ったら、以下のように記述します。

config/lacales/ja.yml
ja:
  views:
      pagination:
        first: '最初'
        last: '最後'
        previous: '前'
        next: '次'
        truncate: '...'

記述したら、rails sでサーバーを再起動してください。

以下のように、日本語になっていたら成功です。

image.png

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

【Rails】kaminariを使ってページネーションを実装する方法

ページネーションを作るのに、kaminariというgemを使ったのですが久しぶりで忘れてしまっていました。

そこで備忘録として

  • kaminariの導入方法
  • kaminariのカスタマイズ方法

についてまとめてみました。

よかったら参考にしてみてください。

【Rails】kaminariを使ってページネーションを実装する方法

kaminariによるページネーションの実装は、大きく分けて6つのステップでできます。

ステップ1:Gemgileに記述

Gemfileに以下のように記述してください。

gem 'kaminari'

image.png

ステップ2:bundle installする

Gemfileにkaminariを記述したら、ターミナルを開いてbundle installしてください。

image.png

ステップ3:kaminariの設定ファイルを生成する

次にkanimariの設定ファイルを作ります。

以下のコマンドをターミナルに打ち込んでください。

rails g kaminari:config

以下のように表示されたら成功です。

image.png

ステップ4:kaminariのviewファイルの生成

次にviewファイルを作ります。
今回はbootstrap4で見た目を整えるので、以下のようなコマンドを打ちました。

rails g kaminari:views bootstrap4

以下のように表示されたら成功です。

image.png

ちなみにBootstrap4を使わない場合は、以下のように打ち込んでください。

rails g kaminari:views default

ステップ5:controllerにpageメソッドを追加

ページネーションを実装したいアクションを持つcontrollerにpageメソッドを追加しましょう。

.page(params[:page])

例えば、indexアクションに表示される記事一覧(@articles)にページネーションを使いたい場合は、こんな感じで書けばOK。

image.png

pageメソッドを呼び出すことにより、引数に指定したページに表示するデータだけを取得できます。

デフォルトの設定では、1ページに25件のデータが取得されます。

ステップ6:viewファイルの修正

次にviewファイルを修正していきましょう。

ページネーションを表示したい箇所に以下のコードを書いてください。

<%= paginate @articles %>

以下のように表示されたら成功です。

image.png

まとめ

kaminari便利!

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

【初心者向け】コメント機能作成追加

前提

・記事投稿一覧ページがすでに作成されている状態であること
・記事投稿一覧ページにコメント機能を追加する
・コメントはUserと紐づいてはいない仕様にしています
(説明が少なくて恐縮ですがお許しください)

コメント機能作成

コメントモデルを作成する

作成するカラムは
user_id
content
topic_id
を用意します。

$ rails g model comment user_id:integer content:text topic_id:integer
rails db:migrate

アソシエーションの追加(モデルの関連付け)

コメントと投稿の関係は以下のようになります。
・Commentは1つのTopicを持っている
・Commentは1つのUserを持っている
・Topicは複数のCommentを持っている
という関連を持っています。

ですので、Commentモデルに「belongs_to」、Topicモデルに「has_many」を以下のように追加します。

app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :topic
  belongs_to :user

  validates :content, presence: true
end
models/topic.rb
  has_many :comments, dependent: :destroy
  has_many :comment_users, through: :comments, source: 'user'

コントローラを作成しルーティングを設定する

$rails g controller comments
config/routes.rb
  get 'comment/new'
  #中略

resources :topics do
  resources :comments
  #/topics/:topic_id/comment/newのパスが使用できる
end
controllers/comments_controller.rb
class CommentsController < ApplicationController
  def new
    @comment = Comment.new
    @topic_id = params[:topic_id]
  end

  def create #コメントを登録する
    @comment = Comment.new #コメントのインスタンスを作成
    @comment.topic_id = params[:topic_id] #記事番号をパラメータから受け取る
    @comment.content = params[:content] #コメントの内容をパラメータから受け取る

    if @comment.save #コメント登録の条件分岐
      redirect_to topics_path, success: 'コメントに成功しました' 
    else
      flash.now[:danger] = "コメントに失敗しました"
      render :new
    end
  end
end

リンクの追加

今回は投稿一覧ページの投稿からコメントページへと遷移できるようにします。コメントアイコンを用意してリンクを貼ります。

topics/index.html.erb
<%= link_to new_topic_comment_path(topic_id: topic.id), method: :get do %>
 <%= image_tag 'icons/comment.png', class: 'topic-index-icon' %>
<% end %>

コメント投稿のページ作成

views/comments/new.html.erb
<div class="topic-new-wrapper" >
  <div class="container">
    <div class="row">
      <div class="col-md-6 col-md-offset-3">
        <h1 class="text-center">Add Comment</h1>
        <%= form_with url: '/topics/' + @topic_id.to_s + '/comments' ,local: true do |f| %>
          <div class="form-group">
            <%= f.label :content %>
            <%= f.text_area :content, class: 'form-control' %>
          </div>

          <% if logged_in? %>
            <%= f.submit 'コメント送信', class: 'btn btn-black btn-block' %>
          <% end %>
        <% end %>
      </div>
    </div>
  </div>
</div>

作成結果

コメント追加ページ
スクリーンショット 2019-09-29 23.31.29.png

記事投稿一覧ページ
comment_function.png

拙い記事で恐縮ですが、何かの参考になればと思います。
間違いや認識違い等ございましたらご指摘くださればと思います。

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

Ruby on Rails ツイートにユーザー情報を追加する。Active Recordによるアソシエーションとn + 1問題の解消

ツイートにユーザー情報を追加する

作業内容

  • tweetsテーブルにカラムを追加する
  • ツイート保存時にユーザー情報を追加する処理を記述する
  • tweetsテーブルにカラムを追加
  • tweetsテーブルにuser_idカラムをinteger型で追加する。
ターミナル
$ rails g migration AddUserIdToTweets user_id:integer
# tweetsテーブルにuser_idカラムをinteger型で追加するマイグレーションファイルの作成

$ rake db:migrate
# マイグレーションファイルの実行

1. ツイート保存時にユーザー情報を追加する処理をする

current_user
deviseでログイン機能を実装すると、current_userというヘルパーメソッドを使用することができる。これは、現在ログイン中のユーザーのレコードを、userクラスのインスタンスとして取得することができるメソッドである。
例を上げるとusersテーブルでのidカラムの値が1のアカウントでログインしている場合、current_userUser.find(1)と同じ意味を持つ。
この時、以下の例のようにcurrent_user.usersテーブルのカラム名とすることで、ログイン中のユーザーの情報として登録されている各カラムの値を取得することができる。

【例】

# current_userを利用して、ログインしているユーザーのデータを取得する
current_user.name
=> "nakamura"

current_user.email
=> "hoge@sample.com"
  • tweetsテーブルのuser_idカラムに保存すべきなのはcurrent_userのidカラムの値になる。
  • ツイートを保存する際に、image,textというビューから送られてくる情報に加えて、user_idカラムにログイン中のユーザーのidを保存しなければならない。
tweets_controller.rb
class TweetsController < ApplicationController

  before_action :move_to_index, except: :index

  def index
    @tweets = Tweet.page(params[:page]).per(5).order("created_at DESC")
  end

  def new
  end

  def create
    # tweet_paramsというストロングパラメーターを使用
    Tweet.create(image: tweet_params[:image], text: tweet_params[:text], user_id: current_user.id)

  private
  def tweet_params
    params.permit(:image, :text)
  end

  def move_to_index
    redirect_to action: :index unless user_signed_in?
  end
end
  • createアクションのuser_id: current_user.idは現在ログイン中のユーザーのidを保存するための記述になる。

2. マイページの作成

  • マイページのルーティングを記述
  • コントローラとアクションを作成
  • マイページ用のビューファイルを作成

routes.rbの編集

routes.rb
Rails.application.routes.draw do
  devise_for :users
  root 'tweets#index"
  get  'tweets'      => 'tweets#index'
  get  'tweets/new'  => 'tweets#new'
  post 'tweets'      => 'tweets#create'
  get  'users/:id'   => 'users#show'
end

コントローラーとアクションを作成

● whereメソッド
whereメソッドはActiveRecordメソッドのうちの一つであり、モデル.where(条件)のように引数部分に条件を指定することで、テーブル内の条件に一致したレコードのインスタンスを配列型で取得できる。
また、whereメソッドを連続して記述することで複数の条件に一致したレコードも取得できる。
【例】

コンソール
[1] pry(main)> Tweet.where('id < 3')
=> [#<Tweet id: 1, image: "test1.jpg", text: "ナイス!", created_at: "2019-09-27 00:00:00", updated_at: "2019-09-27 00:00:00", user_id: 1>,#<Tweet id: 2, image: "test2.jpg", text: "Thank you!", created_at: "2014-12-07 00:00:00", updated_at: "2014-12-07 00:00:00", user_id: 2>]
# idが3未満のtweetsテーブルのインスタンスを配列で取得

[2] pry(main)> Tweet.where('id < 3').where(user_id: 1)
=> [#<Tweet id: 1, image: "test1.jpg", text: "いい景色だ。", created_at: "2014-12-06 00:00:00", updated_at: "2014-12-06 00:00:00", user_id: 1>]
# idが3未満かつuser_idが1のtweetsテーブルのインスタンスを配列で取得

users_controllerの作成

ターミナル
$ rails g controller users

usersコントローラーのshowアクションにマイページに表示したい情報を定義

users_controller.rb
class UsersController < ApplicationController
  def show
    # 現在ログインしているユーザーのニックネーム
    @nickname = current_user.nickname
    # 現在ログインしているユーザーが投稿したツイート
    @tweets = Tweet.where(user_id: current_user.id).page(params[:page]).per(5).order("created_at DESC")
  end
end

マイページ用のビューファイルを作成

/users/show.html.erb
   #省略
   <header class="header">
    <div class="header__bar row">
      <h1 class="grid-6"><a href="/">Tweet</a></h1>
      <% if user_signed_in? %>
        <div class="user_nav grid-6">
          <span><%= current_user.nickname %>
            <ul class="user__info">
              <li>
                <a href="/users/<%= current_user.id %>">マイページ</a>
                <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
              </li>
            </ul>
          </span>
          <a class="post" href="/tweets/new">投稿する</a>
        </div>
      <% else %>
        <div class="grid-6">
          <%= link_to "ログイン", new_user_session_path, :class => 'post' %>
          <%= link_to "新規登録", new_user_registration_path, :class => 'post' %>
        </div>
      <% end %>
    </div>
  </header>
   #省略

次回からはaタグの記述はヘルパーを使うようにする。

アソシエーションの利用

  • アソシエーションの定義
  • アソシエーションの実装

アソシエーションを定義する

  • モデルクラスにはhas_manybelogns_toなどの定義がされている。
  • 所属する側のテーブルに所属するクラス名_idというカラムがある。(user_idというカラムをtweetsテーブルに追加してあるため)

userの作成したtweetが複数個(has_many :tweets)

/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
           :recoverable, :rememberable, :validatable
  has_many :tweets
end

tweetはいずれかのuserに属する(belongs_to :user)

/models/tweet.rb
class Tweet < ApplicationRecord
  belongs_to :user
end

以上でアソシエーションの定義をしたことになる。

アソシエーションを実装

【例】 アソシエーションをしない場合

$ rails c
[1] pry(main)> user = User.find(1)
[2] pry(main)> Tweet.where(user_id: user.id)
=> [#<Tweet id: 1, image: "http://photo1.jpg", text: "ナイス!", created_at: "2014-12-06 09:00:00", updated_at: "2014-12-06 09:00:00", user_id: 1>,
  #<Tweet id: 1, image: "http://photo2.jpg", text: "beautiful!", created_at: "2014-12-06 10:00:00", updated_at: "2014-12-06 10:00:00", user_id: 1>]

【例】 アソシエーションを使用

$ rails c
[1] pry(main)> user = User.find(1)
[2] pry(main)> user.tweets
=> [#<Tweet id: 1, image: "http://photo1.jpg", text: "ナイス!", created_at: "2014-12-06 09:00:00", updated_at: "2014-12-06 09:00:00", user_id: 1>,
  #<Tweet id: 1, image: "http://photo2.jpg", text: "beautiful!", created_at: "2014-12-06 10:00:00", updated_at: "2014-12-06 10:00:00", user_id: 1>]

アソシエーションを利用した@tweetsを定義する

users_controller
class UsersController < ApplicationController
  def show
    @nickname = current_user.nickname
    @tweets = current_user.tweets.page(params[:page]).per(5).order("created_at DESC")
  end
end

上記のようにcurrent_user.tweetsと続ければ、現在ログインしているユーザーの投稿したツイート全てを取得できる。

ツイートからユーザー情報を先読み

● n+1問題

  • n+1問題とは、データを呼び出す際に大量のSQLが発行されてしまう問題のことである。例にあげると tweetsのindexアクションで全ツイートを取得する一回に加えて、アソシエーションを利用してツイートの数だけユーザー情報をその度に呼び出してしまう。 よって、その状態だとツイート数+1回SQLが発行されてしまう。この状態のことをn+1問題と言う。

● includesメソッド

  • n+1問題includesメソッドを使うことによって解消することができる。指定された関連モデルをまとめて取得することで、SQLの発行回数を減らすことができる。
  • 記述の仕方はinludes(:モデル名)のように引数で、関連モデルをシンボル型で指定する。

includesメソッドを追記

tweets_controller.rb
class TweetsController < ApplicationController

  def index
    @tweets = Tweet.includes(:user).page(params[:page]).per(5).order("created_at DESC")
  end
  #以下省略
end

まとめ

久しぶりに上記関係の復習をやってみましたが以前はなんのこっちゃ?みたいな部分があったのですがゆっくり読み返したことによって理解できたのではないかと思っています。
やはりこうしたアウトプットは重要だと痛感しました。
説明不足で端折ってる部分もあるかと思いますが、自分の備忘録としてこの記事を残させていただきます。

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

[初心者]Docker+Rails+MySQLでの環境構築の記録

はじめに

cloud9やvagrant以外にも
Dockerを使って環境構築してみたいと思い、勉強をしてみました!
Docker環境構築をする上で行った事を振り返ってまとめています。

Dockerについて

  • ホストOSのカーネルを使用している為、軽量で起動が高速
  • 複数人で開発する時に開発環境を統一しやすい
  • そのまま本番サーバーにデプロイする事ができる

などのメリットがあると知り、
様々の方のqiita記事など参考に環境構築に取り組んでみました。

環境

  • Mac OS
  • Ruby 2.6.3
  • Rails 5.2.3
  • MySQL 8.0

【1】作業ディレクトリの作成、移動

$ mkdir sample
$ cd sample

【2】Dockerfileの作成

$ vi Dockerfile
Dockerfile
FROM ruby:2.6.3
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
RUN mkdir /sample
WORKDIR /sample
ADD Gemfile /sample/Gemfile
ADD Gemfile.lock /sample/Gemfile.lock
RUN bundle install
ADD . /sample

【3】Gemfile、空のGemfile.lockを作成

$ vi Gemfile
Gemfile
source 'https://rubygems.org'
gem 'rails', '5.2.3'
$ touch Gemfile.lock

【4】docker-compose.ymlの作成

$ vi docker-compose.yml
docker-compose.yml
version: '3'
services:
  db:
    image: mysql
    command: mysqld --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_USER: root
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '3316:3306'
    volumes:
      - ./db/mysql/volumes:/var/lib/mysql


  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/sample
    ports:
      - "3000:3000"
    depends_on:
      - db

【5】Rails newを実行する

$ docker-compose run web rails new . --force --database=mysql

【6】イメージを再ビルドする

$ docker-compose build

【7】config/database.ymlの設定

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: localhost

database.ymlが上記のようになっているので
下記のように書き換える。

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

【8】サービスを起動する

$ docker-compose up -d

【9】DBを作成する

$ docker-compose run web rails db:create

【10】アクセスしてサーバーの起動を確認する

http://localhost:3000にアクセスしてRailsのトップページが表示されたら完成です。

その後、サーバーを止める時はdocker-compose stopとします。

docker-rails1.png

感じたこと

まだまだDockerに関する知識は浅いですがとにかく環境を構築する事ができました!

環境構築中MySQLの認証プラグインのエラーが発生してかなり苦戦したのですが、
どうやらMySQL8.0は
【4】docker-compose.ymlの作成のところで
command: mysqld --default-authentication-plugin=mysql_native_password
を書かないとエラーが出てしまうようです。
このエラーを解決する為に様々な方法を試していくうちに
MySQLの権限エラーが新たに出るなどして泥沼にハマっていき
10時間ぐらいかかって解決して何とか環境構築できました(汗)

環境構築はとりあえずできましたが
何がどうなっているのかなどまだまだDockerを理解できていない部分が多いので
今後更に深く学習してみようと感じました。

参考

[Rails] DockerでRails + MySQLの開発環境をつくる手順
丁寧すぎるDocker-composeによるrails + MySQL on Dockerの環境構築(Docker for Mac)
Docker Composeのインストール方法(CentOS7.3)

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

【初心者】Docker+Rails+MySQLでの環境構築の記録

はじめに

cloud9やvagrant以外にも
Dockerを使って環境構築してみたいと思い、勉強をしてみました!
Docker環境構築をする上で行った事を振り返ってまとめています。

Dockerについて

  • ホストOSのカーネルを使用している為、軽量で起動が高速
  • 複数人で開発する時に開発環境を統一しやすい
  • そのまま本番サーバーにデプロイする事ができる

などのメリットがあると知り、
様々の方のqiita記事など参考に環境構築に取り組んでみました。

環境

  • Mac OS
  • Ruby 2.6.3
  • Rails 5.2.3
  • MySQL 8.0

【1】作業ディレクトリの作成、移動

$ mkdir sample
$ cd sample

【2】Dockerfileの作成

$ vi Dockerfile
Dockerfile
FROM ruby:2.6.3
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
RUN mkdir /sample
WORKDIR /sample
ADD Gemfile /sample/Gemfile
ADD Gemfile.lock /sample/Gemfile.lock
RUN bundle install
ADD . /sample

【3】Gemfile、空のGemfile.lockを作成

$ vi Gemfile
Gemfile
source 'https://rubygems.org'
gem 'rails', '5.2.3'
$ touch Gemfile.lock

【4】docker-compose.ymlの作成

$ vi docker-compose.yml
docker-compose.yml
version: '3'
services:
  db:
    image: mysql
    command: mysqld --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_USER: root
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '3316:3306'
    volumes:
      - ./db/mysql/volumes:/var/lib/mysql


  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/sample
    ports:
      - "3000:3000"
    depends_on:
      - db

【5】Rails newを実行する

$ docker-compose run web rails new . --force --database=mysql

【6】イメージを再ビルドする

$ docker-compose build

【7】config/database.ymlの設定

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: localhost

database.ymlが上記のようになっているので
下記のように書き換える。

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

【8】サービスを起動する

$ docker-compose up -d

【9】DBを作成する

$ docker-compose run web rails db:create

【10】アクセスしてサーバーの起動を確認する

http://localhost:3000にアクセスしてRailsのトップページが表示されたら完成です。

その後、サーバーを止める時はdocker-compose stopとします。

docker-rails1.png

感じたこと

まだまだDockerに関する知識は浅いですがとにかく環境を構築する事ができました!

環境構築中MySQLの認証プラグインのエラーが発生してかなり苦戦したのですが、
どうやらMySQL8.0は
【4】docker-compose.ymlの作成のところで
command: mysqld --default-authentication-plugin=mysql_native_password
を書かないとエラーが出てしまうようです。
このエラーを解決する為に様々な方法を試していくうちに
MySQLの権限エラーが新たに出るなどして泥沼にハマっていき
10時間ぐらいかかって解決して何とか環境構築できました(汗)

環境構築はとりあえずできましたが
何がどうなっているのかなどまだまだDockerを理解できていない部分が多いので
今後更に深く学習してみようと感じました。

参考

[Rails] DockerでRails + MySQLの開発環境をつくる手順
丁寧すぎるDocker-composeによるrails + MySQL on Dockerの環境構築(Docker for Mac)
Docker Composeのインストール方法(CentOS7.3)

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

カレンダーの上で同一の時間帯のタスクの登録/更新をできないようにする

何がしたいか

このような状況を無くしたい
スクリーンショット 2019-09-29 16.24.30.png

目指す姿?
スクリーンショット 2019-09-29 16.27.36.png

環境

Rails v5.2.3
ruby v2.6.3p62

gem
fullcalendar-rails v3.9

テーブル構造概要

スクリーンショット 2019-09-29 16.36.06.png

どうやって実現しよう...

modelにカスタムバリデーションを追加してエラーにしたい。
(controllerで行うと責務が増えすぎる気もした)
バリデーションでDBに保存済みの時間と画面から入力された時間を比較し、件数が0件か1件以上あるかで実装を頑張る。

条件としては
DBに保存済みの開始時間<入力された開始時間or入力された終了時間<DBに保存されている終了時間
にエラーとするべきと考えた。
⇨SQLでの抽出条件:
"DBに保存済みの開始時間 < 画面から入力された開始時間
AND 画面から入力された開始時間 < DBに保存されている終了時間
OR DBに保存済みの開始時間 < 画面から入力された終了時間
AND 画面から入力された終了時間 < DBに保存されている終了時間"

実装したソース

controller.rb
def create #新規登録
  act_history = current_user.activity_historys.build(
    activity_name:      @json_hash[:activity_name],
    from_time:          @json_hash[:from_time],
    to_time:            @json_hash[:to_time],
    remarks:            @json_hash[:remarks],
    action:             "create",      #アクセサにセット
    current_user:       current_user   #アクセサにセット
  )

...(略)
end

def update #更新
  ...(略)
  respond_to do |format|
    if act_history.update(
        activity_name:      @json_hash[:activity_name],
        from_time:          @json_hash[:from_time],
        to_time:            @json_hash[:to_time],
        remarks:            @json_hash[:remarks],
        action:             "update",       #アクセサにセット
        current_user:       current_user    #アクセサにセット
      )
  ...(略)
end
model.rb
  # アクセサ
  attr_accessor :action, :current_user
  # バリデーション
  validate :check_time

 ...(略)

 private

  def check_time
    ...(略)
    same_time_zone_exists
  end
  ...(略)

 # 画面から受け取った開始時間、終了時間をもとにすでにDBに存在しているか検証する
  def same_time_zone_exists
    exists_act_historys = current_user.activity_historys.where(
      #SQL抽出条件
      "(from_time < ? AND ? < to_time OR from_time < ? AND ? < to_time)",
      from_time,
      from_time,
      to_time,
      to_time
    )


    if action == "create" && exists_act_historys.count > 0
      errors.add(:from_time, ": 同一時間帯の登録はできません")
      errors.add(:to_time, ": 同一時間帯の登録はできません")
    elsif action == "update" && exists_act_historys.count > 0
      exists_act_historys.pluck(:id).each do |exists_ids|
        if exists_ids != id
          errors.add(:from_time, ": 同一時間帯の登録はできません")
          errors.add(:to_time, ": 同一時間帯の登録はできません")
        end
      end
    end
  end

実装後

スクリーンショット 2019-09-29 18.04.07.png
⬇️エラーになるかテストその1
スクリーンショット 2019-09-29 18.05.35.png
⬇️正しくエラーになる!
スクリーンショット 2019-09-29 18.06.26.png

スクリーンショット 2019-09-29 18.04.07.png
⬇️エラーになるかテストその2
スクリーンショット 2019-09-29 18.07.56.png
⬇️エラーにならない...
スクリーンショット 2019-09-29 18.08.51.png

抽出条件が足りない模様。
入力された開始時間<DBに保存済みの開始時間〜終了時間<入力された終了時間
⇨SQLでの抽出条件:
"画面から入力された開始時間 < DBに保存済みの開始時間
AND 画面から入力された終了時間 < DBに保存されている終了時間"

完成版ソース

model.rb
# 画面から受け取った開始時間、終了時間をもとにすでにDBに存在しているか検証する
  def same_time_zone_exists
    exists_act_historys = current_user.activity_historys.where(
      #抽出条件追加
      "(from_time < ? AND ? < to_time OR from_time < ? AND ? < to_time) OR
       (? < from_time AND to_time < ?)",
      from_time,
      from_time,
      to_time,
      to_time,
      from_time,
      to_time
    )

    if action == "create" && exists_act_historys.count > 0
      errors.add(:from_time, ": 同一時間帯の登録はできません")
      errors.add(:to_time, ": 同一時間帯の登録はできません")
    elsif action == "update" && exists_act_historys.count > 0
      exists_act_historys.pluck(:id).each do |exists_ids|
        if exists_ids != id
          errors.add(:from_time, ": 同一時間帯の登録はできません")
          errors.add(:to_time, ": 同一時間帯の登録はできません")
        end
      end
    end
  end

スクリーンショット 2019-09-29 18.04.07.png
⬇️テスト再挑戦
スクリーンショット 2019-09-29 18.07.56.png
⬇️正しくエラーになることを確認
スクリーンショット 2019-09-29 18.16.06.png

感想

とりあえず実装はできたので良いが、結構処理が複雑になっているように見えるので、もっとスリムにしたいです。他に方法があったらご指摘ください

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

Watirのgemを使ってSelenium::WebDriver::Error::WebDriverErrorのエラーが出た時の対処法

コマンド

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

Angular8+Rails5でアプリを作ってみる

AngularとRailsを組み合わせてみる

1_nmQ30kViITehpW9xALP2Rg.jpeg
前回の記事ではAngular+NestJSの組み合わせについてピックアップをしましたが、今回はRuby on Rails5とAngular8の組み合わせでwebアプリを作成している記事があり、読んでいてとても面白い内容だったので翻訳に挑戦しました!

どちらも個人的に気に入っているフレームワークなので、世の中にこの組み合わせでの開発が増えてくると嬉しいです。

Angular8とRails5 (*以下記事の翻訳)

あたなはどうか知らないが、学びやすく、素早く書けて、オープンソースという理由で私はRuby on Railsが大好きだ。また、Ivyというクールな新しいレンダーエンジンを導入したAngularの新バージョン、Angular8のリリースにもワクワクしている。

もしあなたが私と同じでこれら2つのフレームワークに関する興味をわかってくれて、でも、その素晴らしい2つをどう始めたら良いのかさっぱりわからない場合は、ぜひこれを読んでほしい。

さっそく初めてみよう!

慣習的にJavascriptフレームワークと一緒にRailsを使う場合は、webpackが必要とされていた。しかし、Railsにはwebpackerと呼ばれるこれにうってつけのgemがあり、Angularプロジェクトを動かしてくれる。しかし、最新バージョン(7系以上)のAngularでは内部でwebpackシステムを持っており、そのようなプロジェクト構成が、素晴らしいAngularCLIを使う妨げとなってしまうのだ。

その代わりに私たちがしなければならないのは、Angularからの出力ファイルを受け取り、それらを提供するコントローラを備えたAPIとしてのRailsアプリを使うことだ。

始める前に下記をインストールしてほしい。

  • node (8.9以上)
  • NPM (5.5.1以上)
  • Angular CLI
  • Rails 5+ (インストールにRVMまたはRbenvを使うことをおすすめします)
  • Postgresql

1. Angularプロジェクトを作成する

好きなディレクトリの場所でターミナルを開いて、Rails/Angularプロジェクトを作成する。

ng new rails-angular-project

Angular routingを使うか聞かれたら、迷わず「YES」を押しちゃってください!(かなり良いですよ!)
その次に、好きなスタイルシートの形式を選択して下さい。(個人的にはSCSSで進めてます)
そして、全て完了したら先ほど作ったプロジェクトにジャンプしましょう!

cd rails-angular-project

2. Railsプロジェクトを作成する

では、現在のディレクトリにRailsプロジェクトを作成しましょう!

rails new . -database=postgresql

これによって、Angularプロジェクトの内部にRailsプロジェクトを作成し始めるが、全てのマージを拒否した場合、コンフリクトが発生する。
Railsプロジェクトが作成されたなら、.gitignoreファイルにいくつかファイルを追加する必要があります。
.gitignoreファイルを開いて、下記をコピペして下さい。

# Ignore bundler config.
/.bundle

# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep

# Ignore uploaded files in development
/storage/*
!/storage/.keep

.byebug_history

# Ignore master key for decrypting credentials and more.
/config/master.key

# Ignore application configuration
/config/application.yml

3. 上記2つを合体する

今の段階で、Angularプロジェクトと一緒になったRailsアプリとなっており、これはAPIとしてのRailsを使用し提供している一方で、Angularフレームワークを用いたSPAの利益も享受していることと言える。しかしその前にRailsにAngularが提供するファイルにアクセスできるようにしなければならない。

通常はAngularアプリでは、ng serveを使用して開発を進めるのだが、今回はng buildを使用してRailsのパブリックディレクトリに出力するように設定する。

ルートディレクトリにあるAngular.jsonを開いて、出力先ディレクトリをdist/rails-angular-appからpublicに変更する。

angular.json
...

"options": {
    "outputPath": "public",
...

次に、app/controllersにstatic_controller.rbという名前のファイルを作成する。

app/controllers/static_controller.rb
class StaticController < Rails::ApplicationController
    def index
        render file: Rails.root.join('public', 'index.html')
    end
end

static_controllerはAngularによって提供されるindex.htmlをレンダリングする役目を持ちます。

そして、下記のコードをroutes.rbに追加して下さい。

routes.rb
Rails.application.routes.draw do
    get '*other', to: 'static#index'
end

これで、Railsにルートをキャッチさせて、static_controllerにリダイレクトすることができます。また、上記のコードによってapiに必要となるどのルートも定義することができるのです。でもそれは次の機会に回しましょう!

4. Foremanを使って便利にする

Foremanを導入することによって、Webアプリを提供しやすくしましょう。

gemにforemanを追加することによってインストールして下さい。

Gemfile
...
# Use foreman
gem 'foreman'
...

bundle installで新しいgemをインストールします。

bundle install

Procfileと呼ばれるファイルを新しくルートディレクトリに追加します。これは組み合わせされる全てのコマンドをforemanが読み込めるようにするためのファイルです。

Procfile
web: rails s -p 3000
client: ng build --watch=true

これでforemanを起動できます。

foreman start

RailsアプリとAngularアプリが同時に起動されるはずです。そして、localhost:3000にアクセスすると以下のような画面が表示されるでしょう。

1_RCUJQELq4Gpw5AYRc7wUCQ.png

完了です!これで自由にWebアプリを開発することができますし、apiにシンプルかつパワフルなフレームワークを与えるのと同時に、Angular CLIの力を用いることができます。

5.終わりに

今回、Rails5のバックエンドとAngular8のフロントエンドで、同じディレクトリに存在し、それぞれの力を最大限に活用するアプリケーションを作りました。このチュートリアルがあなたにとって役に立てれば幸いです。

参考記事

Angular 8+ with Rails 5+ - Sam Green

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

Rails 6 に Bootstrap3 導入

1. yarn で Bootstrap と jquery を導入する

$ yarn add bootstrap-sass jquery

2. app/config/webpack/environment.js に jquery が使えるように設定追加

app/config/webpack/environment.js
const { environment } = require('@rails/webpacker')

const webpack = require('webpack')
environment.plugins.append('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery'
  })
)

module.exports = environment

3. app/javascript/packs/application.js に 以下追記

app/javascript/packs/application.js
import '../javascripts/application.js'
import '../stylesheets/application.scss'

4. app/javascript/packs/stylesheets/application.scss に以下追記

app/javascript/packs/stylesheets/application.scss
// bootstrap-sass 内部で 相対パスを解決できないため、パス変数を差し替え
$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/";

@import "~bootstrap-sass/assets/stylesheets/_bootstrap.scss"

5. app/javascript/javascripts/application.js に以下追記

app/javascript/javascripts/application.js
import 'bootstrap-sass';
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

洋書多読コミュニティサイト「多読マラソン」をリリースしました!

tadoku.gif

はじめに

以前、こういうWebサイトをリリースしました。(リンクは外しています)
多読マラソン

サイト名は、多読マラソンです。ネーミングセンスとかは突っ込まないでいただけると嬉しいですね!

制作経緯

僕は大学受験時代に洋書を読むことにハマり、そこから受験勉強そっちのけで英語の本を読みまくってきました。いわゆる多読というやつですね。洋書をとにかくたくさん読むと。

一時期、TOEICの勉強をやっていたこともあるんですが、あまりにつまらなすぎて1ヶ月立たずに挫折してしまいました。そんな僕ですが、この洋書多読にはハマりました。これがとにかく楽しい。

ちなみに、洋書多読にハマったおかげで、ぶっつけ本番で受けたTOEICでも900点突破できたので、本当、多読のおかげで人生変わったと言っても過言ではないです。僕の場合は。

で、この多読界隈では、読んだ洋書の量を「語数」で表すんですね。100万語突破とか、500万語突破とか。200〜300ページの洋書がだいたい50000語なので、100万語突破だと、普通の洋書を20冊読んだ計算になります。

多読実践者の間では、100万語読むことがベンチマークとされていまして、皆この数字を目標にがんばります。そして、この語数を記録するために、いろいろなサービスがあるわけなのですが、僕も例に漏れず、あるwebサイトを使用していました。

ところがこのサイト、ところどころ使いづらいんですね。あまりメンテナンスもされてなさそうで、正直なんだかなーという思いを感じていました。これが受験生の頃ですね。

それから数年が経ち、プログラミングを初めて1年半にもなりまして、あるときこう思ったわけなんですね。「ぶっちゃけ今の俺なら、もっと良いサイト作れるんじゃねーの?」と。

制作風景

制作期間は約3週間。2019年9月10日から制作を開始して、この記事を書いているのが9月29日。サイトの規模的に、10日ぐらいで完成させたかったんですが、インターンをしていたので思うように時間が取れなかったことと、サービスが完成に近づくにつれて、ここ直したい!、という箇所が増えて、3歩進んで3歩下がる的な状況に陥ってしまい、なかなか遠かったです。

ですが!
ひとまずMVP的なものにはなったので、一旦リリースしてみようか、って感じです。

仕様

Amazon Product Advertising API
を使ったサービスとなっています。これがなきゃ成立しません。

使用技術

  • Amazon Product Advertising API
  • Ruby on Rails
  • HTML(Slim), CSS(Sass)
  • DB
    • Postgresql
  • CircleCI
  • Docker
  • Heroku(独自ドメイン)

感想

ちょっとRailsに飽きてきた感がありましたw

中小規模のサービスをちゃちゃっと開発するのにはRails最強だと思うんですが、あまりに便利すぎて、なんか堕落してるような気分になるのが玉に瑕ですかねw

参考文献・サイト・ライブラリ

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

[Rails]stringからenumに変更する

stringで保存されたデータをenumに変更します。

enumの設定

ゴールとなるenumの設定。

Reservation.rb
class Reservation < ApplicationRecord
    enum status: { applied: 0, accepted: 1, cancelled: 2, completed: 3 }
end

migrationの設定

元データは、Reservation.statusにstringで'applied','accepted','cancelled','completed'が入っています。

以下のようなマイグレーションで、カラム型をStringからIntegerに変更し、データも移行するところまで一括で行います。

class ReservationStatusConvertToEnum < ActiveRecord::Migration[6.0]
  # データベース操作の際にコールバックを呼ばないよう、テーブルを使うだけのTmpモデルを作る
  class TmpReservation < ApplicationRecord
    self.table_name = :reservations
  end

  def change
    # 元データのカラム名を変更
    change_table :reservations do |t|
      t.rename :status, :old_status
    end

    reversible do |dir|
      dir.up do
         # 新しくenum用のintegerカラムを追加
        add_column :reservations, :status, :integer, null: false, default: 0
        # テーブル情報をリセットする
        TmpReservation.reset_column_information
        # stringで持っていた名前をenumと同じ順でarrayにし、旧データをインデックス番号で取る
        TmpReservation.find_each do |tr|
          tr.status = %w(applied accepted cancelled completed).index(tr.old_status)
          tr.save!
        end
        # 旧データを捨てる
        remove_column :reservations, :old_status
      end

      # upの逆
      dir.down do
        add_column :reservations, :old_status, :string
        TmpReservation.reset_column_information
        TmpReservation.find_each do |tr|
          r = Reservation.find(tr.id)
          tr.old_status = r.status
          tr.save!
        end
        remove_column :reservations, :status
      end
    end
  end
end

参考: Rails Migration, converting from string to enum | stack overflow

Scopeの修正

条件をSQL形式で書いてしまっているところは、enumに変換後に期待するデータが取ってこれなくなりますので、修正します。

scope :applied_reservations, -> { where("status = 'applied'") } # NG
scope :applied_reservations, -> { where(status: 'applied') } # OK

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

rails 条件付きでバリデーションを効かせる

こういうときはバリデーション効かせないで欲しい

ってときの対応。

通常のバリデーションは以下の感じ

class User < ApplicationRecord
    validates :name, presence: true
    validates :email, presence: true, length: {maximum: 255},
                      uniqueness: { case_sensitive: false}
    validates :password, presence: true, confirmation: true
    has_many :questions

どういう状況で使う?

例えばSNSログインの場合、twitterでやろうとするとemailの値が取得できない(2019/08/10現在)のでバリデーションが効くとエラーになる。。。

⇨「SNSログインの際に取得するuidカラムに値が入っていないときは通常のログインとみなしてバリデーションを効かせたい。」時

ifunlessで条件を指定して、目的の項目へ追加する。

class User < ApplicationRecord
    validates :name, presence: true
    validates :email, presence: true, length: {maximum: 255},
                      uniqueness: { case_sensitive: false}, unless: :uid?  #email取れないからこんな感じ
    validates :password, length: {minimum: 6}, presence: true, confirmation: true, unless: :uid? #パスワードもSNSログインと別管理ならこんな感じ
    has_many :questions

callbackに対しても指定可能

以下はバリデーション用のヘルパーを定義してそのヘルパーの中でunlessを使い処理をするかしないかを判定している。

class User < ApplicationRecord
    validates :name, presence: true, length: {maximum: 50}
    #以下でヘルパーを呼び出す
    before_save :email_downcase
    VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
    validates :email, presence: true, length: {maximum: 255},
                      format: { with: VALID_EMAIL_REGEX },
                      uniqueness: { case_sensitive: false}, unless: :uid?  #こんな感じ
    validates :password, length: {minimum: 6}, presence: true, confirmation: true, unless: :uid? 
    has_many :questions

    #email用にヘルパーを定義
    def email_downcase
        unless uid?
            self.email.downcase!
        end
    end

とりあえず、こんなけ。

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

【Rails】 controller内で共通の処理をメソッド化してbefore_actionで呼び出す

環境

rails 5.2.3
ruby 2.5.1

概要

コントローラー内で共通の処理を書いていたら、メンターにメソッド化しましょうと言われたので、脱・初心者を目指し、備忘録。

メンターレビュー前

hogehoge_controller.rb
class hogehogeController < ApplicationController

  def new
    @user = User.find(params[:user_id])
    @evaluation_all = Evaluation.group(:seller_id).size[@user.id]
    @evaluation = Evaluation.new
  end


  def create
    @user = User.find(params[:user_id])
    @evaluation = Evaluation.new(evaluation_params)
    if @evaluation.save
      redirect_to introduction_user_path(@user.id), notice: "評価が送信されました"
    else
      redirect_to introduction_user_path, notice: "評価が正常に送信できませんでした。もう一度行ってください。"
    end
  end

  private

  def user_params
    params.require(:user)
  end

  def evaluation_params
    params.require(:evaluation).permit(:rating, :comment).merge(buyer_id: current_user.id, seller_id: @user.id)
  end
end

指摘されたのはこの部分。

@user = User.find(params[:user_id])

同じ記述が2回出てきていますね。
この@userの処理は共通のものなので、分けましょう、と言われたわけです。

またこの処理は外部から呼び込まれる必要はないので、private以下に追記します。

private
  def set_user
    @user = User.find(params[:user_id])
  end

こうすることで、呼び出しがクラス内部に限定されるんですね。

privateについてはこちらの記事が勉強になりました。ありがとうございます。
Ruby の private と protected 。歴史と使い分け

さて、あとは各メソッドが実行される時に呼び出されると善きですね。

そんな時は before_actionを使います。

class EvaluationsController < ApplicationController

  before_action :set_user, only: [:new, :create]

  def new
    hogehoge...

before_action [メソッド名],[おぷしょん]

onlyは、どのメソッドが呼ばれた時にbefore_actionを実行するのかを示し、
expect は、指定したメソッドが呼ばれた時は実行しません。

今回は、newとcreateの実行前にやってほしい処理ですからこうします。

 before_action :set_user, only: [:new, :create]

メンターレビュー後

...というわけで直したのがこちら

hogehoge_controller.rb
class EvaluationsController < ApplicationController

  before_action :set_user, only: [:new, :create]

  def new
    @evaluation_all = Evaluation.group(:seller_id).size[@user.id]
    @evaluation = Evaluation.new
  end


  def create
    @evaluation = Evaluation.new(evaluation_params)
    if @evaluation.save
      redirect_to introduction_user_path(@user.id), notice: "評価が送信されました"
    else
      redirect_to introduction_user_path, notice: "評価が正常に送信できませんでした。もう一度行ってください。"
    end
  end

  private

  def set_user
    @user = User.find(params[:user_id])
  end

  def user_params
    params.require(:user)
  end

  def evaluation_params
    params.require(:evaluation).permit(:rating, :comment).merge(buyer_id: current_user.id, seller_id: @user.id)
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む