20191126のRailsに関する記事は30件です。

eager_load/preload/includes/joinsの違いメモ

動機

日頃Goしか書いてないGopherがRails書いたときにつまったActiveRecord、特にjoins周りの挙動が直感的によくわからなかったので実際にデータ用意して調べてみた
(メソッドの具体的な説明は他にたくさん記事があるのでそちらを参照してください。)

実行環境

Rails公式Docのサンプルブログアプリ
article.rbcomment.rb のモデルを使用。
今回は、それぞれ約25万行を用意した。

article.rb
class Article < ApplicationRecord
  has_many :comments, dependent: :destroy
  validates :title, presence: true,
                    length: { minimum: 5 }
end
comment.rb
class Comment < ApplicationRecord
  belongs_to :article
end

Controllerから preload/eager_load/joins/includes を用い、それぞれの吐くSQLやパフォーマンスを比較してみる

0. 素

Article.limit(100)

SELECT `articles`.* FROM `articles` LIMIT 100
-- Completed 200 OK in 154ms (Views: 127.1ms | ActiveRecord: 15.7ms | Allocations: 20418)

まずは検証用にそのままで...

1. eager_load

Article.eager_load(:comments).limit(100)

SQL (1.9ms)  SELECT DISTINCT `articles`.`id` FROM `articles` LEFT OUTER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` LIMIT 100
SQL (575.4ms)  SELECT `articles`.`id` AS t0_r0, `articles`.`title` AS t0_r1, `articles`.`text` AS t0_r2, `articles`.`created_at` AS t0_r3, `articles`.`updated_at` AS t0_r4, `comments`.`id` AS t1_r0, `comments`.`commenter` AS t1_r1, `comments`.`body` AS t1_r2, `comments`.`article_id` AS t1_r3, `comments`.`created_at` AS t1_r4, `comments`.`updated_at` AS t1_r5 FROM `articles` LEFT OUTER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE `articles`.`id` 
IN (1, 2, 3, 4, 6, 7, 8, 9, 13, 14, 15, 16, 17, 18, 19, 20, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157)
-- Completed 200 OK in 4869ms (Views: 4280.1ms | ActiveRecord: 585.9ms | Allocations: 2103879)

万能感ある。駆動表のキャッシュをしながら、絞り込みもできる。IN長すぎワロタ。

2. preload

Article.preload(:comments).limit(100)

Article Load (1.9ms)    SELECT `articles`.* FROM `articles` LIMIT 100
Comment Load (405.8ms)  SELECT `comments`.* FROM `comments` WHERE `comments`.`article_id` 
IN (1, 2, 3, 4, 6, 7, 8, 9, 13, 14, 15, 16, 17, 18, 19, 20, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157)
-- Completed 200 OK in 4304ms (Views: 3883.6ms | ActiveRecord: 416.9ms | Allocations: 1658640)

eager_loadより省エネな感じ。ただ、最後のSELECT文がcommentsテーブルしか参照してないので絞り込みができない

3. includes

Article.includes(:comments).limit(100)

Article Load (1.5ms)  SELECT `articles`.* FROM `articles` LIMIT 100
Comment Load (384.1ms)  SELECT `comments`.* FROM `comments` WHERE `comments`.`article_id` 
IN (1, 2, 3, 4, 6, 7, 8, 9, 13, 14, 15, 16, 17, 18, 19, 20, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157)
-- Completed 200 OK in 3737ms (Views: 3339.1ms | ActiveRecord: 395.3ms | Allocations: 1652329)

今回はpreloadと同じ挙動
(includes は場合によって挙動が変わる. 最後のSELECT文にないテーブルのwhereとかはできないので、そういう時はeager_loadになる)

def eager_loading?
  @should_eager_load ||=
    eager_load_values.any? ||
      includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
end

4. joins

Article.joins(:comments).limit(100)

Article Load (2.1ms)  SELECT `articles`.* FROM `articles` INNER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` LIMIT 100
-- Completed 200 OK in 67ms (Views: 56.1ms | ActiveRecord: 7.0ms | Allocations: 21703)

いつものアレ。安心感ある。

まとめ

そもそもActiveRecordは LazyLoad、すなわち 必要になったときにSQLが実行される
for文の中でSQLが複数回発行(N+1)されないために、こういう機構が必要になってくる。
(生SQLerの私にはこの感覚があんまりない)

後は、where使いたいなら eager_load 何もしないなら preload って感じ。
頭使いたくない人は 「とりあえず includes」 しとけばなんとかなる。
ただ、複数紐づくテーブル扱い出すと最終的に吐き出されるSQLが予測困難になるので、ちゃんと使い分けたいところ。
ちゃんと使い分けたい人はこの記事とか読みましょう。実装追ってて勉強になります。

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

Rails +MySQLのプロジェクトをherokuへデプロイ

RailsのデフォルトDBはSQLiteですが、
MySQLにしたものとして先に進めます
MySQL導入方法
https://qiita.com/shigeshige/items/02ce019e02bcfff14b9b
今回はheorkuへ会員登録したものとして先に進めます

production環境

実際にHerokuでデプロイした後もMySQLで動くようにします

デフォルトでfalseとなっている以下の箇所をtrueに変更

Railsは本番環境での動的な画像の表示がデフォルトでオフになっています。

config/environments/production.rb
# 以下の箇所を変更
config.assets.compile = true
config.assets.initialize_on_precompile=false

heroku だと、 sqlite3 がビルドできないので、開発環境でだけ使うよ、っていう宣言が必要
 (heroku では、 production の gem だけビルドされる)

↓を削除し

Gemfile
gem 'sqlite3'

代わりに↓を挿入する

Gemfile
gem 'sqlite3', group: [:development, :test]
gem 'mysql2'
gem 'rails_12factor', group: :production
$ bundle install
コミットしておきます

Heroku作成

$ heroku create
$ git push heroku master

cleardbアドオンを追加

後述するアドオンの追加のところで、Herokuにクレジットカード情報を登録しておく必要がある(お金はかかりません)ので、登録しておいてください。

$ heroku addons:create cleardb:ignite

heroku-postgresqlアドオンを削除

$ heroku addons:destroy heroku-postgresql -a <アプリ名>

DATABASE_URLを編集

$ heroku config | grep CLEARDB_DATABASE_URL
CLEARDB_DATABASE_URL:mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true

環境変数を設定

それぞれを設定する

heroku config:add DB_NAME='<データベース名>'
heroku config:add DB_USERNAME='<ユーザー名>'
heroku config:add DB_PASSWORD='<パスワード>'
heroku config:add DB_HOSTNAME='<ホスト名>'
heroku config:add DB_PORT='3306'

上記のURLをMySQL2に切り替えます。コピーペーストしつつmysql2に書き換えてください

heroku config:add DATABASE_URL='mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true'
heroku config
で確認できます

database.ymlに設定

database.yml
production:
  <<: *default
  database: <%= ENV['DB_NAME'] %> 
  username: <%= ENV['DB_USERNAME'] %>
  password: <%= ENV['DB_PASSWORD'] %>

Herokuにデプロイ

$ git push heroku master

$ heroku run rake db:create
$ heroku run rake db:migrate

テストデータがある場合
$ heroku run rake db:seed
$ heroku open

でデプロイできました!!!
色々とうまくいかない場合もありましたがなんとかできました
参考になれば幸いです

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

Rails + TypeScript + Swagger 環境をDockerで爆速で構築する

なにこの記事

単純にdcoker-composeを使って、フロントとWEB APIを分けて、ついでにAPIをSwaggerで管理するテンプレートの紹介と、テンプレート作るまで手順を書いたザックリとした記事です。
たぶん爆速でできる、はず。

テンプレート

こちらのリポジトリからクローンしてください。 ./qs init で環境整うので、あとは http://localhost:8080 にアクセスすると、こんな感じで簡単なWEBアプリが作られてます。

mock_railswebver02.gif

Swaggerを利用する際は、 ./qs up doc した後で、 http://localhost にアクセスするとAPIドキュメントを利用できます。

FireShot Capture 023 - Swagger UI - localhost.png

テンプレート作成までの手順

ここからはどうやって上記のテンプレート作ったかの手順を説明していきます。

構成

docker-composeのサービス構成としては下記のようにしました。
webがフロントエンドでTypeScriptにて処理を行います。
apiがWEB APIでRuby on Railsで作られてます。
フロントも含めて全てRailsで記述できますが、今回はフロントとAPIのプラットフォームが別という想定で構築しました。
APIドキュメントはSwaggerがイメージを配布してたのでサービスを分けました。

docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    ports:
      - "5432:5432"
    volumes:
      - data:/var/lib/postgresql/data:cached
  api:
    build: ./api
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - ./api:/app:cached
      - bundle:/usr/local/bundle:cached
    environment:
      HOME: /app
      RAILS_ENV: development
    ports:
      - "3000:3000"
    tty: true
    links:
      - db
  web:
    build: ./web
    command: npm start
    volumes:
      - ./web:/app:cached
    environment:
      NODE_ENV: development
    ports:
      - "8080:8080"
    tty: true
    links:
      - api
  doc:
    image: swaggerapi/swagger-ui
    volumes:
      - ./api.yml:/usr/share/nginx/html/api.yml
    environment:
      API_URL: api.yml
    ports:
      - "80:8080"

volumes:
  bundle:
    driver: local
  data:
    driver: local

ディレクトリ構成は以下のようにしてます。各フォルダ配下にDockerfileを配置して、プロジェクトフォルダ直下のdocker-composeから管理しています。

projects # プロジェクトフォルダ
  - api # Railsのソースを配置
    - Dockerfile
    - Gemfile
  - web # TypeScriptのソースを配置
    - Dockerfile
    - package.json
  api.yml # Swagger定義
  docker-compose.yml

それでは各サービスの構築手順を解説していきます。

API

まずはAPIを立ち上げましょう。とは言っても簡単です。
コマンド打っただけで簡単に構築できました。やっぱりRailsって便利ですね。

アプリの立ち上げ

DockerfileとGemfileを配置して、下記のコマンドを実行していきます。 http://localhost:3000 にアクセスしてRailsのウェルカムページが表示されれば完了です。

$ docker-compose build api
$ docker-compose run --rm api bundle install
$ docker-compose run --rm api bundle exec rails new . -f -d=postgresql --api
$ docker-compose run --rm api bundle update
# database.ymlを下記の内容で手動で書き換える
$ docker-compose run --rm api bundle exec rails db:create
$ docker-compose up api
database.yml
default: &default
  adapter: postgresql
  encoding: utf8
  min_messages: WARNING
  host: db
  port: 5432
  username: postgres
  password: postgres
  pool: 5
  timeout: 5000
  stats_execution_limit: 10

development:
  <<: *default
  database: development

test:
  <<: *default
  database: test

production:
  <<: *default
  database: production

Dockerfile

# use ruby version 2.6.5
FROM ruby:2.6.5

# using japanese on rails console
ENV LANG C.UTF-8

# remove warn
ENV DEBCONF_NOWARNINGS yes
ENV APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE yes
ENV XDG_CACHE_HOME /tmp
EXPOSE 3000

# install package to docker container
RUN apt-get update -qq && apt-get install -y \
    build-essential \
    libpq-dev \
    vim \
    less

# install yarn
RUN apt-get install apt-transport-https
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update && apt-get install -y yarn

# install nodejs
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
RUN apt-get install -y nodejs

# setting work directory
RUN mkdir /app
WORKDIR /app

# setting environment value
ENV HOME /app

# executing bundle install
COPY Gemfile /app/Gemfile

Gemfile

source 'https://rubygems.org'

gem 'rails', '~> 5.2.3'

エンドポイントの追加

文字列フィールドを1つもつModelのAPIを追加します。下記のコマンド打てば、API一式とテストを作ってくれます。素晴らしいいい。
Rspecを使うのでGemfileにgemを追加しておいてください。

$ docker-compose run --rm api bundle exec rails g scaffold Content body:string
$ docker-compose run --rm api bundle install
$ docker-compose run --rm api bundle exec rails g rspec:install
$ docker-compose run --rm api bundle exec g rspec:integration Content

これでほぼ完成ですが、なぜかapiフラグをつけるとストロングパラメーターを使わないので、下記のようにコントローラーを修正します。

contents_controllers.rb
  def content_params
    params.require(:content).permit(:body)
  end

あとは、適宜取得したいようにController、Modelの修正と、テストの追加はしておいてください。
面倒なんでここでは割愛します。リポジトリのコミットログ見てください。

rack_corsの導入

このままだとフロントからアクセスできないので、rack_corsを導入します。
Gemfileにrack-corsを追加して、docker-compose run --rm api bundle installしてください。
そして、下記の修正を config/application.rb に追記すれば完成です。

config/application.rb
    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins 'localhost:3000', 'localhost:8080'
        resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options]
      end
    end

疎通確認

これだけでAPIの構築が完了しました。最後に疎通確認を行います。
RailsコンソールからContentモデルに適当にデータを登録して、ブラウザから http://localhost:3000/contents を叩いて見ましょう。

[
  {
    "id": 1,
    "body": "hoge",
    "created_at": "2019-11-22T15:54:26.543Z",
    "updated_at": "2019-11-22T15:54:26.543Z"
  },
  {
    "id": 2,
    "body": "foo",
    "created_at": "2019-11-22T15:54:30.464Z",
    "updated_at": "2019-11-22T15:54:30.464Z"
  }
]

無事に取得できました。これでAPIの作成は終了です。

フロント

実際にユーザーがアクセスして使うフロントアプリケーションを作成します。
React、VueなどJSフレームワークを使ってもいいのですが、今回は超小さいアプリなのでTypeScriptを使ってサクッと作ろうかと思います。

アプリの立ち上げ

apiと同じようにDockerfileとpackage.jsonを配置して、下記のコマンドを実行していきます。

$ docker-compose build web
$ docker-compose run --rm web npm install 

urlとかは適宜変えてください。パッケージは今回使うものを記載してあります。

package.json
{
  "name": "web",
  "version": "1.0.0",
  "description": "web app",
  "main": "index.js",
  "scripts": {
    "start": "http-server -o",
    "tsc": "tsc",
    "build": "webpack"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/belion-freee/rails_web_api.git"
  },
  "author": "belion-freee",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/belion-freee/rails_web_api/issues"
  },
  "homepage": "https://github.com/belion-freee/rails_web_api#readme",
  "devDependencies": {
    "axios": "^0.19.0",
    "http-server": "^0.11.1",
    "ts-loader": "^6.2.1",
    "typescript": "^3.7.2",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10"
  }
}

処理の記述

TypeScriptは初めてでしたので、下記の記事を参考にして作成しました。

https://qiita.com/EBIHARA_kenji/items/31b7c1c62426bdabd263

API通信のために axios

Webサーバーとして立ち上げるために http-server

をそれぞれ使ってます。

tsconfig.jsonがTypeScriptの設定を記述するファイルです。自分は下記のように設定しました。もっといろんな設定ができるので公式リファレンスを読んでみてください。

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2015",                       /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "outDir": "./build",                      /* Redirect output structure to the directory. */
    "rootDir": "./src",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    "strict": true,                           /* Enable all strict type-checking options. */
    "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    "forceConsistentCasingInFileNames": true  /* Disallow inconsistently-cased references to the same file. */
  },
  "include": [
      "src/**/*.ts"
  ],
  "exclude": [
      "node_modules",
      "**/*.spec.ts"
  ]
}

ソースコード

ここからは実際のソースコードです。下記のように記述しました。

index.html を用意して、DOMに要素を動的にjsで展開して行く構成です。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./style.css">
    <title>TS-Web</title>
  </head>
  <body>
    <h1>This is the template of Web APP</h1>
    <h2>created by TypeScript</h2>
    <h3>Those are contents from Web API</h3>
    <div id="contents-form">
      <form>
        <input type="text" value="" id="content-body">
        <button type="button" class="add-content">Add</button>
      </form>
    </div>
    <div id="contents"></div>
    <script src="./build/index.js"></script>
  </body>
</html>

srcディレクトリ配下にindex.htmlを操作するjsファイルを配置します。

  • index.ts : indexを操作するためのjs。
  • api.ts : Rails-APIと通信するための機能を備えたクラス。

https://github.com/belion-freee/rails_web_api/tree/master/web/src

ビルド

ここまで作成したら次にビルドする必要があります。ビルドにはwebpackを仕様します。すでにインストール済みなので、コマンドを実行してビルドしましょう。

$ docker-compose run --rm web npm run build

buildディレクトリにソースが作成されていれば成功です。
ここまででフロントの実装は終了です。

APIドキュメント

次にAPIドキュメントを作成します。とは言ってもすごい簡単です。

プロジェクト配下にapi.ymlを配置します。

サンプルは公式から拝借しました。
https://github.com/swagger-api/swagger-samples/blob/master/java/inflector-dropwizard/src/main/swagger/swagger.yaml

内容はこんな感じです。
https://github.com/belion-freee/rails_web_api/blob/master/api.yml

記述に関しては、API自体がシンプルなのもありますが、ドキュメント読まなくてもなんとなく書けるくらい分かり易かったです。

記述が終わったら、docker-compose up doc して http://localhost にアクセスすると読めるので、記述が正しいか確認してください。
これで全ての工程が終了です。

まとめ

いかがだったでしょうか。
API関連はすごい簡単に導入できましたが、TypeScriptはちょっと時間がかかりました。
TypeScriptに関してはもっとちゃんと書きたいなと思います。だいぶやっつけ感ある。

それでも、ものすごい早く複数プラットフォームのミニマムなWEBアプリを開発できたので、Dockerさんに感謝です。
流石にミニマムすぎるので、暇なときにAPI認証のエンドポイントを実装したいと思います。

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

Herokuでデプロイした際にApplication error(エラーコードH10 (App crashed))が出た時の対処法

環境

Ruby 2.6.3
Rails 5.2.3

現象

Railsアプリケーションを作成後、Herokuへデプロイした際に以下画像のエラーが発生。
その際の対処法を備忘録として残しておきます。

https___qiita-image-store.s3.amazonaws.com_0_110452_526e9c21-ec79-5305-c7e5-b536f689a3c4.jpeg

 【対処方法】エラーの原因を把握する

以下コードで状況を把握します。

$ heroku logs --tail

エラーコードが確認できました。

2019-11-26T05:06:44.357974+00:00 heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/" host=agile-meadow-66722.herokuapp.com request_id=e2c36b19-f3d1-4544-9f84-5ddcb7027cd2 fwd="219.113.146.130" dyno= connect= service= status=503 bytes= protocol=https
2019-11-26T05:06:44.823379+00:00 heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/favicon.ico" host=agile-meadow-66722.herokuapp.com request_id=c5d323a0-b4be-497c-847b-2d3ebf4942d0 fwd="219.113.146.130" dyno= connect= service= status=503 bytes= protocol=https

エラーコードの中身を読んでみると、code=H10でApp crashedとのこと。
ネットで調べてみたところ、以下のようにリスタートすれば直ることもあると情報があったため試しました。

$ heroku restart -app

もしくは

$ heroku restart -app application_name

しかし、こちらを試してもエラーのままでした。

コンソールを開いて詳細エラー情報を確認する。

エラーの詳細内容を確かめるために以下コードでHeroku上でコンソールを開きました。

$ heroku run rails c

すると、以下エラーコードが出てきました。

Error loading the 'sqlite3' Active Record adapter. Missing a gem it depends on? sqlite3 is not part of the bundle. Add it to your Gemfile. (LoadError)

sqlite3がバンドルされていない?Gemfileに加えろとの指示。
しかし、sqlite3は本番環境に対応していません。

どうやらrails newの時点でpostgresqlの指定を忘れていてsqlite3がデータベース作成時に指定されていたみたいです・・・

アプリ作成初期段階の凡ミスでエラーが発生することになりました。

SQLite3からPostgreSQLに変換

※参照
https://qiita.com/isotai/items/67bafc37a16ea5de5e3b

上記記事を参考にし、
datebase.ymlのsqlite3時点の記載内容を全て消し、
postgresql指定でrails newした場合の記載に変更しました。

gemfileに以下を追加しました。

gem 'pg', '>= 0.18', '< 2.0'

以上。

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

kaminariのlast_page?の動作に気をつけて!

主旨と結論

連番で管理するのではなく、「前へ」「次へ」ボタンを設置しての運用です。
記事のポイントは、データが入っていない状態でページネーション機能を使う際に気をつけること
常に何らかのデータが存在する場合は気にする必要はありません。

下記を実現したい場合に参考になります。

  • 最初のページは「前へ」ボタンを消す
  • 最後のページは「次へ」ボタンを消す
  • 最初で最後のページは両方とも消す←ここで次へ問題が発生します。

なので

当初問題のlast_page?の条件分岐は、

<% unless pagenates.last_page? %>
 <a href="<%= path_to_next_page(pagenates) %>">次のページ</a>
<% end %>

のようにしていました。
これだと、データがない状態では、最初で最後のページに次へボタンが表示されてしまうのです。

最終的には下記のようにすればOK

<% unless pagenates.last_page? || paenates.out_of_range %>
 <a href="<%= path_to_next_page(pagenates) %>">次のページ</a>
<% end %>

まとめ

  • last_page?は、現在ページと総ページ数が同じ時にtrueを返し、違う時にfalseを返す。
  • データが存在しない際は、表示されるページ数(current_page)は、1となるが、total_pagesは、0となる。
  • そのため、データが存在しない場合、表示されたページが最初で最後のページとなるものの条件分岐にはout_of_rangeを併用する必要がある。

最初で最後のページの戻り値は下記の通り

メソッド データあり データなし
current_page 1 1
total_pages 1 0
last_page? true false
first_page? true true
out_of_range false true

解説

結論に書きましたが、last_page?は、最後のページの場合trueを返す、ではありません。
last_page?は、現在ページと総ページ数が同じ時にtrueを返し、違う時にfalseを返すです。

kaminariのソースを確認します。

def last_page?
  current_page == total_page
end

これだけではよくわからないので、

  • current_pageメソッド
  • total_pageメソッド

を確認します。

current_pageメソッド

    def current_page
      offset_without_padding = offset_value
      offset_without_padding -= @_padding if defined?(@_padding) && @_padding
      offset_without_padding = 0 if offset_without_padding < 0

      (offset_without_padding / limit_value) + 1
    rescue ZeroDivisionError
      raise ZeroPerPageOperation, "Current page was incalculable. Perhaps you called .per(0)?"
    end

これは、offset_without_paddingの値が0になるのでしょうか。
データがないのでpaddingなど出ないはずです。
最後には、

(offset_without_padding / limit_value) + 1

として、0をlimit_valueで割って+1しているので、基本的に戻り値は1以上であることが想像できます。

そして、下記の実行結果を確認すると、1とでていましたのでよしとしましょう。
お分かりの方コメント頂けると幸いです、、、、!!!

<%= @users.current_page %>

total_pagesメソッド

    def total_pages
      count_without_padding = total_count
      count_without_padding -= @_padding if defined?(@_padding) && @_padding
      count_without_padding = 0 if count_without_padding < 0

      total_pages_count = (count_without_padding.to_f / limit_value).ceil
      max_pages && (max_pages < total_pages_count) ? max_pages : total_pages_count
    rescue FloatDomainError
      raise ZeroPerPageOperation, "The number of total pages was incalculable. Perhaps you called .per(0)?"
    end

これも
count_without_paddingが0になるのはわかります。
あとは単純な計算とif文ですね。
戻り値がtotal_pages_countになるのはわかりますので、答えは0。(お分かりの方コメンry...)

そして、下記の結果は0でした。

<%= @users.total_pages %>

まとめ、last_page?メソッドとout_of_rangeメソッド

def last_page?
  current_page == total_page
end

データがない状態では、

  • current_pageは1
  • total_pageは0

となりますので、最初で最後のページで、last_page?を使って条件分岐してしまうと、falseが返ってきます。

そこで登場するのが

  • out_of_rangeメソッド

これは

    def out_of_range?
      current_page > total_pages
    end

なので、データがない状態ではtrueが返ってきます。
現在ページの方が合計ページを上回ることって普通ないと思うのですが、今回のようなケースを想定していたのでしょうか。

git_hub、紹介したメソッドが定義されているページ

Git_Hub/kaminari
https://github.com/kaminari/kaminari/blob/c5186f5d9b7f23299d115408e62047447fd3189d/kaminari-core/lib/kaminari/models/page_scope_methods.rb

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

Ruby on rails記事まとめ

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

※Ruby on rails記事まとめ

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

RubyMineをさらに便利にするプラグインたち:後編

はじめに

Ateam cyma Adevent Calendar 2019 の 3日目です。
本日の担当は昨日に引き続き、エイチームのEC事業本部でWebアプリケーションエンジニアをしている@hibiheionです。

この記事はRubyMineをより便利にするプラグインたち:前編の続きです。
後編では、あると便利なプラグインを紹介します。

紹介内容について

前編と同じ内容ですが、紹介方法について改めて記載しておきます。

  • 各プラグインの基本機能のみ説明しています。詳細は各プラグインのWebページをご確認ください。
  • 特定の言語やサービスを使えるようにするものは対象外としています。
  • 個人的な評価を三段階でつけています。
    • 優:ないと困る
    • 良:使用頻度が多め
    • 可:使用頻度が少なめ
  • ショートカットはキーマップ次第で変わるのでメニューからの操作で説明しています。なお、メニューから操作するとショートカットキーを教えてくれるプラグインもあります。
  • プラグインの情報は記事を公開した2019年12月時点のものです。

プラグインの紹介

一覧

全部で20個あります。
順序は評価と名称の順です。
後編では「AceJump」から「SQL Formatter」の11個を説明します。
残りのプラグインは前編で説明しています。

名称 概要 評価
Grep Console tail -fで出力したログをGrep検索で絞り込める
highlightbracketpair カーソルがある箇所を囲んでいるカッコをハイライト表示する
IdeaVim Vimと同じような操作で使用できるようにする
Key Promoter X ショートカットキーが存在する操作をマウス等から実行した際に操作内容をショートカットキーとともに記録する
Quick File Preview ファイル一覧で選択したファイルのプレビューを表示する
Railways Railsのルーティングの一覧を表示する
Rainbow Brackets 対になるカッコを色分けして表示する
Scroll From Source Projectツールウィンドウ(ファイル一覧)の開いているファイルの場所に移動する
Tabdir タブ一覧で同じ名前のファイルにはディレクトリ名を表示する
AceJump ページ内の指定した文字にカーソルを移動する
Git Scope ファイルの一覧からGitの差分を確認できる
GitToolBox Git連携の機能を強化する
MultiHighlight 複数の検索結果を異なった色でハイライト表示する
String Manipulation 色々な方法で文字列を操作する
BrowseWordAtCaret カーソルがあたっている単語をファイル内検索する
JSON Viewer JSONを見やすい形に整形する
Open in splitted tab 参照先を分割したウィンドウで表示する
Pipe Table Formatter パイプ(|)を使ったテーブルを揃える
Realigner 指定した文字で複数行の連結、複数行への分割、文字列の囲い込みができる
SQL Formatter SQLを見やすい形に整形する

AceJump

https://plugins.jetbrains.com/plugin/7086-acejump/

機能

  • ページ内の指定した文字にカーソルを移動する
    • 例えば、ページ内の「A」という文字がある場所に少ないタイプ数で移動できる
  • 効率よくカーソルを移動できる
  • 使用例
    「d」をタイプすると下記のように「d」の位置にナビゲーションが表示され、この状態で「j」をタイプすると「def」にカーソルを移動する ace_jump.png

使い方

説明のために機能の検索から実行していますが、ショートカットキーを割り当てて使うのが現実的です。

  • 指定した文字に移動
    1. shiftキーを2回押して表示される検索ウィンドウで「acejump」を検索する
    2. 検索結果から「Activate AceJump mode」を選択する
    3. 移動したい文字(1文字)をキーボードからタイプする
    4. ページ内のタイプした文字の左にナビゲーションが出るので、移動したい先のアルファベットをキーボードからタイプする
  • 指定した行に移動
    1. shiftキーを2回押して表示される検索ウィンドウで「acejump」を検索する
    2. 検索結果から「Display Line Markers」を選択する
    3. ページ内の各行にナビゲーションが出るので、移動したい先のアルファベットをキーボードからタイプする

補足

  • 機能を限定したAceJump-Liteというプラグインもある
    • 個人的にはAceJump-Liteのほうがナビゲーションが見やすいのでこちらを使っている

Git Scope

https://plugins.jetbrains.com/plugin/10083-git-scope/

機能

  • ファイルの一覧からGitの差分を確認できる
    • 内容はトップメニューの「VCS→Git→Commit file…」で出てくる内容と同じ
  • Git Scopeはサイドメニューに出せるため確認しやすい

使い方

  • ツールウィンドウの表示
    • サイドメニューから「Git Scope」のツールウィンドウを開く
    • サイドメニューに出ていない場合、トップメニューの「View→Tool Windows→Git Scope」から表示できる
  • ツールウィンドウの操作
    • ツールウィンドウには差分のあるファイルの一覧が表示される
    • ファイル名をクリックすると差分を確認できる

GitToolBox

https://plugins.jetbrains.com/plugin/7499-gittoolbox/

機能

  • Git連携の機能を拡張する
  • 次のような機能が増える
    • ステータスバーに出す情報を増やす
      • ステータスの表示
      • 今いる行の最終更新者の表示(git blameの情報)
    • blame(更新情報)が見れるようになる
    • ブランチにタグをつけられる
    • Gitのリモートリポジトリを定期的に自動でフェッチし、変更があれば通知する

使い方

  • プラグインの設定の変更
    • 設定画面で「Other Settings→GitToolBox Global」を開く
      • 自動フェッチ以外の表示項目などの設定
    • 設定画面で「Other Settings→GitToolBox Project」を開く
      • プロジェクト別の自動フェッチの設定
  • git blameの確認
    • 右クリックメニューの「Git→show inline blame」にチェックを入れると、カーソルが当たっている行のblameをつねに表示する(意外と邪魔にならない)
    • 右クリックメニューの「Git→show blame details」でカーソルのある行の詳細なblameをポップアップで表示する
      • ポップアップからコミット履歴の確認やリビジョンのコピーなどができる
  • ブランチへのタグの追加
    • 右クリックメニューの「Git→repository→Push Tags on Branch」を選択する

MultiHighlight

https://plugins.jetbrains.com/plugin/9511-multihighlight/

機能

  • 複数の検索結果を異なった色でハイライト表示する
  • 込み入ったコードを読まないといけないときにあると便利
  • 使用例 multihighlight.png

使い方

説明のために機能の検索から実行していますが、ショートカットキーを割り当てて使うのが現実的です。

  • ハイライト表示の切り替え
    1. 対象の文字列を選択する
    2. shiftキーを2回押して表示される検索ウィンドウで「multihighlight」を検索する
    3. 検索結果から「MultiHighlight: toggle highlight」を選択する
  • ハイライト表示の全クリア
    1. shiftキーを2回押して表示される検索ウィンドウで「multihighlight」を検索する
    2. 検索結果から「MultiHighlight: clear highlights in current editor」を選択する

String Manipulation

https://plugins.jetbrains.com/plugin/2162-string-manipulation/

機能

  • 色々な方法で文字列を操作する
  • 操作内容は盛りだくさん
    • フォーマット(JSON等)ごとに文字列をエスケープする
    • エンコードする(URLエンコード等)
    • スネークケース・キャメルケースといった記述方式を変換する
    • 数値に対して値の加減算などをの計算を行う
    • 複数の行を並び替える
    • 条件を指定して不要な行を削除する
    • 余分な半角スペースを取り除く(トリム)
    • 複数の行の左端や右端を揃える

使い方

  • 文字列の操作方法
    1. 操作対象の文字列を選択する
    2. 右クリックメニューの「String Manupilation」を選択する
  • プラグインの設定の変更
    • 設定画面で「Other Settings→String Manipulation」を開く

BrowseWordAtCaret

https://plugins.jetbrains.com/plugin/201-browsewordatcaret/

機能

  • カーソルがあたっている単語をファイル内検索する
    • 通常のファイル内検索でwordsにチェックを入れて検索したときの検索方法を使う
  • IdeaVimを使っていると、「*」の検索で同じことができる
  • 通常のファイル内検索より少ない手数で使用できる

使い方

説明のために機能の検索から実行していますが、ショートカットキーを割り当てて使うのが現実的です。

  • 下方向に検索
    1. 検索したい単語にカーソルを合わせる
    2. shiftキーを2回押して表示される検索ウィンドウで「browse to」を検索する
    3. 検索結果から「browse to next word」を選択する
  • 上方向に検索
    1. 検索したい単語にカーソルを合わせる
    2. shiftキーを2回押して表示される検索ウィンドウで「browse to」を検索する
    3. 検索結果から「browse to previous word」を選択する

JSON Viewer

https://plugins.jetbrains.com/plugin/9679-json-viewer/

機能

  • JSONを見やすい形に整形する
  • レスポンスの解析などで使う

使い方

  • ツールウィンドウの表示
    • サイドメニューから「JSON Viewer」のツールウィンドウを開く
    • サイドメニューに出ていない場合、トップメニューの「View→Tool Windows→JSON Viewer」から表示できる
  • JSONの整形
    1. JSONをコピーし、ツールウィンドウにペーストする
    2. ツールウィンドウの「Format」ボタンをクリックする

Open in splitted tab

https://plugins.jetbrains.com/plugin/7407-open-in-splitted-tab/

機能

  • 参照先を分割したウィンドウで表示する

使い方

  1. ジャンプしたいメソッド等にカーソルを合わせる
  2. 右クリックメニューの「Go To→Open in split tab」を選択する

Pipe Table Formatter

https://plugins.jetbrains.com/plugin/7550-pipe-table-formatter/

機能

  • パイプ(|)を使ったテーブルを揃える
    • RubyではCucumberやTurnipのテストフィーチャ(Gherkin)のテーブルで使う
  • ただ、日本語などのマルチバイト文字が含まれていると揃って見えない
  • 操作例
    • 操作前
      pipe_before.png
    • 操作後
      pipe_after.png

使い方

  • カーソルが当たっているテーブルを揃える
    1. 対象のテーブルにカーソルを合わせる
    2. 右クリックメニューの「Pipe Table→Format」を選択する
  • ファイル内のすべてのテーブルを揃える
    1. 対象ファイルを開く
    2. 右クリックメニューの「Pipe Table→Format All」を選択する
  • テーブルに列を追加する
    1. 列を追加したい位置の後ろにカーソルを移動する
    2. 右クリックメニューの「Pipe Table→Add Column Before」を選択する
  • テーブルを選択する
    1. 対象のテーブルにカーソルを合わせる
    2. 右クリックメニューの「Pipe Table→Select Table」を選択する

Realigner

https://plugins.jetbrains.com/plugin/7082-realigner/

機能

  • 指定した文字で複数行の連結、複数行への分割、文字列の囲い込みができる
    • 例えば、ハッシュの項目が増えたときに「,」で複数行に分けられる
    • 分割や結合に使う文字は決められる

使い方

  • 文字列の分割
    1. 分割する文字列を選択する
    2. トップメニューの「Edit→Split into Lines」を選択する
    3. 分割に使う文字などを選択する
  • 文字列の結合
    1. 結合する文字列を選択する
    2. トップメニューの「Edit→Join Lines With Glue」を選択する
    3. 結合に使う文字などを選択する
  • 文字列の囲い込み
    1. 囲む文字列を選択する
    2. トップメニューの「Edit→Wrap」を選択する
    3. 囲む文字などを選択する
      • 開始文字と終了文字で別の文字を指定できる

SQL Formatter

https://plugins.jetbrains.com/plugin/10858-sql-formatter/

機能

  • SQLを見やすい形に整形する
  • ログに出ているSQLをコピペしてSQLを確認できる

使い方

  • ツールウィンドウの表示
    • サイドメニューから「SQL Formatter」のツールウィンドウを開く
    • サイドメニューに出ていない場合、トップメニューの「View→Tool Windows→SQL Formatter」から表示できる
  • SQLの整形
    1. SQLをコピーし、ツールウィンドウにペーストする
    2. ツールウィンドウの「Format」ボタンをクリックする

まとめ

RubyMineのプラグインは以下のサイトで検索できます。
https://plugins.jetbrains.com/search?products=ruby

メジャーなプラグインで出てきていないものもありますが、それは私の使い方に合わなかったためです。
例えば、前編で紹介したIdeaVimはVimmer以外には不要なものですよね。
時間があるときにでも探してみれば、自分にあったプラグインがきっと見つかると思います。

最後にプラグインの開発者の皆様に対して感謝を述べて締めたいと思います。
プラグインを作ってくださってありがとうございます!

次回予告

Ateam cyma Adevent Calendar 2019 の 3日目の記事は以上です。
2回にわたりお付き合いいただきありがとうございました。

4日目はエンジニアの@bayasistさんが担当します。
Railsのフォームオブジェクトに関する記事です。

さいごに

株式会社エイチームでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。

エンジニアで興味を持った方はcymaのQiita Jobsをご覧ください。

そのほかの職種は、エイチームグループ採用サイトをご覧ください。

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

RubyMineをさらに便利にするプラグインたち:前編

はじめに

Ateam cyma Adevent Calendar 2019 の 2日目です。
本日の担当はエイチームのEC事業本部でWebアプリケーションエンジニアをしている@hibiheionです。
業務では主に自転車ECサイトcymaのバックエンドの機能をRailsで書いています。

本題

みなさんはエディタは何を使ってますか?
私は業務ではRailsを触っているのですが、エディタはRubyMineを使っています。
RubyMineは有料なだけあってRailsでの開発をとても効率よくできるようになっています。
RubyMineは他のエディタと同じようにプラグインで機能を拡張することも可能です。
ただ、RubyMineは他のエディタと比べると日本語でプラグインの情報が見つかりにくい気がします。

そんなわけで、RubyMineのプラグインの情報をまとめれば役に立つのではないかと思ったので、自分が使ってみて良かったと感じたプラグインのざっくりとした使い方を紹介します。
この記事でRubyMineを使った開発がより便利になれば幸いです。

なお、紹介するプラグインの多くはPHPStorm等の他のJetBrains製品でも利用できます。

紹介内容について

  • 各プラグインの基本機能のみ説明しています。詳細は各プラグインのWebページをご確認ください。
  • 特定の言語やサービスを使えるようにするものは対象外としています。
  • 個人的な評価を三段階でつけています。
    • 優:ないと困る
    • 良:使用頻度が多め
    • 可:使用頻度が少なめ
  • ショートカットはキーマップ次第で変わるのでメニューからの操作で説明しています。なお、メニューから操作するとショートカットキーを教えてくれるプラグインもあります。
  • プラグインの情報は記事を公開した2019年12月時点のものです。

プラグインのインストール方法

プラグインの紹介の前に念のためプラグインのインストール方法を紹介しておきます。

  1. 設定画面を開く
    • Macではトップメニューの「RubyMine→Preferrences」を選択する
    • Windowsではトップメニューの「File→Settings」を選択する
  2. 「Plugins」を開く
  3. 「Marketplace」から検索してインストールする

プラグインの紹介

一覧

全部で20個あります。
順序は評価と名称の順です。
記事が長くなったため、前後編の2回に分割しました。
今回は個人的には手放せない「Grep Console」から「Tabdir」の9個を紹介します。

名称 概要 評価
Grep Console tail -fで出力したログをGrep検索で絞り込める
HighlightBracketPair カーソルがある箇所を囲んでいるカッコをハイライト表示する
IdeaVim Vimと同じような操作で使用できるようにする
Key Promoter X ショートカットキーが存在する操作をマウス等から実行した際に操作内容をショートカットキーとともに記録する
Quick File Preview ファイル一覧で選択したファイルのプレビューを表示する
Railways Railsのルーティングの一覧を表示する
Rainbow Brackets 対になるカッコを色分けして表示する
Scroll From Source ファイル一覧で開いているファイルの場所に移動する
Tabdir タブ一覧で同じ名前のファイルにはディレクトリ名を表示する
AceJump ページ内の指定した文字にカーソルを移動する
Git Scope ファイルの一覧からGitの差分を確認できる
GitToolBox Git連携の機能を強化する
MultiHighlight 複数の検索結果をハイライト表示する
String Manipulation 色々な方法で文字列を操作する
BrowseWordAtCaret カーソルがあたっている単語をファイル内検索する
JSON Viewer JSONを見やすい形に整形する
Open in splitted tab 参照先を分割したウィンドウで表示する
Pipe Table Formatter パイプ(|)を使ったテーブルを揃える
Realigner 指定した文字で複数行の連結、複数行への分割、文字列の囲い込みができる
SQL Formatter SQLを見やすい形に整形する

GrepConsole

https://plugins.jetbrains.com/plugin/7125-grep-console/

機能

  • tail -fで出力したログをツールウィンドウに表示する
    • tail -fで出力するログをGrep検索で絞り込める
    • 設定に基づいてログをハイライト表示する
      • Rails用、SQL用など用途に応じた設定が可能
      • 例えば、Rails用ではFATALを赤くする、SQL用ではINSERTを赤くする、といったことができる
      • 設定内容は慣れないとわかりにくい
  • 設定に基づいてファイルをハイライト表示する
    • 基本的にログファイルを対象に使う
  • 確認したいログを見つけやすくなる

使い方

  • tailログに関する機能
    • tailログの開始
      • トップメニューの「Tools→Tail Fail in Console」もしくは「Tools→Tail Current File in Console」を選択する
        • エディタで開いているファイルのログを出力するときに後者を使う
    • tailログの絞り込み
      1. ログ出力のツールウィンドウで右クリックする
      2. 右クリックメニューの「Grep」を選択する
      3. Grep用の表示に切り替わるので、「Expression」に条件を入力する
        • この状態でログが増えると、条件に該当するログだけが表示される
        • 「RELOAD」ボタンをクリックすると、出力済みのログから条件に該当するものだけを絞り込む
    • tailログのハイライト表示
      1. ログ出力のツールウィンドウで右クリックする
      2. 右クリックメニューの「Show Grep Console Statistics in Console」を選択する
      3. 設定用のウィンドウが表示されるので設定を行い、「OK」をクリックする
  • ファイルに関する機能
    • ファイルのハイライト表示
      1. 対象のファイルを開いている状態で、トップメニューの「Tools→Highlight Editor According to Grep Console Settings」を選択する
      2. 設定用のウィンドウが表示されるので設定を行い、「OK」をクリックする
    • ファイルのハイライト表示の解除
      • ハイライト表示した状態で、トップメニューの「Tools→Clear Grep Highlight in Editor」を選択する

HighlightBracketPair

https://plugins.jetbrains.com/plugin/10465-highlightbracketpair/

機能

  • カーソルがある箇所を囲んでいるカッコをハイライト表示する
  • ソースコードが読みやすくなる
  • 表示例
    • このプラグインを入れていない場合
      highlight_bracket_pair_before.png
    • このプラグインを入れている場合
      highlight_bracket_pair_after.png

使い方

  • プラグインを有効にするだけ

IdeaVim

https://plugins.jetbrains.com/plugin/164-ideavim/

機能

  • Vimと同じような操作で使用できるようにする
    • 私はVimからRubyMineに乗り換えたのですが、さほど違和感なく使用できています
  • RubyMineの機能も同時に使用できる
    • IdeaVimの状態がノーマルモードでも挿入モードでも動作する
  • IdeaVimがCtrlキーを使うため、RubyMineのキーマップの調整が必要
    • Macはまだしも、Windowsだと影響が大きい

使い方

  • ON・OFFの切り替え
    • トップメニューの「Tools→Vim Emulator」でON・OFFを切り替えられる
  • プラグインの設定の変更
    • 本家Vimの「.vimrc」と同じように、「.ideavimrc」というファイルで設定を変更できる
  • RubyMineと重複しているキーマップの確認
    • 設定画面の「Editor→Vim Emulation」からRubyMineと重複しているキーマップを確認できる
    • Vim Emulatorが有効な状態で確認する必要がある

Key Promoter X

https://plugins.jetbrains.com/plugin/9792-key-promoter-x/

機能

  • ショートカットキーが存在する操作をマウス等から実行した際に操作内容をショートカットキーとともに記録する
  • ショートカットキーの存在の気づくことができるため、ショートカットキーを覚えやすくなる

使い方

  • 操作の記録
    • プラグインを有効にするだけ
  • 操作履歴の確認
    • サイドメニューから「Key Promoter X」のツールウィンドウを開く
    • サイドメニューに出ていない場合、トップメニューの「View→Tool Windows→Key Promoter X」から表示できる

Quick File Preview

https://plugins.jetbrains.com/plugin/12778-quick-file-preview/

機能

  • Projectツールウィンドウ(ファイル一覧)で選択したファイルのプレビューを表示する
    • プレビューといっても普通にファイルを開いたときとの違いはほとんどなく、プレビューの状態でも内容を変更できる
    • 普通にファイルを開いたときと違い、Projectツールウィンドウで他のファイルに移動したときにプレビューは自動的に閉じられる
  • 複数のファイルを辿っていくときに便利

使い方

  • 他の設定の影響を受けるため、以下の事前準備が必要
    1. Projectツールウィンドウの設定を開く
    2. 「AutoScroll to Source」を無効にする
  • プレビューの表示
    • Projectツールウィンドウで対象ファイルをクリックする
      • ダブルクリックしたときに正式にファイルを開く
    • プラグインの設定によって変わるため、これは初期設定の場合の動作
  • プラグインの設定の変更
    • 設定画面で「Editor→General→Quick File Preview」を開く

Railways

https://plugins.jetbrains.com/plugin/7110-railways/

機能

  • Railsのルーティングの一覧を表示する
    • ルーティングを検索できる
    • 選択したルーティングのアクションに移動できる
  • ルーティングが把握しやすくなる

使い方

  • サイドメニューから「Routes」のツールウィンドウを開く
  • サイドメニューに出ていない場合、トップメニューの「View→Tool Windows→Routes」から表示できる

Rainbow Brackets

https://plugins.jetbrains.com/plugin/10080-rainbow-brackets/

機能

  • 対になるカッコを色分けして表示する
    • Rubyでは()[]{}が対象
    • HTMLではペアになるタグの<>を同じ色で表示する
  • ソースコードが読みやすくなる
  • 表示例
    • このプラグインを入れていない場合
      rainbow_brackets_before.png
    • このプラグインを入れている場合
      rainbow_brackets_after.png

使い方

  • カッコの色分け
    • プラグインを有効にするだけ
  • プラグインの設定の変更
    • 設定画面で「Other Settings→Rainbow Brackets」を開く

Scroll From Source

https://plugins.jetbrains.com/plugin/7606-scroll-from-source/

機能

  • Projectツールウィンドウ(ファイル一覧)で開いているファイルの場所に移動する
  • 他の方法でも開いているファイルに移動できるが、そちらの方法よりショートカットが割り当てられる・任意のタイミングで実行できるという利点がある
    • ナビゲーションバー(パンくずリスト)から「Jump To Source」したときと違い、ショートカットキーが割り当てられる
    • Projectツールウィンドウの設定で「Autoscroll from Source」を有効にした場合はファイルを開いたときに自動で移動するが、このプラグインでは必要なときにだけ移動できる

使い方

  1. ファイルを開いている状態で右クリックする
  2. 右クリックメニューの「Scroll From Source」を選択する

Tabdir

https://plugins.jetbrains.com/plugin/5045-tabdir/

機能

  • タブ一覧で同じ名前のものが存在するファイルにはディレクトリ名を表示する
    • RubyMineの標準機能だと同じ名前のファイルがタブ一覧にないとディレクトリ名が出ないが、このプラグインでは同じ名前のファイルがタブ一覧になくても良い
  • Railsではどうしても同じ名前のファイルが増えるので、このプラグインがあればファイルを見分けやすくなる
  • 表示例
    • このプラグインを入れていない場合
      tabdir_before.png
    • このプラグインを入れている場合
      tabdir_after.png

使い方

  • 他の設定の影響を受けるため、以下の事前準備が必要
    1. 設定画面で「Editor→General→Editor Tabs」を開く
    2. 「Show directory for non-unique file names」のチェックを外す
  • ディレクトリ名の表示
    • プラグインを有効にするだけ
  • プラグインの設定の変更
    • 設定画面で「Other Settings→Tabdir」を開く

次回予告

Ateam cyma Adevent Calendar 2019 の 2日目の記事は以上です。

3日目は引き続き私@hibiheionが担当します。
内容はもちろん「RubyMineをより便利にするプラグインたち:後編」です。
今回紹介しきれなかった残りの11個のプラグインを紹介します。

さいごに

株式会社エイチームでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。

エンジニアで興味を持った方はcymaのQiita Jobsをご覧ください。

そのほかの職種は、エイチームグループ採用サイトをご覧ください。

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

【Rails】Guardによるテスト自動化の設定【Rails Tutorial 3章まとめ】

GemfileにGuardを導入

group :test do
  gem 'rails-controller-testing', '1.0.2'
  gem 'minitest',                 '5.10.3'
  gem 'minitest-reporters',       '1.1.14'
  gem 'guard',                    '2.13.0'
  gem 'guard-minitest',           '2.4.4'
end

テストグループ内に導入する。
minitestも一緒に入れとくと良さげ。

Guardの初期化とGuardfileの編集

$ bundle exec guard init

初期化するとルートライブラリにGuardfileが生成される。

Guardfile.rb
# Guardのマッチング規則を定義
guard :minitest, spring: "bin/rails test", all_on_start: false do
  watch(%r{^test/(.*)/?(.*)_test\.rb$})
  watch('test/test_helper.rb') { 'test' }
  watch('config/routes.rb')    { integration_tests }
  watch(%r{^app/models/(.*?)\.rb$}) do |matches|
    "test/models/#{matches[1]}_test.rb"
  end
  watch(%r{^app/controllers/(.*?)_controller\.rb$}) do |matches|
    resource_tests(matches[1])
  end
  watch(%r{^app/views/([^/]*?)/.*\.html\.erb$}) do |matches|
    ["test/controllers/#{matches[1]}_controller_test.rb"] +
    integration_tests(matches[1])
  end
  watch(%r{^app/helpers/(.*?)_helper\.rb$}) do |matches|
    integration_tests(matches[1])
  end
  watch('app/views/layouts/application.html.erb') do
    'test/integration/site_layout_test.rb'
  end
  watch('app/helpers/sessions_helper.rb') do
    integration_tests << 'test/helpers/sessions_helper_test.rb'
  end
  watch('app/controllers/sessions_controller.rb') do
    ['test/controllers/sessions_controller_test.rb',
     'test/integration/users_login_test.rb']
  end
  watch('app/controllers/account_activations_controller.rb') do
    'test/integration/users_signup_test.rb'
  end
  watch(%r{app/views/users/*}) do
    resource_tests('users') +
    ['test/integration/microposts_interface_test.rb']
  end
end

# 与えられたリソースに対応する統合テストを返す
def integration_tests(resource = :all)
  if resource == :all
    Dir["test/integration/*"]  else
    Dir["test/integration/#{resource}_*.rb"]
  end
end

# 与えられたリソースに対応するコントローラのテストを返す
def controller_test(resource)
  "test/controllers/#{resource}_controller_test.rb"
end

# 与えられたリソースに対応するすべてのテストを返す
def resource_tests(resource)
  integration_tests(resource) << controller_test(resource)
end

springサーバーを使用するようにマッチング規則(?)を書き換える。

gitignoreの編集

# Ignore Spring files.
/spring/*.pid

gitとspringが競合するらしい(意味はわかってない)ので、gitignoreに上の二行を記述。

Guardを起動

bundle exec guard

コンソールを新規に開いてGuardを起動。
Returnキーで手動実行できる。
Ctrl+Dで終了する。

デスクトップに結果を通知

こちらの記事を参考にさせていただきました。
「Mac OSX El CapitanでGuardからテスト結果の通知を受け取ってデスクトップに表示する」
http://b0npu.hatenablog.com/entry/2016/04/17/155457

$brew search terminal-notifier
$brew install terminal-notifier

Homebrewでterminal-notifierをインストール。

Gemfile.rb
group :development do
  gem 'terminal-notifier-guard', '~> 1.6.1'
end

deveropmentグループにterminal-notifier-guardを導入して、bundle install。
Guardを起動して適当にファイルを変更すると...

スクリーンショット 2019-11-26 21.33.02.jpg

ちゃんとデスクトップ通知が来た🐳

springのkill

テストが重くなってきたら、プロセスを確認してspringをkillする。

  $ ps aux | grep spring
    ec2-user 12241 0.3 0.5 589960 178416 ? Ssl Sep20 1:46
    spring app | sample_app | started 7 hours ago
  $ kill -15 12241

  $ pkill -15 -f spring

最初の数字(プロセスid、pid)で指定する。
spring関係を一括でkillするにはpkillを使う。

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

Ruby on railsの基本箇所の復習②(rake db:create)

はじめに

引き続きrailsでアプリを作成する手順を開設します。

実行

アプリのフォルダを作ったら、アプリのディレクトリに移動してrails sというコマンドを入力するとアプリが起動します。
…と言いたいところですが、この時点ではアプリに対応するデータベースが存在していないので、エラーが発生してしまいます。
これを解決するには、データベースを作ります。
データベースの作成は次のコマンドをターミナルで実行すれば完了します。

rake db:create

そして次のようにコマンドを実行すればアプリが実行します。

rails s
(前略)
* Environment: development
* Listening on tcp://localhost:3000
Use Ctrl-C to stop

という風に表示されたら成功です。

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

Ruby on railsの基本箇所についての復習②(rake db:create)

はじめに

引き続きrailsでアプリを作成する手順を開設します。

実行

アプリのフォルダを作ったら、アプリのディレクトリに移動してrails sというコマンドを入力するとアプリが起動します。
…と言いたいところですが、この時点ではアプリに対応するデータベースが存在していないので、エラーが発生してしまいます。
これを解決するには、データベースを作ります。
データベースの作成は次のコマンドをターミナルで実行すれば完了します。

rake db:create

そして次のようにコマンドを実行すればアプリが実行します。

rails s
(前略)
* Environment: development
* Listening on tcp://localhost:3000
Use Ctrl-C to stop

という風に表示されたら成功です。

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

RSpecのlet(let!)とは

letとは

  • RSpecのメソッドのひとつ
  • letは呼ばれた時に初めてデータを読み込むという遅延読み込みをするメソッド。letはboforeブロックの外部で呼ばれるため、セットアップのコードを減らすことが可能。
  • before ブロックの場合では各dscribeやcontextの前に毎回呼び出される。これはテストに予期しない影響を及ぼす恐れがある。また、不要なデータ作成をしてテスト実行を遅くする原因になることもある。

beforeブロックとの違い

  • letは遅延読み込み
  • letはインスタンス変数に格納しない

let!

  • let!は遅延読み込みされないlet
  • すべてのテストが余分なデータを持つ可能性がある

letとlet!,beforeブロックの使い分け

  • 決まった定義はないがテストはDRYを意識するあまり読みにくくなってしまったら本末転倒。
  • よって、読みやすいと思った方を使用していく。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

APIキー・環境変数を設定したのにうまく反映されない時には.....

概要

メルカリのクローンサイトを作成する際にインフラ、デプロイ関係の担当をした者が沼にハマった部分を共有し、同じような状態から抜け出すための一助になればと思ったことが投稿するきっかけになります。
今回は、API関連のキーや環境変数を設定したのに本番環境では上手く挙動しない際に対処した方法をいくつか紹介します!
基本的なことから盲点な部分まで、同じような状況で困っている方の一助に少しでもなれたら幸いです。

環境

  • Rails v5.2.3
  • Ruby v2.5.1
  • Unicorn
  • capistrano
  • nginx
  • mysql
  • AWS, EC2(本番環境)

①まずはスペルミス、スペース(空白)の確認

最初から言われなくても分かるよっていう内容ですいません。
しかし、スペルミスは皆さん気をつけていることだとは思いますが、意外と足を引っ張るのはスペース(空白)の確認です。

(例)
AWS_SECRET_ACCESS_KEY = '************************'
=> スペースあり
AWS_SECRET_ACCESS_KEY='************************'
=> スペースなし

上記の微妙な違いで、内容は正確だがキーや環境変数が反映されない状態に陥ってしまいます。
まずは初心に戻った気持ちでスペース(空白)のチェックをしてみるのも良いかもしれません。

②設定を反映させるためには...

スペルミスやスペースもなく設定が完了。以上終わり。 ではないのです。
設定した内容を反映させるためにはAWS(EC2)であれば、一度EC2上からログアウトし再度ログインすることによって設定が反映されます。
ただし、 .bash_profile などに設定をし直ぐに反映をさせたい場合は以下のコマンドを打つことによって反映されます。

source ~/.bash_profile

設定ファイル名は任意のもので大丈夫です。
また、設定ファイルの違いって案外知らないと思うので(自分は笑)、気になる方は下記にまとめたので見てみてください!

読み込み順 設定ファイル 概要
/etc/profile ログインする全ユーザー共通設定
2 ~/.bash_profile ※下記に概要記載
3 ~/.bashrc bash起動時に読み込ませたい設定
4 /etc/bashrc システム全体の関数等の設定

※ ~/.bash_profileに関しては、~/.bash_login, ~/.profileと順に、上のファイルが無ければ下のファイルが読み込まれる流れになります。

③capistrano(自動デプロイ)導入後に設定が反映されない時

今回の投稿で一番伝えたかった内容になります。なぜかと言いますと...自身が沼にハマったからです。笑
スペルミス、余計なスペースも無い、EC2からログアウト・再ログインして設定の反映もバッチリ。さあ、後はcapistranoを動かして挙動を見に行きますか.....? んん? あれ、反映されていない。(白目)
この状況で沼にハマり、かれこれ2~3時間。 やっと見つけた救世主のコマンドを紹介します。(※今回はcapistranoを使用しています。)

bundle exec cap -t production unicorn:stop
=> unicornの停止。これを打たないとコードが反映されていない状況に。
bundle exec cap production deploy
=>再度capistranoで自動デプロイの実行。これで反映。

この2つのコマンドを実行することによって、本番環境でAPIを利用したり、しっかりと環境変数も用いることができていました。

終わりに

インフラ関連に対する苦手意識を持っていましたが、何事も根拠があって、それを理解することによって物事の流れを掴むことができ、やりがいに繋がっていくと感じます。僕もまだまだこれからですが、また沼ったことを共有できればと考えています。長々と見て頂きありがとうございました。

参照

https://qiita.com/yunzeroin/items/480a3a677f78a57ac52f

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

【Rails】provideとyieldを使ったページタイトルの設定【Rails Tutorial 3章まとめ】

各ページのビューにprovideを記述

app/views/static_pages/home.html.erb
<% provide(:title, "Home") %>
<h1>Sample App</h1>

appilication.htmlのtitleにyiledを記述

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
    .
    .
    .
  </head>

  <body>
    <%= yield %>
  </body>
</html>

ページタイトルに対するテスト

test/controllers/static_pages_controller_test.rb
  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

assert_selectを使って、titlesセレクタがページタイトルと一致することを確認する。
ページタイトルの重複部分はsetup内の変数@base_titleに入れておく。

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

【Rails】provideとyieldを使ったページタイトルの設定【Rails Tutorial 3・4章まとめ】

各ページのビューにprovideを記述

app/views/static_pages/home.html.erb
<% provide(:title, "Home") %>
<h1>Sample App</h1>

appilication.htmlのtitleにyieldを記述

app/views/layouts/application.html.erb
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
  </head>

ページタイトルに対するテスト

test/controllers/static_pages_controller_test.rb
  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

assert_selectを使って、titlesセレクタがページタイトルと一致することを確認する。
ページタイトルの重複部分はsetup内の変数@base_titleに入れておく。

~ここから4章~

full_titleヘルパーを作成

rootであるhomeビューでは、アプリケーション名のみをタイトルに表示したい。
そのため、homeビューではprovideは不要である。
しかし、この場合タイトルが「 | Ruby on Rails Tutorial Sample App」となり、余計な|が入る。
これを修正するためにページタイトルを返すfull_titleヘルパーを作成する。

app/helpers/application_helper.rb
module ApplicationHelper

  # ページごとの完全なタイトルを返す。
  def full_title(page_title = "")
    base_title = "Ruby on Rails Tutorial Sample App"
    if page_title.empty?
      base_title
    else
      page_title + " | " + base_title
    end
  end

end
app/views/layouts/application.html.erb
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
  </head>

変数page_titleの初期値を空にしておき、provideからくる引数yield(:title)が与えられなければ、base_titleのみを表示する。
与えられれば、|を追加して、完全なタイトルを表示する。
(ちなみに、empty?メソッドの末尾の?は、論理値を返すメソッドであることを意味する)

homeビューからはprovideを消しておく。

app/views/static_pages/home.html.erb
<h1>Sample App</h1>

「home」という文字はもうタイトルには無いので、テストを修正しておく。

test/controllers/static_pages_controller_test.rb
  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", @base_title
  end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】provideとyieldを使ったページタイトルの設定【Rails Tutorial 3・4・5章まとめ】

各ページのビューにprovideを記述

app/views/static_pages/home.html.erb
<% provide(:title, "Home") %>
<h1>Sample App</h1>

appilication.htmlのtitleにyieldを記述

app/views/layouts/application.html.erb
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
  </head>

ページタイトルに対するテスト

test/controllers/static_pages_controller_test.rb
  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

assert_selectを使って、titlesセレクタがページタイトルと一致することを確認する。
ページタイトルの重複部分はsetup内の変数@base_titleに入れておく。

~ここから4章~

full_titleヘルパーを作成

rootであるhomeビューでは、アプリケーション名のみをタイトルに表示したい。
そのため、homeビューではprovideは不要である。
しかし、この場合タイトルが「 | Ruby on Rails Tutorial Sample App」となり、余計な|が入る。
これを修正するためにページタイトルを返すfull_titleヘルパーを作成する。

app/helpers/application_helper.rb
module ApplicationHelper

  # ページごとの完全なタイトルを返す。
  def full_title(page_title = "")
    base_title = "Ruby on Rails Tutorial Sample App"
    if page_title.empty?
      base_title
    else
      page_title + " | " + base_title
    end
  end

end
app/views/layouts/application.html.erb
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
  </head>

変数page_titleの初期値を空にしておき、provideからくる引数yield(:title)が与えられなければ、base_titleのみを表示する。
与えられれば、|を追加して、完全なタイトルを表示する。
(ちなみに、empty?メソッドの末尾の?は、論理値を返すメソッドであることを意味する)

homeビューからはprovideを消しておく。

app/views/static_pages/home.html.erb
<h1>Sample App</h1>

「home」という文字はもうタイトルには無いので、テストを修正しておく。

test/controllers/static_pages_controller_test.rb
  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", @base_title
  end

〜ここから5章〜

full_titleヘルパーのテスト

タイトルに誤字などがないか、正しく機能しているかどうかをテストする。

applicationヘルパーにあるfull_titleヘルパーはそのままではテストで使えないので、includeを使ってテストで使えるようにする。

test/test_helper.rb
class ActiveSupport::TestCase
  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  fixtures :all
  # Add more helper methods to be used by all tests here...
  include ApplicationHelper
end
test/helpers/application_helper_test.rb
require 'test_helper'

class ApplicationHelperTest < ActionView::TestCase
  test "full title helper" do
    assert_equal full_title,         "Ruby on Rails Tutorial Sample App"
    assert_equal full_title("Help"), "Help | Ruby on Rails Tutorial Sample App"
  end
end

asseet_equalは第一引数と第二引数が等しいかどうかを確認するために使用する。

ところで、test/helpers/application_helper_test.rbは自動で生成されないようである。

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

【Rails】generateとdbの元に戻し方【Rails Tutorial 3章まとめ】

generate

  $ rails generate controller StaticPages home help
  $ rails destroy  controller StaticPages home help

  $ rails generate model User name:string email:string
  $ rails destroy model User

コントローラーの場合、コントローラー名のみでなくアクション名も指定する。
モデルの場合は不要。

db

  $ rails db:migrate
  $ rails db:rollback
  $ rails db:migrate VERSION=0

rollbackで一つ前の状態に戻す。
VERSION=0で最初の状態に戻す。

キャメルケースでコントローラーを生成

$ rails generate controller StaticPages home help
      create  app/controllers/static_pages_controller.rb

コントローラー名をキャメルケースで指定すると、自動でスネークケース(アンダーバーで繋がれた名前)になる。

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

rails 自動更新

ajaxを用いて自動更新機能を実装する。
今回は前回、作成した非同期通信のコードに書き加える形で自動更新を実装する。

完成品

Image from Gyazo

環境

ruby:2.5.1
rails:2.5.3
DB:mysql(Sequel Pro)
ブラウザ:Google
OS:Mac(10.14.6)

Github

https://github.com/tana1818/auto_update

作成する前に

なぜ必要か

日々使ってるLINEを考えてみましょう。
LINEのように、相手側が入力すると自分側が、LINEを開いたままでも、
リアルタイムで更新されます。あれも自動更新機能が実装されています。
自動更新がないままLINEのチャット機能を使用するとなると
表示されている画面をいちいちリロードしないと画面が更新されません。
これを一人ならまだしも、グループでチャットをするとなると面倒です。

自動更新の大きなメリットはリロードしなくてもリアルタイムでデータが更新されるという点です。

通信の流れ

実装に移る前に通信の流れを把握しておきましょう。
①ブラウザからindexアクションのリクエストが来たのでサーバーはレスポンスを返す
②ブラウザはサーバーからのレスポンスを解析
③解析して必要な全てのアセット (JavaScriptファイル、スタイルシート、画像) をサーバーから取得する
④JavaScriptを読み込む(最新のデータがあったら取得、なかったら取得しない)
⑤json形式で返ってきた値をcontrollerで判別
⑥最後にviewとjbuilderの情報を返す

同じ画面に居続けたら③から⑥を繰り返し自動更新が行われる

作成手順

ここでは自動更新機能を実装するだけの人用で書いていくが
一応データベースの情報はいじってないので自分の非同期通信を実装した方がいれば
「コード編集(ビュー)」の手順からファイルに追記していけば同じデータベースで自動更新機能が実装できる。

結論、データベースの名前が一律"ajax"になる。
もし「非同期通信」と「自動更新」のファイルを比較化をしたいって人は
データベースの名前を決めるファイル(database.yml)を編集してrails db:createしてください

 

Githubからコードをダウンロード

下記のURLからコードをダウンロード
https://github.com/tana1818/ajax
1.png

ダウンロードが完了したらファイル名を変更(なんでも良い)

任意のディレクトリにファイルうを配置したらファイル名変更
自分は自動更新なので’auto_update’って名前にしました。

 

gemのインストール、DBの作成、マイグレーション実行、サーバー起動

auto_update
bundle install #gemのインストール
rake db:create #DB作成
rake db:migrate  #マイグレーションファイルを実行する
rails s  #サーバーの起動

http://localhost:3000/
を検索

下図のように表示されたらOK
2.png

コード編集(ビュー)

①タイトルバーの名前変更

views/fruits/application.html.haml
!!!
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    //下記記述変更
    %title Auto_update
    = csrf_meta_tags
    = csp_meta_tag
    = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
  %body
    = yield

 
②indexにdata-id付与

views/fruits/index.html.haml
.container 
  .row
    %h1 Listing fruit
    - if flash[:notice]
      %p= flash[:notice]

  .row
    %table.table.table-bordered.table-sortable
      %thead
        %tr
          %th Name
          %th
          %th
          %th
      %tbody.fruit_item
        - @fruits.each do |fruit|
          //下記追加('item'クラスにデータのid情報追加)
          %tr.item{"data-id": "#{fruit.id}"}
            %td
              = fruit.name
            %td
              = link_to 'Show', fruit
            %td
              = link_to 'Edit', edit_fruit_path(fruit)
            %td
              = link_to 'Destroy', fruit, data: {confirm: 'Are you sure?'}, method: :delete

  .row
    %h1 New fruit
    = render 'form'

 

jsファイル編集

assets/javascripts/fruits.js
$(function(){
  //非同期通信
  function buildHTML(fruit){ //通信が成功するとdoneメソッドの引数にデータが入るようになっているため、これを利用してHTMLを組み立てる
    //5行目にdata-id = ${fruit.id}を追加
    var html = `<tr class = "item ui-sortable-handle" data-id = ${fruit.id}>
                  <td>
                    ${fruit.name}
                  </td>
                  <td>
                    <a href = /fruits/${fruit.id}>Show</a>
                  </td>
                  <td>
                    <a href = /fruits/${fruit.id}/edit>Edit</a>
                  </td>
                  <td>
                  <a data-confirm = "Are you sure?", rel = "nofollow", href = /fruits/${fruit.id}, data-method = "DELETE">Destroy</a>
                  </td>
                </tr>`
    return html;
  }
  $('#new_fruit').on('submit', function(e){ //'#new_fruit'の'submit'が押された時に発火
    e.preventDefault(); //これを書いてるせいで'submit'を押した際に要素にdisabledが付与される→最後の.alwasが書いてある行を足すことでそのdisabled要素を削除してる
    var formData = new FormData(this); //FormDataオブジェクトの引数はthisとなってる。イベントで設定したfunction内でthisを利用した場合はイベントが発生したDOM要素を指す。今回であればnew_commentというIDがついたフォームの情報を取得している
    $.ajax({
      url: "/fruits/", //ここはアクションのURLなのでrails routesで確認
      type: "POST", //ここはアクション名
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
    .done(function(data){ //非同期通信の結果として返ってくるデータは、done(function(data) { 処理 })の関数の引数で受け取る
    var html = buildHTML(data);
      $('.fruit_item').append(html) //htmlに追加
      // $('.b').val('') //なんかエラー出るなーと思ったらvarと書き間違えていた(ここでは'submit'を押した後テキストボックスに空の要素を付与してる)
      $('.new_fruit')[0].reset() //初期値があれば初期値にリセットされる
    })
    .fail(function(){ //エラーが置きた際
      alert('投稿できませんでした')
    });

    // .always(function(){ //この記述を書いてないと連続で投稿できない
    //   $(".c").removeAttr("disabled")
    // })
    return false;
    //要素の効果を無効化する、ちょっとわかんないけどとりま親要素のクリックをなかったことにするっぽいw
    //上記のコメント化記述は'disabled'を消しにいってる
    //こっちの方が記述量が少なくて良い
  })


  //自動更新
  if (location.pathname.match()){ //もし現在のURLパスがindexアクションだったら(http://localhost:3000/fruitsもしくはhttp://localhost:3000)
    setInterval(update, 5000);//5000ミリ秒ごとにupdateという関数を実行する
  }

  function update(){
    if($('.item')[0]){ //もし'.item'というクラスがあったら
      var fruit_id = $('.item:last').data('id'); //一番最後にある'.item'クラスの'id'というデータ属性を取得し、'fruit_id'という変数に代入
      $.ajax({
        url: location.href, //urlは現在のページを指定
        type: 'GET', //アクション名指定(データを表示させる)
        data: { id: fruit_id }, //rails に引き渡すデータ(これがparams[:id]になる)
        dataType: 'json'
      })
      .done(function(data){
        if (data.length){ //もしdataに値があったら
          $.each(data, function(i, data){ //'data'を'data'に代入してeachで回す
            var html = buildHTML(data);
            $('.fruit_item').append(html);
          })
        }
      })
      .fail(function(){ //そんなにこの記述はいらない気がするけど、異常系のエラー(途中で通信が中断されたり)が起きた時用
        alert('自動更新に失敗しました')
      });
    }
    else {
      clearInterval(); //値がなかったらsetIntervalを止める(この記述はなくても動く、別画面に遷移した際は、ブラウザを閉じた時同様、遷移した時点で遷移前のJavaScript実行は勝手に打ち切られる模様)
    }
  }
});

 

コントローラー編集(indexのみ)

controllers/fruites.controller.rb
  def index
    @fruits = Fruit.all
    @fruit = Fruit.new

    respond_to do |format| #記述追加
      format.html
      format.json { @new_fruit = Fruit.where('id > ?', params[:id]) } #jsのdataの情報がparams[:id]に入る
    end
  end

index.json.jbuilderのファイル編集

views/fruits/index.json.jbuilder
json.array! @new_fruit.each do |fruit|
  json.name fruit.name
  json.id fruit.id # 配列かつjson形式で@new_fruitを返す
end

 

以上で実装は終了
再度サーバーを立ち上げ直して

auto_update
rails s

うまく実装してできていれば上記のGIFのように自動で最新のデータが更新される

 
うまく実装できました?
実装できて自分の理解ができたら無駄なコメントは削除して読みやすいコードにしましょう。
(なんだか自分のコードにも_form.html.hamlに無駄なコメントアウトがあったりしたので、、)

今回でqiita2記事目なんですが、めちゃしんどいっすw
書いてみるとやってる事全然簡単に見えるのに、これあげるのにどれだけかかったか、、、
qiita人民の凄味を改めて実感しました。
ちょいまだまだ補足が書けていないのですが、一旦あげようと思います。
指摘事項あれば教えて頂けると嬉しいです。
それでは

はてなブログもやっているので是非!
https://tanagram18.hatenablog.com/

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

Rails Deviseでユーザー編集を現在のパスワードを入力しないで更新する

個人の備忘録

#背景
railsでユーザー登録のためにdeviseを使った簡単なプリを作成中に編集画面で
現在のパスワードを入れないで更新できないかと考えたため。

環境

rails 5.2.3
ruby 2.6.3

まずdeviseを用いた際のルーティングを確認

config/routes.rb
devise_for :users, controllers: {
        registrations: 'users/registrations',
        sessions: 'users/sessions'
      }

特に問題なさそう

registrations_controller.rbに下記を記載

registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  before_action :authenticate_user!

  protected

  *この部分を記載
  def update_resource(resource, params)
    resource.update_without_current_password(params)
  end

  (省略)

end

受けとったリソースを、update_without_current_passwordメソッド(Userモデルに後で実装します)で
パスワード抜きで更新できるようにし、引数にparamsを指定してリソースの中身を更新しています。

User モデルに update_without_current_password を実装

一個前でregistration_controllerに自力で定義したメソッドを使えるようにモデルに定義していく

models/user.rb
def update_without_current_password(params, *options)
    params.delete(:current_password)   *params.delete(:current_password) で current_password のパラメータを削除。

    if params[:password].blank? && params[:password_confirmation].blank?   *パスワード変更のためのパスワード入力フィールドとその確認フィールドの両者とも空の場合のみ、パスワードなしで更新できるようにするためです。

      params.delete(:password)
      params.delete(:password_confirmation)
    end

    result = update_attributes(params, *options)
    clean_up_passwords
    result
  end

あとはビューのフォームを消すだけ

パスワードのフォームを削除して終了

参考にしたサイト
Devise でユーザーがパスワードなしでアカウント情報を変更するのを許可
Deviseでパスワードの入力無しでユーザー情報の更新をする方法

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

「 LoadError: dlopen(〜): Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib」と表示された場合の解決法

railsで新しいアプリのDBを作成するべくrake db:createをしたところ、以下のエラーメッセージが表示。

LoadError: dlopen(/Users/(ユーザー名)/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mysql2-0.5.2/lib/mysql2/mysql2.bundle, 9): Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib
  Referenced from: /usr/local/opt/mysql@5.6/lib/libmysqlclient.18.dylib
  Reason: image not found - /Users/(ユーザー名)/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mysql2-0.5.2/lib/mysql2/mysql2.bundle
/Users/(ユーザー名)/projects/(アプリ名)/config/application.rb:7:in `<top (required)>'
/Users/(ユーザー名)/projects/(アプリ名)/Rakefile:4:in `require_relative'
/Users/(ユーザー名)/projects/(アプリ名)/Rakefile:4:in `<top (required)>'
/Users/(ユーザー名)/.rbenv/versions/2.5.1/bin/bundle:23:in `load'
/Users/(ユーザー名)/.rbenv/versions/2.5.1/bin/bundle:23:in `<main>'
(See full trace by running task with --trace)

rake routesなどのコマンドでも同様のエラーが出るため、ニッチもサッチも行かず困った事態に。

bundle doctorを行いエラーが出たgemを再インストールしたり、 opensslを再インストールしてもダメ。

色々と探した結果、最終的にはこちらの記事の対処法で上手く行きました。
https://github.com/kelaberetiv/TagUI/issues/86

brew update && brew upgrade

でHomebrewとインストールしたパッケージ(Formula)のアップデートを実施。

改めてrake db:createをしたところDB作成に成功しました。他のコマンドもちゃんと実行できます。

直前にHomebrewで色々と入れたので、それが問題だったのかもしれません(node.jsが怪しい)。

同様のエラーが出た場合はご参考までにどうぞ。


参考にさせていただいた記事:brewのupdateとupgradeの違い
https://qiita.com/okhrn/items/aa71b066a525456550c0
(こちらの記事によればbrew upgradeだけで大丈夫かもしれません。)

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

RailsでAPIを作ってJson形式のレスポンスを返すまで

はじめに

まとめておくと後で楽だよねってことで

環境

Ruby 2.6.5p114
Rails 6.0.1

環境自体はDockerコンテナに押し込めているので、まずはDockerの設定周りからやっていきます。

Docker関連

docker-compose.yml

こちらの記事を参考にしつつ作成しています。
ENTRYPOINTは自分が理解しきれてないので指定していません。
なくても動きます。

後、重要なのはmysqlのvolumesとして/etc/mysql/conf.dがいったん必須です。
mysql8からデフォルトの認証方式がパスワードになっていないので、それをパスワードにするために永続化して設定ファイルを入れてあげないと困ります。
認証方式変わったということは、それをデフォルトとして他を変えるのが本筋ですが、公開するサービスではないので、いったんパスワードにしています。

version: '3'
services:
  db:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '13306:3306'
    volumes:
      - ./mysql:/var/lib/mysql
      - ./mysql-confd:/etc/mysql/conf.d
  rails:
    build:
      context: .
      dockerfile: ruby/Dockerfile
    command: bash -c "bundle exec rails s -p 3000 -b '0.0.0.0'"
    environment:
      - "DATABASE_HOST=db"
      - "DATABASE_PORT=13306"
      - "DATABASE_USER=root"
      - "DATABASE_PASSWORD=password"
    volumes:
      - .:/app
    ports:
      - "13000:3000"

Dockerfile

こちらはRails6からwebpackerを入れる必要があるため、その設定は入れています。
今回はAPIを作るようのアプリケーションにするため、この設定はまるごといらないのですが、削除しなくても困らないので残しています。
APIモードで作らない場合はdocker-compose.ymlのcommandでインストールしてあげるかDockerfile内でインストールしてあげればOKです。

あと重要なのは最後にRUN bundle installを入れておくぐらいでしょうか。
Gemを書き換えた際にはイメージを書き換えないと駄目っぽいのですが(本当か?)、これやっておかないと起動時にGemがないと言われて怒られます。
1個目の方はRails関連のGemを入れるようなので、別にやっておかないとアプリケーションの方のGemの読み直してくれません。


ここも色々参考にさせていただいた記事があるのですが、見つけ次第追記します。
どの記事だっけなぁ・・・

FROM ruby:2.6.5

ENV TZ='Asia/Tokyo'

RUN /bin/cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

ENV LANG C.UTF-8

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

RUN apt-get update -qq && apt-get install -y nodejs yarn

RUN gem install bundler

WORKDIR /tmp
ADD ruby/Gemfile Gemfile
ADD ruby/Gemfile.lock Gemfile.lock
RUN bundle install

ENV APP_HOME /app
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
ADD . $APP_HOME

RUN bundle install

Gemfile

バージョンは適宜読み替えてください。
Gemfile.lockは参考にある通り空ファイルです。

Gemfileはrailsアプリケーションを生成した際に上書きされると困るので、rootフォルダとは別の場所に置いてあります。

source 'https://rubygems.org'

gem 'rails', '~> 6.0.1'

railsアプリケーションの生成

ファイルの準備が出来たらアプリケーションを生成します。

docker-compose run rails rails new . --api --force --no-deps --database=mysql

config/database.yml

立ち上げる前にデータベースの設定を書き換えます。
passwordとhostの書き換えだけです。

docker-composeでアプリケーションを作成するとネットワークをいい感じで作ってくれるため、hostをサービス名で設定しておけばいいので楽ですね。
docker-composeで設定したportsもこのネットワーク外部から繋いだ時のものでネットワーク内部からは普通のportで接続となります。

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

database作成

rakeコマンド使うなり自力で作るなりしてDB作っておいてください。

docker起動

ビルドして起動します。
中々に時間がかかるので、間違えてやり直しをするのが結構辛かったです。

docker-compose build
docker-compose up -d

起動確認

localhost:13000につないで画面出ればオッケーです。
駄目ならエラー見つつ頑張ってください。

routes.rbの設定

起動したので、routesを書いてURLを生成します。
適当でも良いですが、せっかくなので少し真面目に書きます。
resourcesをnewsにしているのは作ろうとしているアプリケーションのURLだからです。

routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :news
    end
  end
end

routeの確認

railsの操作をする際は色んなことをやっていくことになるので、内部で操作していった方が楽だと思います。外からでも操作できるので、どっちでも良いです。
入ってやる方は以下のコマンドで入れます。

docker-compose exec rails bash

確認したらこんな感じで出ると思います。
rails初期から存在するrouteと共に設定したrouteが見れます。

これで上のrouteが正しく設定されたことを確認します。

root@69c223a51ae2:/app# rails routes
                               Prefix Verb   URI Pattern                                                                              Controller#Action
                    api_v1_news_index GET    /api/v1/news(.:format)                                                                   api/v1/news#index
                                      POST   /api/v1/news(.:format)                                                                   api/v1/news#create
                          api_v1_news GET    /api/v1/news/:id(.:format)                                                               api/v1/news#show
                                      PATCH  /api/v1/news/:id(.:format)                                                               api/v1/news#update
                                      PUT    /api/v1/news/:id(.:format)                                                               api/v1/news#update
                                      DELETE /api/v1/news/:id(.:format)                                                               api/v1/news#destroy
        rails_mandrill_inbound_emails POST   /rails/action_mailbox/mandrill/inbound_emails(.:format)                                  action_mailbox/ingresses/mandrill/inbound_emails#create
        rails_postmark_inbound_emails POST   /rails/action_mailbox/postmark/inbound_emails(.:format)                                  action_mailbox/ingresses/postmark/inbound_emails#create
           rails_relay_inbound_emails POST   /rails/action_mailbox/relay/inbound_emails(.:format)                                     action_mailbox/ingresses/relay/inbound_emails#create
        rails_sendgrid_inbound_emails POST   /rails/action_mailbox/sendgrid/inbound_emails(.:format)                                  action_mailbox/ingresses/sendgrid/inbound_emails#create
         rails_mailgun_inbound_emails POST   /rails/action_mailbox/mailgun/inbound_emails/mime(.:format)                              action_mailbox/ingresses/mailgun/inbound_emails#create
       rails_conductor_inbound_emails GET    /rails/conductor/action_mailbox/inbound_emails(.:format)                                 rails/conductor/action_mailbox/inbound_emails#index
                                      POST   /rails/conductor/action_mailbox/inbound_emails(.:format)                                 rails/conductor/action_mailbox/inbound_emails#create
        rails_conductor_inbound_email GET    /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                             rails/conductor/action_mailbox/inbound_emails#show
                                      PATCH  /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                             rails/conductor/action_mailbox/inbound_emails#update
                                      PUT    /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                             rails/conductor/action_mailbox/inbound_emails#update
                                      DELETE /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                             rails/conductor/action_mailbox/inbound_emails#destroy
rails_conductor_inbound_email_reroute POST   /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format)                      rails/conductor/action_mailbox/reroutes#create
                   rails_service_blob GET    /rails/active_storage/blobs/:signed_id/*filename(.:format)                               active_storage/blobs#show
            rails_blob_representation GET    /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
                   rails_disk_service GET    /rails/active_storage/disk/:encoded_key/*filename(.:format)                              active_storage/disk#show
            update_rails_disk_service PUT    /rails/active_storage/disk/:encoded_token(.:format)                                      active_storage/disk#update
                 rails_direct_uploads POST   /rails/active_storage/direct_uploads(.:format)                                           active_storage/direct_uploads#create

controllerの作成

これもコマンドで作成します。
Controllerの名前はroutesで確認したControllerと同じにする必要があるので、その点だけ注意ください。

rails generate controller api/v1/news

作ったら中身です。

news_controller.rb
class Api::V1::NewsController < ApplicationController

  def index
    render json: { status: 'SUCCESS'}
  end

end

とりあえずのレスポンスを返却するためだけの中身です。
途中でキャッシュ関連で変更が反映されないという問題に直面しましたが、その際はこちらを参考にして解決しました。
Rails6でも同様の方法で解決できるようです。

終わりに

Rails使うならここまではぱぱっと出来るようになりたいですね。
一通りやってみて環境構築周りの注意点とか分かったので、やっぱり手を動かして作るって大事です。

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

MaterializeのParallaxとかsidenavをRailsで使うときの注意点

はじめに

RailsでMaterializeのParallaxとかsidenavとかとか使おうと
意気揚々とgem入れてサンプルコードをコピペして動かなかった人へ。
私も同じことをしたので、これ読んで速攻解決して次に進みましょう!
一応、jQueryを使います。

Railsで使うには初期化が必要

javascriptファイル作って中身をコピペしましょう。
私はわかりやすく「materialize-jquery.js」としました。

materialize-jquery.js
$(document).on('turbolinks:load', function () {
  // 各項目を初期化
  $('.sidenav').sidenav();
  $('.parallax').parallax();
  $('.dropdown-trigger').dropdown();
  $('.modal').modal();
});

// sidenavだけは、進む/戻るで動かなくなるので都度destroyが必要
$(document).on("turbolinks:before-cache", function () {
  $('.sidenav').sidenav('destroy');
  $('.dropdown-trigger').dropdown('destroy');
  $('.modal').modal('destroy');
});

これで問題なく動きます。

ついでに

こんな風にすると、sidenavが右から開きます。

  $('.sidenav').sidenav({
     edge: "right"
   });

Materializeのページではオプションの使い方の例すら書いておらず
非常に不親切 自走力が試されます。
他のオプションも同様なので、色々試してみましょう!

それでも動かない人

もしかしたら、jqueryとかを読み込む順番が間違っているかもしれないので
下記を参考に並び替えてみてください。

app/assets/javascripts/application.js
//= require rails-ujs
//= require jquery
//= require materialize
//= require activestorage
//= require turbolinks
//= require_tree .
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails link_toでアンカーを設定する

背景

スタッフの詳細ページにそのスタッフが作成したメニューの一覧を表示したいが、ただリンクさせるだけだと
スクロールをしなくてはならないためあまりつかいやすいとは思わなかったため

*個人の備忘録のために記載

環境

Rails 5.2.3
ruby 2.6.3

まずリンク先の飛びたい場所にidを指定

<div class="mi-contents2" id="staff_menu">

link_toにanchorを指定する

<%= link_to 'テキスト', リンク先のpath(anchor: 'リンク先のid', ~~~ %>

下記が実際の設定した例

<li class="usi-menu"><%= link_to 'このスタッフのメニューへ', staff_path(staff.id, anchor: 'staff_menu'), class: 'btn btn-primary' %></li>

上記のような記述をすると

<a href="/staffs/1#staff_menu">

のようなリンクが生成される

参考にしたサイト
kzy52's blog
Rails アンカーリンクを使い遷移先ページの場所(位置)を指定する

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

Codr0 : TwiterでコードをシェアできるWebアプリを作ろうと思った

たぶん、長い記事

きっかけ(こんな呟きを見かけた

作成の過程で得られたもの。

  • Active Record Storage
  • Twitter Login方法と仕組み、そのたTwitterあれこれ
  • JSの基礎
  • AWS S3の使い方
  • XSS対策

作成要件

Untitled.png

  • マークダウン投稿、シンタックスハイライト表示
    • gem: redcarpet, rouge
  • 投稿から画像生成
  • クラウドストレージに保存
    • AWS S3を使用

作成の流れ:予定

  1. rails new codr, git init, heroku create、Active Storage
  2. AWS S3あれこれ
  3. twitter登録、ログイン機能作成

開発環境

  • vm : Linux Ubuntu (virtualbox + vagrant)
    • Ruby 2.5.1p57
    • Rails 5.2.3
    • Postgresql

実作業: アプリ作成、諸準備

rails new codr -d postgresql
# DB設定等は割愛

Gem

今回は公開にまで至る予定なので、railsやdeviseの日本語化等も。が、想定ユーザはエンジニアだしと思い、最終的には英語オンリーのサイトになった。

Gemfile
gem 'mini_racer'   # uncomment
gem 'rails-i18n'   # japanize

# authetication
gem 'devise' # login
gem 'omniauth' # SNS login
gem 'omniauth-twitter' # twitter login
gem 'devise-i18n'  # japanize devise
gem 'devise-i18n-views'

gem 'redcarpet'   # for markdown
gem 'rouge' # for syntax highlight

gem 'meta-tags'
gem 'aws-sdk-s3' # for aws s3

gitignore => rails.credentials.yml

当初は.gitignoregem 'dotenv'等を使っていた。が、作成途中でRails5.2からのrails.credentials.ymlを知り、利用した。rails.credentials.ymlは暗号化されており、なお、復号化には/config/master.key`を利用。

irb
# editor setting
 EDITOR="vi" bin/rails credentials:edit
# edit credentials.yml
rails credentials:edit
# show credential.yml
rails credentials.yml:show

# herokuにmaster.keyを環境変数として指定
heroku config:set ENV_VAR="環境変数" --app "アプリ名"

# 追加した変数を使用するには
Rails.application.credentials.dig(:twitter, :API_Key)

rails gあれこれ

# devise
# install devise
rails g devise:install
rails g devise User name:String

# Add Admin column to User
rails g migration AddAdminToUsers
# add setting at /db/migrate/20191103141531_add_admin_to_users.rb
add_column :users, :admin, :boolean, default: false

# add views and controllers to modify devise
rails g devise:controllers users
rails g devise:views users

# japanize
# add at /config/application.rb
config.i18n.default_locale = :ja
=> create /config/locale/devise.view.ja.yml
# scaffold post
rails g scaffold Post user:references name:string content:text date:datetime

Active Record Associations関連付け

/app/model/user.rb
has_many :posts
/app/model/post.rb
belongs_to :user

投稿関連

マークダウン投稿

基本:Redcarpet::Markdown.new(renderer, extensions = {}).render(@post.content)
オプションやXSS対策等を追加したく、helperメソッドを作成した。

app/helpers/posts_helper.rb
Module PostsHelper
  require 'rouge/plugins/redcarpet'
  class RougeRedcarpetRenderer < Redcarpet::Render::HTML
    include Rouge::Plugins::Redcarpet

    def header(text, level)  # #や##等がh2、h3となるようにした。
      level += 1
      "<h#{level}>#{text}</h#{level}>"
    end
  end

  def markdown(text)
    render_options = {
      filter_html: true,  # do not allow any user-inputted HTML in the output.
      hard_wrap: true,
    }

    extensions = {
      autolink: true, # <>で囲まれていない時は、リンクとして認識しない
      fenced_code_blocks: true,   #  ```\n ```内をコード部分と見做す
      lax_spacing: true, 
      no_intra_emphasis: true,
      strikethrough: true,
      superscript: true,
      tables: false,  # テーブルを認識しない
      highlight: true,
      disable_indented_code_blocks: true,
      space_after_headers: false # #の後にスペースが無くても、h1等とする。
    }
    renderer = RougeRedcarpetRenderer.new(render_options)
    Redcarpet::Markdown.new(renderer, extensions).render(text).html_safe
  end
end

html_safe => sanitize

html_safeではXSS対策としては駄目と知った。名前詐欺である。
sanitizeヘルパーを使用した。ホワイトリスト方式。要参照

app/views/posts/index.html.erb
# sanitize(html, options = {})
 <div id="capture" class="content">
    <%= sanitize(markdown(@post.content), tags: %w(div img h1 h2 h3 h4 h5 strong em a p pre code ), attributes: %w(class href)) %>
</div>

投稿内容のデータ化、AWSへの画像保存

最初はTwitterAPIを利用して、投稿から作成、DBに直接保存した画像でTwitter投稿しようとした。だが、Herokuでは画像が保持されない事、TwitterAPIの変更などいろいろ面倒なことが発生したので、最終的には画像をAWS S3に保存し、og:imageに添付する形を取った。

  1. Webアプリ内で通常投稿
  2. showページ表示(同時にhtml2canvasでBase64としてデータ取得、hidden_fieldに収納
  3. Tweetボタン押す(Postされ、postモデル内でbase64をデコード
  4. Active Storageを通して、AWS S3に保存
  5. 保存画像urlをog:imageに添付

Active Storage

Rail5.2からの機能で、今まで常用されてたと言うcarrievaveやpaperclip等を使わずに、クラウドストレージ等へのアップロードが容易になる。今回はAWS S3を使った話。

irb
# set up
rails active_storage:install
# 今回は画像が紐づくPostテーブルが既にあるので、不要
# rails g resource comment content:text
rails db:migrate
app/models/post.rb
class Post < ApplicationRecord
# 今回は1つの投稿につき、1枚の画像なので。複数なら => has_many_attached :prtscs
  has_one_attached :prtsc
end
app/config/enviroments/
# ファイル保存先変更
# development.rb
config.active_storage.service = :local
# production.rb
config.active_storage.service = :amazon

rails credentials:editでAWSアクセスキーとシークレットキーを追加。

config/credentials.yml.enc
aws:
  access_key_id: 
  secret_access_key: 
config/storage.yml
test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: ap-northeast-1
  bucket: codr0
Gemfile
# gemが必要
gem 'mini_magick'
# 今回は不要だったので、入れず。
gem 'aws-sdk-s3', require: false

html2canvas

参考にしたページ

  1. Tweetボタン押下時に、画像をPostするためのフォーム、hidden_fieldを用意
  2. html2canvas.jsapp/assets/javascriptsディレクトリ配下に保存。
  3. html上に置くscriptコードを改修
app/views/posts/show.html.erb
<%= form_with(model: @post, local: true) do |form| %>
  <%= form.hidden_field :id, value: @post.id %>
  <%= form.hidden_field :prtsc, value: "" %> # idはpost_prtscになる。
  <%= form.submit "Post", class:"btn btn-outline-dark", id:"tweet", value:"tweet" %>
<% end %>

showページ描画後に、Redcarpetで出力された<div id="capture"></div>内にある<pre><code>を選択し、html2canvasの基本機能でBase64データ化し、最後に<input type="hidden" id="post_prtsc">のvalueにセットするようにした。

app/views/layouts/application.html.erb
<script type="text/javascript">
  html2canvas(document.querySelector("#capture"),{scale:1, width:600}).then(canvas => {
    var base64 = canvas.toDataURL('image/jpeg', 1.0);
    document.getElementById('post_prtsc').setAttribute('value', base64);
 });
</script>

Base64デコード

app/models/post.rb
attr_accessor :img

def parse_base64(img)
  if img.present?
    # data:image/jpeg;base64,/9j/4AAQSkZJRgABA・・・から/9j/4AA以降を選択取得
    content = img.split(',')[1]
    # 今回は、ユーザによる画像アップロード投稿ではなく、拡張子が決まっている
    filename = Time.zone.now.to_s + '.jpg'
    decoded_data = Base64.decode64(content)
    # String.IO.newにより、アプリ内に一時ファイルを作成しなくて済む
    prtsc.attach(io: StringIO.new(decoded_data), filename: filename)
  end
end

あとはposts_controllerで、paramsから受け取ったBase64データを上のparse_base64(img)で変換し、保存すれば完了。

AWS S3

AWS上での登録、設定、バケット作成等は割愛。

Tweet button

公式で生成されるTweetボタンのURLを利用し、押下時にwindow.openでTweet投稿ページを開くようにした。rubyonrailsで用意した変数をjsに渡すgem 'gon'も考えたが、見送った。

app/views/layouts/application.html.erb
<script>
  var base = 'https://twitter.com/intent/tweet?url=';
  var pageUrl = 'https://codr0.herokuapp.com/posts/' + document.getElementById('post_id').value;
  var option = '&button_hashtag=Codr0&ref_src=twsrc%5Etfw';
  var href = base + pageUrl + option;
  var twit = document.getElementById('tweet');
  twit.addEventListener('click', function() {
        window.open( href );
      });
</script>

Twitterログイン

TwitterDeveloperAccountが必要。割愛。

なお、omniauthは脆弱性が見つかっており、githubの方でもアラートが来るのだが、パッチが無いのだが。クックパッドの人が対処してくれたので、感謝したい。

app/models/user.rb
# 参考ページと同じ基礎的な所は割愛する。
class User < ApplicationRecord
  def self.from_omniauth(auth)
    find_or_create_by!(provider: auth['provider'], uid: auth['uid']) do |user|
      # 一部割愛
      user.username = auth['info']['nickname']
      # SNS登録時は、ダミーメールを登録
      user.email = User.dummy_email(auth)
    end
   end

  # SNS登録(providerが存在する)時は、パスワード要求をしない
  def password_required?
    super && provider.blank?
  end

  def self.new_with_session(params, session)
    if session['devise.user_attributes']
      new(session['devise.user_attributes']) do |user|
        user.attributes = params
      end
    else
      super
    end
  end

  private

  def self.dummy_email(auth)
    "#{auth.uid}-#{auth.provider}@example.com"
  end
end

Twitterのニックネームが取得できるようになったので、元からあるUserのnameテーブルは削除した。

デザイン

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

[RSpec] Ajaxのテストの書き方

はじめに

いいね機能やフォロー機能をajaxで作る時、Request specではどう書くかわからず
困ったので、メモしておきます。

結論

結論から言うとこのような形で書くと、ajaxで書いた機能をテストできます。
今回はいいね機能のテストを書いております。
いいね機能についてはこちらの記事を参考に作りました。
[Rails]いいね機能の非同期での実装!!!

(前提としてlike_pathのrouteにはusernamepost_idが表示されるようにしているため、FactoryBotでuserとそのpostを作り、そのpostにいいねできることをテストしております。)

it "いいねしていない場合、いいねができること" do
  expect do
    post like_path(username: user.user_name, post_id: 1), xhr: true
  end.to change(Like, :count).by(1)
end

このxhr: trueを書かないと下のエラーが出てしまいます。

 ActionController::UnknownFormat:
       LikesController#like is missing a template for this request format and variant.

       request.formats: ["text/html"]
       request.variant: []

xhr: trueを書くだけでテストがうまく通るのですが、調べてもすぐには分からず時間がかかってしまいました。
以上です!

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

ActiveAdmin 力技集

はじめに

皆さんは、ActiveAdmin使ってますか?

ActiveAdmin、サッと管理画面作るには素晴らしい反面、
ちょっとでも基本的な実装から外れると、途端につまる、、
そんな経験をされた方も多いかと思います。

そして往々として、業務では基本的な実装から外れるものですよね。

そんな格闘の中で得た、「ActiveAdminでちょっと凝ったことをするのに使える(かもしれない)方法」を、
「ActiveAdmin 力技集」 としてシェアしようと思います。

なお、この方法が正しいというわけではなく、こう乗り切ったよ、くらいの感覚で見ていただけるとありがたいです。
もっといい方法があれば、ぜひコメントをお願いします!

環境

Ruby 2.5.5
Rails 5.2.2.1
activeadmin 1.3.1

ActiveAdmin 力技集

Model

顧客がいて、接点があって、請求と明細がある

app/models/customer.rb
class Customer < ApplicationRecord
  has_many :contacts
  has_many :invoices
end
app/models/contact.rb
class Contact < ApplicationRecord
  belongs_to :customer
end
app/models/invoice.rb
class Invoice < ApplicationRecord
  belongs_to :customer
  has_many :items, class_name: 'InvoiceItem'
end
app/models/invoice_item.rb
class InvoiceItem < ApplicationRecord
  belongs_to :invoice
end

絞り込まれた一覧へのリンク

例えば顧客の詳細画面に、その顧客との接点一覧へのボタンを置きたい時。

app/admin/customers.rb
button_to '接点履歴一覧へ', admin_customer_contacts_path, method: :get, params: { q: { customer_id_equals: customer.id } }

検索条件がparams[:q]に入るのがキモ。
filterで検索を実装して実際に検索してみて、そのパラメータを再現すると手っ取り早い。

なお、このパラメータの使用に際して、filterで実装されている必要はない。

別のカラムでソート

app/admin/customers.rb
ActiveAdmin.register Customer do
  index do
    column '顧客ID', :id
    # sortable: にソートしたいカラム名を記述
    column '顧客名', :name, sortable: :kana
  end
end

このように漢字を表示しつつ読み仮名でのソートができる。

formでjsを使う

例えばformで顧客を選択したいがselectだと厳しいので、オートコンプリートを実装したい時などに。

app/admin/contacts.rb
ActiveAdmin.register Contact do
  # ...
  form partial: 'form'
end
app/views/admin/contacts/_form.html.erb
<script language="JavaScript">
  // @contactが暗黙的に渡ってくる。
  var contact = <%= raw @contact.to_json %>
  // ...
</script>

当然、この例では@contactの中身がブラウザで丸見えになるので、セキュリティ等問題がないユースケースか、検証が必要。

関連先によるソート・絞り込み

例えば顧客との接点の一覧画面で、顧客によってソート・絞り込みをしたい時。

app/admin/contacts.rb
ActiveAdmin.register Contact do
  filter :date, label: '年月日'
  # 関連先の型までは見てくれないようなので、as: :string などの指定が必要
  filter :customer_name, as: :string, label: '顧客名'

  index do
    column '年月日', :date
    column '顧客ID', :customer_id
    # sortableには発行されるSQLで使えるカラム名を渡す。今回はcustomersテーブルをjoinしているので、customers.kana
    # なお、関連名を渡すといい感じにリンク化してくれる
    column '顧客名', :customer, sortable: :'customers.kana'
    column '内容', :memo
  end

  controller do
    def scoped_collection
      # 関連先でソートを行う場合はjoinが必要
      end_of_association_chain.joins(:customer)
    end
  end
end

indexの表はscoped_collectionで発行されたSQLの結果を使っている、と考えるとイメージしやすい。

デフォルト検索条件

必ず「ある年月の一覧」を表示したい場合はこのように。

app/admin/invoices.rb
ActiveAdmin.register Invoice do
  # ...
  controller do
    # 必ず年月による絞り込みがかかるよう、indexページ表示前にparamsの不足を補う
    before_action only: :index do
      # 最初に表示されるのは先月分。
      prev_month = Time.current.prev_month
      if params[:q].present?
        params[:q].merge!(year_eq: prev_month.year, month_eq: prev_month.month) if params[:q][:year_eq].blank? || params[:q][:month_eq].blank?
      else
        params.merge!(q: { year_eq: prev_month.year, month_eq: prev_month.month })
      end
    end
  end
end

他の検索条件を邪魔しないように注意が必要。

GROUP BYした結果の一覧を作る

例えば版管理を行う請求書の一覧などで、版違いは同じ行でまとめたい時。

app/admin/invoices.rb
ActiveAdmin.register Invoice do
  index do
    # ...
    column 'バージョン', class: 'minWidth100' do |latest_invoice|
      Invoice
        .where(customer_id: latest_invoice.customer)
        .where(year: latest_invoice.year)
        .where(month: latest_invoice.month)
        .each do |invoice|
        div { span link_to "第#{invoice.version}版", admin_invoice_path(invoice) }
      end
      nil # nilを返さないと、上の行の結果が出力されてしまう
    end
  end

  controller do
    # ...
    def scoped_collection
      # 一覧には顧客ごとの請求書を出したいのだが、返るレコード数分、表の行が作られてしまう。
      # そのため、orderした上でgroup、顧客ごとに最新の請求書だけを取得
      # 参考: https://qiita.com/ryo-ishii/items/36b878cf2d0bd8ef7e07
      Invoice
        .group(:customer_id, :year, :month)
        .from(
          end_of_association_chain
            .select('invoices.*, customers.name, customers.kana, CONCAT(year, LPAD(month, 2, 0)) AS ym')
            .joins(:customer)
            .order(version: :desc),
          :invoices
        )
    end
  end
end

まとめ

ActiveAdminの良いところでもあり辛いところは、暗黙的に定義されている変数が多いところだと思います。
それらをいかに把握して差し込んでいくかが、ActiveAdminで力技をする際に重要になります。

それ以前に、力技を使わなくても済むような要件、データモデルの定義を心がけていきたいですね。

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

【Rails Guide】まとめ

6. 2番めのモデルを追加する

app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :article
end

referencesキーワードは、モデルの特殊なデータ型を表す。
指定されたモデル名の後ろに_idを追加した名前を持つ新しいカラムをデーターベーステーブルに作成

db/schema.rb
class CreateComments <  ActiveRecord::Migration[6.0]
def change
  create_table :comments do |t|
    t.string :commenter
    t.text   :body
    t.references :article, null: false, foreign_key: true
    t.timestamps
  end
end
end 

t.references はarticle_idという名前のinteger型のカラムとインデックスとarticlesのidカラムを指す外部キー制約を設定する

dependent: :destroy
削除のオプション

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

Rails server(Rails s)が再起動できない! 問題内容と解決法について

Rails server(以下、Rails s)に関する、バグが発生しましたので、内容と解決法を以下に纏めます。
皆様の参考になれば、幸いです。

問題内容

-内容:
Rails sを再起動しようとしたところ、既にサーバーがあり起動できないという問題
-背景/詳細:
binding.pry使用中、exit!・ctrl+c等をコマンドしてもなぜかループから抜けられない。
=>ターミナルを消してrails sを再起動して使用を試みる。
=>既にサーバーがあるとの文言があり、起動できないとのこと。

以下のような状況でした。

terminal
rails s
=> Rails 5.0.7.2 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
A server is already running. 

解決方法

結論、lsofコマンドを使用し、該当のPIDをキルして解消しました。
詳細を以下に記載していきます。

lsofコマンドを使用して状況を確認

今回はlocalhost:3000使用していたので、$ lsof -i:3000と記載してチェックします。
以下のように「34139」が既に存在していることがわかります。
このタスクが切れてないのが問題の原因であることが、明白になりました。

terminal
$ lsof -i:3000
COMMAND    PID    USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
ruby      34139  
ruby      34139 

該当のPID番号をキルして解消

それでは上記で確認できたタスクを削除します。

terminal
$ kill 34139

*念のため、再度状況を確認。何も動いていない状況であることが確認できました。
これで、再度「rails s」を使用することができます。

terminal
$ lsof -i:3000

因みに。。lsofコマンドについて

今回使用したlsofコマンドについて、調べてみましたので、以下に記載しておきます。
詳細は参照ページを確認頂けますと幸いです。

オプション 意味
-a 複数のオプションを指定した際に、AND(かつ)の意味で機能させる
-P ポート番号をサービス名に変換しない
-c プロセス名を指定する
-i ネットワークソケットファイルを指定する
-n IPアドレスを表示する(名前解決しない)
-p プロセスIDを指定する
-u ユーザー名を指定する
項目 意味
COMMAND 実行されているコマンド名
PID プロセスID
USER ユーザー名
FD ファイルディスクリプタ
TYPE 種類
DEVICE デバイス
SIZE/OFF ファイルサイズ
NODE iノード番号(プロトコル)
NAME ファイル

参照

Linux基本コマンドTips一覧
https://www.atmarkit.co.jp/ait/articles/1904/18/news033.html

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

【Rails環境構築】MySQL2が原因で「bundle install」失敗した時の対処法

はじめに

Railsの環境構築をローカルでしようとrails newからはじめたところ、bundle installができずに引っかかったため備忘録として残しておきます。

環境

OS : Mac OS Catalina 10.15.1
Ruby : 2.6.3p62
Rails : 6.0.1
Homebrew : 2.1.16
Bundle : 1.17.2

エラー

$ bundle install

省略(gemのインストール)

Installing mysql2 0.5.2 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

省略

An error occurred while installing mysql2 (0.5.2), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/'` succeeds
before bundling.

ざっくり解釈した限りだと、「mysql2 0.5.2のインストール時にnative extention(CやC++で書かれるMySQLの拡張のライブラリ)のビルドに失敗したので、bundleの前にgem install mysql2 -v '0.5.2'で確認してください」とのことですが、私の場合root権限でこのコマンドを打っても解決せず...

解決法

結論から言うと、「ビルド時にOpenSSL公開鍵のPATH指定が必要」と「bundle configでPATHオプションをLDFLAGS、CPPFLAGSで別々に指定」で解決しました。

1.公開鍵のPATH確認

brew info openssl

省略

For compilers to find openssl@1.1 you may need to set:
  export LDFLAGS="-L/usr/local/opt/openssl@1.1/lib"      ← ここ使用
  export CPPFLAGS="-I/usr/local/opt/openssl@1.1/include"   ← ここ使用

省略

2.それぞれPATHを指定

$ bundle config --local build.mysql2 "--with-cppflags=-I/usr/local/opt/openssl@1.1/include"
$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib"

それぞれ、「build.mysql2の現在のローカル値を置き換えています」と返答がきます。

3. bundle install (ここで終了)

$ bundle install

私の場合はこれで解決し、「rails new」でも問題なく作成できました。

失敗or解決に直接繋がらなかったコマンド

1. PATHの両指定

bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib --with-cppflags=-I/usr/local/opt/openssl/include"

2.mysqlの再起動

$ sudo mysql.server restart

その他

公開鍵、LDFLAGS、CPPFLAGSなど知識やAppleの独自TLS背景などまだまだ勉強不足でした💦
表記や解釈ミスなどあればご指摘頂けますと幸いです。

参考

こちらの記事がなければ解決できませんでした(本当に助かりました)。
https://qiita.com/fukudakumi/items/463a39406ce713396403
https://qiita.com/akito19/items/e1dc54f907987e688cc0
http://woshidan.hatenadiary.jp/entry/2017/01/21/150948

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