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

Rails6:ActionTextの初期データをseedを使って投入する

はじめに

Rails6から使えるようになったActionTextを使ってみたのですが、初期データを入れるときにつまずいてしまったので、そのときのことを記事にしようと思いました。

前提

Rails:6.0.3.2
使うモデル名:Post
カラム:title
リッチテキストのフィールド名:content

seedの投入

まず簡単にpostをいくつか作る

15.times do
  Post.create!(title: Faker::Book.unique.title)
end

その後にaction_text_rich_texts テーブルにPostと関連づけるデータを投入することで、ActionTextの初期データを入れることができます。

実際にはこんな感じになるかと思います。

Post.all.each do |post|
  ActionText::RichText.create!(record_type: 'Post', record_id: post.id, name: 'content', body: Faker::Lorem.sentence)
end

record_typeにはActionTextを使っているモデル名、recor_idはpostのid、nameにはリッチテキストフィールド名、bodyには実際にいれたい文面をいれるように設定してください。

最後に投入して完成です。

rails db:seed

これでview側に表示できるようにすれば投入したデータが表示されてるかと思います。

おまけ:ActionTextから投稿したみたいに改行、太文字を設定する

初期データに改行、太文字を入れたい場合は以下のように設定します。

content = '<div class="trix-content">
  <div><strong>ここが太文字</strong>です。<br><br>ここの文章は改行されています</div>
</div>'
rich = ActionText::RichText.last
rich.update(body: content)

これでview側を確認すると改行と太文字が入った初期データを用意することができていると思います。

余談

画像つきの記事って初期データから用意できるのかな。。

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

Docker導入時のエラー(You must use Bundler 2 or greater with this lockfile.)

はじめに

dockerを既存のアプリに導入された際に発生したエラーとその解決法を今回はまとめさせていただきます。

 エラーの内容

docker file を作成docker-compose.ymlを作成した後にdocker-compose build コマンドをするとなぜか 

You must use Bundler 2 or greater with this lockfile.

という内容のエラーが発生しました。

解決法

ネットの記事を参考にしたところ

RUN gem install bundler 
RUN bundle install

とRUN bundle installの前に RUN gem install bundler を記述すれば治ると買いてありましたが、既に私は記述していました。

調べていくうちにruby2.5.1の場合はこのようなバグが発生するといった記事を見つけ rubyのversionを2.7.1にdockerfile ローカル環境変更した結果今回のエラーは解決しました。

 参考記事

ruby version変更https://qiita.com/_kanacan_/items/c1499f6c13b1c41da982

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

CarrierWaveによって保存されたデータの中身。

調査した背景

CarrierWaveによって画像データをアップロードしたのですが、そのデータを更新したいとき、どのようにデータが入っているか調べました。
(この記事を見られている方ももしかすると、参照された方もいるかもしれません)

データベースでは以下のとおり、ファイル名のみが表示されます(image列)
image.png

DBのUIは「sequelpro」を使っています。列値の詳細を参照しても、ファイル名のみです。
image.png

環境

項目 内容
OS.Catalina v10.15.4
Ruby v2.5.1
Ruby On Rails v5.2.4.3
MySQL v5.6

中身を参照する

以下の通り、コマンドを実行し、中身を参照しました。

「Attachment」というテーブルにアクセスしています。

[6] pry(main)> >> image_data = Attachment.find(7)
image_data = Attachment.find(7)
  Attachment Load (0.5ms)  SELECT  `attachments`.* FROM `attachments` WHERE `attachments`.`id` = 7 LIMIT 1
=> #<Attachment:0x00007f86e6d7b760
 id: 7,
 knowledge_id: 17,
 sub_id: "1",
 name: "test.png",
 width_size: "1200",
 height_size: "799",
 file_type: "png",
 file_size: "72297",
 image: "test.png",
 thumb_image_url:
  "/uploads/tmp/1593690046-968881373703887-0012-2842/thumb_test.png",
 created_at: Thu, 02 Jul 2020 11:40:46 UTC +00:00,
 updated_at: Thu, 02 Jul 2020 11:40:46 UTC +00:00,
 image_url: "/uploads/tmp/1593690046-968881373703887-0012-2842/test.png">

ここからが、イメージ情報になります。

[7] pry(main)> >> image_data.image
image_data.image
=> #<ImageUploader:0x00007f86e6c26a68
 @cache_id=nil,
 @file=
  #<CarrierWave::SanitizedFile:0x00007f86e6c25e10
   @content=nil,
   @content_type=nil,
   @file=
    "/Users/ichikawadaisuke/projects/krown/public/uploads/attachment/image/7/test.png",
   @original_filename=nil>,
 @filename=nil,
 @format=nil,
 @identifier="test.png",
 @model=
  #<Attachment:0x00007f86e6d7b760
   id: 7,
   knowledge_id: 17,
   sub_id: "1",
   name: "test.png",
   width_size: "1200",
   height_size: "799",
   file_type: "png",
   file_size: "72297",
   image: "test.png",
   thumb_image_url:
    "/uploads/tmp/1593690046-968881373703887-0012-2842/thumb_test.png",
   created_at: Thu, 02 Jul 2020 11:40:46 UTC +00:00,
   updated_at: Thu, 02 Jul 2020 11:40:46 UTC +00:00,
   image_url: "/uploads/tmp/1593690046-968881373703887-0012-2842/test.png">,
 @mounted_as=:image,
 @staged=false,
 @storage=
  #<CarrierWave::Storage::File:0x00007f86e6c262c0
   @cache_called=nil,
   @uploader=#<ImageUploader:0x00007f86e6c26a68 ...>>,
 @versions=
  {:thumb=>
    #<ImageUploader::Uploader70108727518060:0x00007f86e6c25c80
     @cache_id=nil,
     @file=
      #<CarrierWave::SanitizedFile:0x00007f86e6c25460
       @content=nil,
       @content_type=nil,
       @file=
        "/Users/ichikawadaisuke/projects/krown/public/uploads/attachment/image/7/thumb_test.png",
       @original_filename=nil>,
     @filename=nil,
     @format=nil,
     @identifier="test.png",
     @model=
      #<Attachment:0x00007f86e6d7b760
       id: 7,
       knowledge_id: 17,
       sub_id: "1",
       name: "test.png",
       width_size: "1200",
       height_size: "799",
       file_type: "png",
       file_size: "72297",
       image: "test.png",
       thumb_image_url:
        "/uploads/tmp/1593690046-968881373703887-0012-2842/thumb_test.png",
       created_at: Thu, 02 Jul 2020 11:40:46 UTC +00:00,
       updated_at: Thu, 02 Jul 2020 11:40:46 UTC +00:00,
       image_url:
        "/uploads/tmp/1593690046-968881373703887-0012-2842/test.png">,
     @mounted_as=:image,
     @parent_version=#<ImageUploader:0x00007f86e6c26a68 ...>,
     @staged=false,
     @storage=
      #<CarrierWave::Storage::File:0x00007f86e6c25a00
       @cache_called=nil,
       @uploader=
        #<ImageUploader::Uploader70108727518060:0x00007f86e6c25c80 ...>>,
     @versions={}>}>
[8] pry(main)> 

さらにワンライナーで、簡単にオブジェクトの情報を取得出来ます。

[9] pry(main)> >> image_data.image.file
image_data.image.file
=> #<CarrierWave::SanitizedFile:0x00007f86e6c25e10
 @content=nil,
 @content_type=nil,
 @file=
  "/Users/ichikawadaisuke/projects/krown/public/uploads/attachment/image/7/test.png",
 @original_filename=nil>
[10] pry(main)> 


今回は以上です。

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

Ruby で任意個の整数の最大公約数・最小公倍数を求める

Ruby で整数 ab の最大公約数,最小公倍数を得るには,それぞれ Integer#gcdInteger#lcm を用いて

# 最大公約数(GCD: Greatest Common Divisor)
a.gcd(b)

# 最小公倍数(LCM: Least Common Multiplier)
a.lcm(b)

のようにする。

たとえば,4 と 6 の最大公約数,最小公倍数は

puts 4.gcd(6) # => 2
puts 4.lcm(6) # => 12

といった具合。

では三つの整数 abc の最大公約数,最小公倍数は?
$a$,$b$,$c$ の最大公約数は,「$a$ と $b$ の最大公約数」と $c$ の最大公約数なので,

a.gcd(b).gcd(c)

で得られる。
最小公倍数も同様で,

a.lcm(b).lcm(c)

で得られる。

では,整数の組が配列で与えられていたら?
以下のように書けばよい。

numbers = [30, 20, 15]

# 最大公約数
puts numbers.inject(:gcd) # => 5

# 最小公倍数
puts numbers.inject(:lcm) # => 60

Enumerable#inject にはブロックを与えずにシンボルを与える用法があったよね。

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

Rails & React & Webpacker & MySQL環境構築マニュアル

突然ですが、環境構築って毎朝髭を剃るのと同じぐらい面倒で苦手です。
この記事をご覧になっているということは、少なからずあなたも環境構築に苦手意識があるのではないでしょうか。

社内のメンバーからも「環境構築はコンビニでたむろするヤンキーぐらい苦手」という声を良く耳にします。
私は思います、この環境構築という最初のハードルが、クリエイティブな行動を阻害していると!
環境構築の手間さえ省ければ、きっとこの世の中にはもっと多くのサービスが創出されると確信しています!
そこで今回は、"RailsをAPIサーバーとして利用"し、"Reactで描画を行う"サービスをつくりまくるための環境構築マニュアルを公開します!

以下すべてに当てはまる人が本記事の対象読者です

  • なんかオシャレっぽいからMac使ってます!
  • プログラミングスクール卒業したから個人アプリつくりたいぜ!
  • React.jsっていうJavascriptのモダンなフレームワークを身につけて周りと差を付けたいぜ!
  • react-railsとかのGemを使わない方法でRailsとReact間のやりとりを疎結合にしたい!
  • Docker?なにそれ美味しいの?(本記事ではDockerの解説はしません)

0. 事前インストール

名前 説明
Ruby いわずもがな
Rails いわずもがな
MySQL いわずもがな
brew パッケージ管理 (主にサーバー側)
yarn パッケージ管理 (主にフロント側)

1. Railsアプリの作成

rails new アプリ名 -–skip-turbolinks --webpack=react --database=mysql --api

人生に何回この『rails new』コマンドを打ったかでRailsエンジニアとしての価値が決まると、まことしやかに噂される。
ちなみにオプションは必要であれば書き換えてOKです

2. Webpackerのインストール

Webpackerとは、Rails標準装備のモジュールバンドラーで、Webpackのラッパーです。
バンドラーというのは束ねる人のことです。
HTML、CSS、JSなど色々な形式のファイルを束ねてくれるやつです。

ちなみにラッパーは韻を踏む人のことではありません。
サランラップとかのラッパーです。『包む』という意味です。
Webpackerは内部でWebpackを呼び出しているので、WebpackerはWebpackのラッパーです。

ちなみにフロントエンドに興味があるなら、Webpackの知識はある程度あった方が良いです。
"Babel"とか"ES6"とかそういうワードとセットで覚えるとGOODです!

rails webpacker:install
rails webpacker:install:react

3. MySQLのインストール

今回はDBにMySQLを使ってみます。
私は普段の業務ではPostgreSQLを使用しているのですが、プログラミングスクール卒の方はMySQLに慣れていると思うので。
新規プロダクトのDB選定はその現場で使い慣れているものを使用しているところが多いような気がしています。
違ったらすみません。
ちなみに余談of余談ですが、個人的にはNoSQLのMongoDBとかに興味があったりします。
理由は、「なんとなく知ってたらイケてるエンジニアっぽいから」です。

今回はbrewというパッケージマネージャー経由でMySQLをインストールします

brew install mysql

4. MySQLユーザーの作成

MySQLがインストールできたら、今回のアプリで使用するためのユーザーを作成します。
各コマンドについては、特に詳しく説明する必要もなさそうなので割愛します。

・ルートユーザーにログイン

mysql -u root -p

・ユーザー作成

好きなユーザー名とパスワードを設定

create user 'ユーザー名'@'localhost' identified by 'パスワード';

・作成したユーザーの確認

作成したユーザーが表示されていれば成功

select User,Host from mysql.user;

・権限付与

grant all on *.* to '[ユーザー名]'@'localhost';

・config/database.ymlの設定変更

ユーザーの作成が一通り終わったら、作成したユーザーとRailsを紐付けます。
RailsのDB設定はdatabase.ymlに記述するのがルールです。

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: <%= ENV['DATABASE_HOST'] %>

development:
  <<: *default
  database: app_name_development

test:
  <<: *default
  database: app_name_test

production:
  <<: *default
  database: app_name_production
  username: <%= ENV['APP_NAME_DATABASE_USERNAME'] %>
  password: <%= ENV['APP_NAME_DATABASE_PASSWORD'] %>

『app_name_○○』はRails newした時のアプリ名に置き換えてください
usernameやpasswordはGitHubで公開しちゃうと見えてしまうので
gem『dotenv』等を使って隠蔽することをおすすめします。
ちなみにdotenvで作成した『.env』ファイルをGit管理から外しておかないと意味が無いので、作ったら『.gitignore』に『.env』を忘れずに追加しましょう!
「何を言っているのかわからない...」という人は「dotenv 環境変数」とかで調べてみよう!
「わからないことを調べる」のは、エンジニアの基本です!
この『調べる』をいかに深堀りしてできるかが、成長の近道のような気がしています。

5. データベースの作成

rake db:create

6. Railsサーバーの起動

rails s

7. Webで確認

http://localhost:3000/

「Yay! You’re on Rails!」が表示されていれば成功

環境構築はもう少し続きます。
もう6合目ぐらいには来てます。もう少し。

8. Webpackerの設定(任意)

・splitchunks

チャンクを自動分割してくれるWebpackのプラグインです。
ファイルサイズの節約ができたりするけど、別になくても良いです。

config/webpack/environment.jsの変更
const { environment } = require('@rails/webpacker');
environment.splitChunks();
module.exports = environment;
app/views/top/show.html.erb

javascript/packs/の中にある「index」という名前の付いたファイルを参照するの意。

<%# splitchunksを使う場合 %>
<%= javascript_packs_with_chunks_tag 'index' %>

<%# splitchunksを使わない場合 %>
<%= javascript_pack_tag 'index' %>

参考: splitchunks

9. ルーティング設定

config/routes.rb
Rails.application.routes.draw do
  # ルートページ設定
  root "top#show"
end

10. エントリーポイントの作成

・ルートページのコンロトーラー作成

app/controllers/top_controller.rb
class TopController < ApplicationController

  def show
  end

end

・Reactで描画するためのid属性を追加

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>アプリケーションタイトル</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= javascript_pack_tag 'application' %>
  </head>

  <body>
    <div id="root"> ←これです
      <%= yield %>
    </div>
  </body>
</html>

・Reactのエントリーポイント作成

app/javascript/packs/index.jsx

app/views/top/show.html.erbから参照されるファイル
このファイルがReactの入り口です。
非同期、ルーティング、状態管理等、React用のパッケージをimportして設定しています。
各パッケージのインストールは後で行います。

// このファイルがRailsのViewから呼ばれる一番最初のファイルです(EntryPoint)
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import {
  Router,
  Route,
  Switch,
  IndexRoute,
  useLocation
} from 'react-router-dom';
import { createBrowserHistory } from 'history';
import { composeWithDevTools } from 'redux-devtools-extension';

// reducer
import rootReducer from '~/src/reducers/';

// Component
import Top from '~/src/components/tops/';

const middleWares = [thunk];
// 開発環境の場合、開発ツールを使用するための設定
const enhancer = process.env.NODE_ENV === 'development' ?
  composeWithDevTools(applyMiddleware(...middleWares)) : applyMiddleware(...middleWares);
const store = createStore(rootReducer, enhancer);
const customHistory = createBrowserHistory();

render(
  <Provider store={store}>
    <Router history={customHistory}>
      <Route render={({ location }) => (
        <div>
          <Switch location={location}>
            <Route exact path='/' component={Top} />
          </Switch>
        </div>
      )}/>
    </Router>
  </Provider>,
  document.getElementById('root')
)

・Reactコンポーネント作成

app/javascript/src/components/tops/index.jsx

Reactコンポーネントの記述にはjsxという拡張子のファイルを使用します。
JSファイルの中にHTMLを記述します。
最初はJSの中にHTMLタグを書くことに気持ち悪さを感じますが、その内慣れます。

import React from 'react';

const Top = () => (
  <h1>
    <center>アプリケーションのタイトル</center>
  </h1>
)
export default Top;

・Reactリデューサーをまとめる処理の作成

app/javascript/src/reducers/index.js
import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';

import top from '~/src/modules/tops/';

export default combineReducers({
  form: formReducer,
  top,
});

ここでまとめたものがReduxのstoreに格納されます。
Reduxとは状態を一元管理してくれるパッケージのことです。
storeとは状態を格納する箱のことです。Reduxの一番重要な機能です。
Reactの開発において、Reduxの利用は必須ではありませんが、React単体だとプロダクトの規模が大きくなるにつれて状態管理が辛くなるので、初めから入れておいた方がいいです。
LPとか規模の小さいプロダクトならなくても良いです。

・Reactモジュール作成

ディレクトリ構成はducksパターンを採用。
ducksパターンというのは『action type』、『action creator』、『reducer』を1つのファイルにまとめて記述する考え方のことです。設計の概念です。何かをインストールするとかではないです。

app/javascript/src/modules/tops/index.js
// action-type
const TOP_INITIAL = 'TOP_INITIAL';

// reducer
const initialState = {
  top: null,
}

export default function top(state = initialState, action) {
  switch (action.type) {
    case TOP_INITIAL:
      return {
        ...state,
      }
    default:
      return state
  }
}

// action-creator
export const topInitial = () => ({
  type: TOP_INITIAL,
});

通常は『action type』、『action creator』、『reducer』それぞれでファイルを作成するところ、ducksパターンを取り入れると1つのファイルにまとまるので、単純にファイル数が少なくて済みます。
中規模プロダクトでも全然耐えられる設計概念なのでおすすめです。
「action typeって何?」と思った人はRedux公式で調べてみましょう!

11. 必要なパッケージインストール

yarnというパッケージマネージャーを使用してインストールします。
似た様なパッケージマネージャーで『npm』がありますが、『yarn』は『npm』の上位互換です。
yarnでインストールしたパッケージは、ルートディレクトリ直下の『package.json』というファイルに自動で追加されます。
『yarn add パッケージ名』でパッケージの追加
『yarn remove パッケージ名』でパッケージの削除です。

yarn add redux react-redux react-router-dom redux-devtools-extension redux-form redux-thunk axios @babel/preset-react babel-plugin-root-import

もし興味があれば『@reduxjs/toolkit』、『@material-ui/core』もおすすめです

12. パス指定設定ファイルの作成(任意)

独学でReactを少しでも開発したことがある方なら一度はこう思ったはず
「React相対パス地獄なりがち。」
Reactはimport時の相対パス指定地獄に陥りがちです。
そうならないよう『babel-plugin-root-import』を入れることをおすすめします。
実は上記11.の『yarn add』の中にこっそり入っているので、コマンドをコピペして実行した方は私の策略によりすでに入っています。

『.babelrc』というファイルを作ってそこに設定を記述します。
『.babelrc』ファイルを作る場所はルートディレクトリ直下。

.babelrc

{
  "plugins": [
    [
      "babel-plugin-root-import",
      {
        "paths": [
          {
            "rootPathSuffix": "./app/javascript/src",
            "rootPathPrefix": "~/src/"
          },
        ]
      }
    ]
  ]
}

上記設定は『./app/javascript/src』というパス指定を『~/src/』という文字列でも指定できるように設定しているだけです。
これで、Reactコンポーネントでのimport時に『~/src/○○』が使えるようになるので、相対パス地獄から抜け出せます。
ちなみに『"~/src/"』の部分は『"~/"』でも『"@/src/"』でも好きに設定できます。

13. webpack-dev-serverの起動

./bin/webpack-dev-server

自動コンパイルしてくれる開発用サーバーです。
常にコードの監視もしているので、Reactのコードを書き換えると自動でブラウザ上の描画も書き換えてくれます。
(ちなみにRailsのModelやController、Viewなどは監視対象外なので変更しても自動描画はされません。素直に『command + R』でブラウザ更新しましょう。)

お疲れ様でした

これでRails & Reactの開発環境が整った...はずです。
http://localhost:3000/に「アプリケーションのタイトル」が表示されていれば無事成功です!
それでは楽しい3R(Ruby on Rails on React)開発を!

トラブルシューティング

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

上記エラーメッセージが表示されてbundle installが失敗する場合↓

sudo xcodebuild -license acceptで解決できる場合もある

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

RailsでURL文字列にaタグに変換する

railsのメール処理で文字列に含まれるURLをaタグ付きに変換したいことありました。

ちょっと調べてみると、URI.extractを使うと文字列のURLが簡単に取得できる。。。割と簡単にできそうやん、と思って書いてみたら、実は罠が結構あって嵌ってしまったので復習がてら書いてみることにしました。

TL;DR

最終的なコードは下記にすることで解決しました。どうやってこれにたどり着いたのか?なぜこうすると良いのかを後述して行きます。

def convert_url_to_a_element(text)
  uri_reg = URI.regexp(%w[http https])
  text.gsub(uri_reg) { %{<a href='#{$&}' target='_blank'>#{$&}</a>} }
end

text = 'url1: http://hogehoge.com/hoge url2: http://hogehoge.com/fuga'
convert_url_to_a_element(text)
=> "url1: <a href='http://hogehoge.com/hoge' target='_blank'>http://hogehoge.com/hoge</a> url2: <a href='http://hogehoge.com/fuga' target='_blank'>http://hogehoge.com/fuga</a>"

アンチパターン

まずは最初に間違っていた処理の書き方です。
とはいえ、これでも下記のようなテキストであれば問題なく処理ができてしまいます。だからこそ今回すぐにこの書き方の罠に気づくことができていませんでした。。。

def convert_url_to_a_element(text)
  URI.extract(text, %w[http https]).uniq.each do |url|
    sub_text = "<a href='#{url}' target='_blank'>#{url}</a>"
    text.gsub(url, sub_text)
  end
  text
end

text = 'url1: http://hogehoge.com url2: http://fugafuga.com'
convert_url_to_a_element(text)
=> 'url1: http://hogehoge.com url2: http://fugafuga.com'

URI.extractを使うと下記のようにURL形式の文字列を全て取得することができる。

text = 'url1: http://hogehoge.com url2: http://fugafuga.com'
URI.extract(text, %w[http https])
=> ["http://hogehoge.com", "http://fugafuga.com"]

これをeachで回して置換しています。しかしながら、下記のように同じドメイン名のURL2種類で実施すると。。。

text = 'url1: http://hogehoge.com/hoge url2: http://hogehoge.com'
convert_url_to_a_element(text)
=> "url1: <a href='<a href='http://hogehoge.com' target='_blank'>http://hogehoge.com</a>/hoge' target='_blank'><a href='http://hogehoge.com' target='_blank'>http://hogehoge.com</a>/hoge</a> url2: <a href='http://hogehoge.com' target='_blank'>http://hogehoge.com</a>"

なんかめっちゃ崩れてる。。。

原因

原因は、2回目の置換にてaタグ変換後のテキストに対しても置換処理を行ってしまったためです。
このように、上記の書き方では同一ホスト名のURLが2つ以上あるとうまく動作しないという落とし穴があります。

対応策

URI.extractで取得した文字列をeachで回すのではなく、正規表現を取得してgsubのパターンに正規表現を使って置換させることで、二重置換を防ぐことができます。

def convert_url_to_a_element(text)
  uri_reg = URI.regexp(%w[http https])
  text.gsub(uri_reg) { %{<a href='#{$&}' target='_blank'>#{$&}</a>} }
end

補足メモ

URI.regexpについて

URI.regexpは指定したスキーマのURL文字列のパターンを正規表現で返すメソッドです。正規表現とは、文字列ものなので、自分で書くことも可能ですがそれをサクッと作ってくれるのがこのメソッドです。

返り値をみるとわかると思いますが、これを自分で1から書く気にはなれませんでした。。。

URI.regexp(%w[http https])
=> /(?=(?-mix:http|https):)
        ([a-zA-Z][\-+.a-zA-Z\d]*):                           (?# 1: scheme)
        (?:
           ((?:[\-_.!~*'()a-zA-Z\d;?:@&=+$,]|%[a-fA-F\d]{2})(?:[\-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]|%[a-fA-F\d]{2})*)                    (?# 2: opaque)
        |
           (?:(?:
             \/\/(?:
                 (?:(?:((?:[\-_.!~*'()a-zA-Z\d;:&=+$,]|%[a-fA-F\d]{2})*)@)?        (?# 3: userinfo)
                   (?:((?:(?:[a-zA-Z0-9\-.]|%\h\h)+|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[(?:(?:[a-fA-F\d]{1,4}:)*(?:[a-fA-F\d]{1,4}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|(?:(?:[a-fA-F\d]{1,4}:)*[a-fA-F\d]{1,4})?::(?:(?:[a-fA-F\d]{1,4}:)*(?:[a-fA-F\d]{1,4}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))?)\]))(?::(\d*))?))? (?# 4: host, 5: port)
               |
                 ((?:[\-_.!~*'()a-zA-Z\d$,;:@&=+]|%[a-fA-F\d]{2})+)                 (?# 6: registry)
               )
             |
             (?!\/\/))                           (?# XXX: '\/\/' is the mark for hostport)
             (\/(?:[\-_.!~*'()a-zA-Z\d:@&=+$,]|%[a-fA-F\d]{2})*(?:;(?:[\-_.!~*'()a-zA-Z\d:@&=+$,]|%[a-fA-F\d]{2})*)*(?:\/(?:[\-_.!~*'()a-zA-Z\d:@&=+$,]|%[a-fA-F\d]{2})*(?:;(?:[\-_.!~*'()a-zA-Z\d:@&=+$,]|%[a-fA-F\d]{2})*)*)*)?                    (?# 7: path)
           )(?:\?((?:[\-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]|%[a-fA-F\d]{2})*))?                 (?# 8: query)
        )
        (?:\#((?:[\-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]|%[a-fA-F\d]{2})*))?                  (?# 9: fragment)
      /x

gsubについて

gsubメソッド自体は正規表現ではなく文字列を渡しても置換することができます。前者の場合では単純に取得したURL文字列をeachで渡して置換しているのですが、その結果、同じドメインが含まれるURLなんかだと、aタグ変換後の文字列に対しても置換処理が実行されてしまい、変な文字列になってしまうようです。

考えてみりゃそりゃそうか。。。って感じですがこの対策が案外思いつかなくて悩みました。まずはgsub

text.gsub!(uri_reg) { %{<a href="#{$&}">#{$&}</a>} }

URI.extractについて

まず、最初に使ったURI.extractだが、スキーマを指定することでテキスト内からURL文字列のみを取得することができる。今回は最終的には使わなかったが、URL文字列のみをシンプルに取得したいのであれば便利そうでした。

text = 'aaaaa http://xxx.com/hoge bbbbb http://xxx.com'
URI.extract(text, %w[http https])
=> ["http://xxx.com/hoge" "http://xxx.com"]

まとめ

  • aタグ変換を行うのであればgsubも正規表現でパターンマッチングした上で置換した方が良さそう
  • 正規表現そのものはURI.regexpを使うと簡単に取得することができる

と、紆余曲折ありましたが良いコードになったんじゃないかと思います。
もっと他に良い書き方があったりしたら是非とも教えていただきたいです。

参考URL

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

Arrayにsplitがあって混乱した

次のようなコードを書いたところ、配列の配列が帰ってきて混乱しました。

numbers.split(/,/) #=> [["1","2","3"]]

原因は、split済みの配列に対してsplitを呼んでいたせいでした。

numbers = "1,2,3".split(/,/)
numbers.split(/,/) #=> [["1","2","3"]]

RailsのActiveSupportは Array#split を用意しています。特定の値の前後で配列を配列の配列に分割するものです。

[1, 2, 3, 4, 5].split(3) # => [[1,2],[4,5]]

しかし、変数に文字列が入っているつもりが配列だった、という場合には面食らうことなります。

def include_three?(string)
  string.split(/,/).include?("3")
end

include_three?("1,2,3,4,5".split(/,/)) #=> false
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

個人アプリ作成#3

投稿画面の作成

スクリーンショット 2020-07-13 20.01.43.png

application.html.haml

= yield の上にヘッダー下にフッターを記述する事によりどのページに行ってもヘッダーとフッターがある状態になる
スクリーンショット 2020-07-13 20.04.05.png

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

Rails5でECサイトを作る⑨ ~カート機能を作る~

はじめに

架空のベーカリーで買い物できるECサイトを作るシリーズ、Rails5でECサイトを作る⑧の続きです。
今回はカート機能を作っていきます。ようやくECサイトの体裁が整いますね。

ソースコード

https://github.com/Sn16799/bakeryFUMIZUKI

Modelのアソシエーション

fumizuki_ER.jpg

カート機能に関連するモデルは主にCustomerとProductです。カート機能ではカートの本体のようなものはなく、商品を選んで「カートに入れる」ボタンを押すと、各商品につき1件のデータが登録される仕組みになっています。CartItemモデルは、CustomerとProductのID、それと商品の個数のみを記憶する中間テーブル的な役割を果たします。

Controller

app/controllers/cart_items_controller.rb
class CartItemsController < ApplicationController

  before_action :authenticate_customer!
  before_action :set_cart_item, only: [:show, :update, :destroy, :edit]
  before_action :set_customer

  def create
    @cart_item = current_customer.cart_items.build(cart_item_params)
    @current_item = CartItem.find_by(product_id: @cart_item.product_id,customer_id: @cart_item.customer_id)
    # カートに同じ商品がなければ新規追加、あれば既存のデータと合算
    if @current_item.nil?
      if @cart_item.save
        flash[:success] = 'カートに商品が追加されました!'
        redirect_to cart_items_path
      else
        @carts_items = @customer.cart_items.all
        render 'index'
        flash[:danger] = 'カートに商品を追加できませんでした。'
      end
    else
      @current_item.quantity += params[:quantity].to_i
      @current_item.update(cart_item_params)
      redirect_to cart_items_path
    end
  end

  def destroy
    @cart_item.destroy
    redirect_to cart_items_path
    flash[:info] = 'カートの商品を取り消しました。'
  end

  def index
    @cart_items = @customer.cart_items.all
  end

  def update
    if @cart_item.update(cart_item_params)
      redirect_to cart_items_path
      flash[:success] = 'カート内の商品を更新しました!'
    end
  end

  def destroy_all #カート内アイテム全部消去
    @customer.cart_items.destroy_all
    redirect_to cart_items_path
    flash[:info] = 'カートを空にしました。'
  end

  private

  def set_customer
    @customer = current_customer
  end

  def set_cart_item
    @cart_item = CartItem.find(params[:id])
  end

  def cart_item_params
    params.require(:cart_item).permit(:product_id, :customer_id, :quantity)
  end
end

createアクションにおいてただsaveとだけ書くと、同じ商品を買おうとした時に「食パン 1、食パン 2、食パン 1、……」のように同じ商品でも別データとして登録されてしまいます。テーブルの構造上このようなことが起きるのですが、やはり後から追加でカートに入れた分もまとめて表示できると便利なので、if文で処理を分けています。

また、カートの商品を個別で取り消すほか、カートの中身を一斉に空にする処理もできるようにしたいと思っていたところ、destroy_allという便利なメソッドを見つけました。

View

index画面

app/views/cart_items/index.html.erb
<div class="col-lg-10 offset-lg-1 space">
  <div class="container-fluid">
    <!-- タイトル + 全消去メソッド -->
    <div class="row">
      <div class="col-lg-4">
        <h2>
          <span style="display: inline-block;">ショッピング</span>
          <span style="display: inline-block;">カート</span>
        </h2>
      </div>
      <div class="col-lg-4">
        <%= link_to 'カートを空にする', destroy_all_cart_items_path, method: :delete, class: 'btn btn-danger' %>
      </div>
    </div>

    <!-- カートの商品一覧 -->
    <div class="d-none d-lg-block">
      <div class="row space">
        <div class="col-lg-5"><h4>商品名</h4></div>
        <div class="col-lg-2"><h4>単価(税込)</h4></div>
        <div class="col-lg-2"><h4>数量</h4></div>
        <div class="col-lg-2"><h4>小計</h4></div>
      </div>
    </div>

    <% sum_all = 0 %>
    <% @cart_items.each do |cart_item| %>
    <div class="row space-sm">
      <div class="col-lg-3">
        <%= link_to product_path(cart_item.product) do %>
        <%= attachment_image_tag(cart_item.product, :image, :fill, 100, 100, fallback: "no_img.jpg") %>
        <% end %>
      </div>
      <div class="col-lg-2">
        <%= link_to product_path(cart_item.product) do %>
        <%= cart_item.product.name %>
        <% end %>
      </div>
      <div class="col-lg-2">
        <%= price_include_tax(cart_item.product.price) %>
      </div>
      <div class="col-lg-2">
        <%= form_with model: cart_item, local: true do |f| %>
        <%= f.number_field :quantity, value: cart_item.quantity, min:1, max:99  %>
        <%= f.submit "変更", class: "btn btn-primary" %>
        <% end %>
      </div>
      <div class="col-lg-2">
        <%= sum_product = price_include_tax(cart_item.product.price).to_i * cart_item.quantity %><% sum_all += sum_product %>
      </div>
      <div class="col-lg-1">
        <%= link_to "削除する", cart_item_path(cart_item), method: :delete, class: "btn btn-danger"%>
      </div>
    </div>
    <% end %>

    <!-- 合計金額 + 情報入力 -->
    <div class="row space">
      <div class="col-lg-2 offset-lg-7 space-sm">
        <%= link_to "買い物を続ける", customer_top_path, class: "btn btn-danger "%>
      </div>
      <div class="col-lg-3 space-sm">
        <div class="row">
          <h4>合計金額:<%= sum_all %></h4>
        </div>
      </div>
    </div>
    <div class="row space">
      <div class="col-lg-3 offset-lg-9">
        <%= link_to "情報入力に進む", new_order_path, class: "btn btn-danger btn-lg" %>
      </div>
    </div>

  </div>
</div>

商品ごとの小計、全商品の総計は、view上のeach文内に計算式を組み込んで表示しています。一般にviewで細かな計算やら分岐処理やら行うのは望ましくないようで、他に良い方法はないものでしょうか。

app/helpers/application_helper.rb
def price_include_tax(price)
  price = price * 1.08
  "#{price.floor}円"
end

上のHTMLで商品の税込価格を表示する際に使っているヘルパーです。小数点以下はfloorで切り捨てにしています。小数点の処理に関しては、こちらに詳しく載っています。

app/assets/stylesheets/application.scss
.space-sm {
  padding-top: 20px;
}

後記

機能の実装もちょっと複雑になってきて、ようやく面白くなってきました。サイト内を眺めてみても、カートに商品を入れるとだいぶお買い物気分を味わうことができます。あとはOrder(注文情報)周辺を作ればcustomerサイトは完成です!

ただ、このシリーズではadminサイトも自作するため、コード量としては半分くらいです。とはいえ、前回作った時の体感ではOrderモデルの機能が断トツで難解だったので、それさえできてしまえば何とかなるでしょう。

さて、私は最難関のOrderモデルを分かりやすく解説できるのか? 次回へ続く!

参考

Pikawaka
【Ruby】小数点以下の桁数を指定して四捨五入、切り上げ、切り捨て!

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

【bcrypt】has_secure_passwordのpresence: trueを解除する方法

bcrptを利用しているけど、パスワードは任意で設定したい

bcrptで投稿にパスワードを設定できるようにしたのですが、
デフォルトで空の投稿は弾かれるように設定されているため、
それを解除する方法をご紹介します。

※投稿にパスワードを設定する方法は以下で紹介してます。

https://qiita.com/hiruhiru/items/9dffc729f5192df243a8

バリテーションを無効にする

has_secure_passwordの横に(validations: false)をつけるだけです。

qiita.rb
class Post < ApplicationRecord
  has_secure_password(validations: false)
end

まとめ

バリテーションを無効にすることで空の投稿もできるようになりました。

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

【rails】投稿にパスワードを設定する方法

投稿にパスワードを設定する方法

投稿にパスワード機能を付けたのでそのアウトプット用に記事を書きました。

この記事のゴール

以下画像のようにパスワードを設定できるようにします。

post_with_password.gif

投稿機能を作る

さくっと投稿機能を作成。
scaffoldを使えば1分で作れます。

$ rails new post_with_password
$ cd post_with_password
$ rails g scaffold Post description:text
$ rails db:migrate

投稿機能.gif

bcryptを設定

以下投稿にパスワードを設定する流れ
1.投稿時にパスワードを設定
2.詳細ページを開く際にパスワードを要求
3.パスワードが一致すれば詳細ページにリダイレクト

ログイン機能以外で使われているところを見ないbcryptを使用します。

Gemfileに以下のgemがコメントアウトされているので#を削除。
gem 'bcrypt', '~> 3.1.7'

$ bundle install

次にモデルを少しだけいじります。

_form.html.erb
class Post < ApplicationRecord
  has_secure_password
end

$ rails g migration add_password_digest_to_posts password_digest:string
$ rails db:migrate

モデルにhas_secure_password,password_digestカラムを追加することで、
passwordとpassword_confirmationと2つの属性を利用可能に。

1.投稿時にパスワードを設定

本題のパスワードを設定していきましょう。

まずはviewでユーザーがパスワードを設定できるようにします。

post.rb
(省略)
  <div class="field">
    <%= form.label :description %>
    <%= form.text_area :description %>
  </div>

  <div class="field">
    <%= form.label :password %>
    <%= form.password_field :password %>
  </div>

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

次にストロングパラメータに:passwordを追加します。

posts_controller.rb
  private
    # Use callbacks to share common setup or constraints between actions.
    def set_post
      @post = Post.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def post_params
      params.require(:post).permit(:description, :password)
    end
end

これでパスワードをdbに保存できるようになりました。

2.詳細ページを開く際にパスワードを要求

次に詳細ページを表示する際にパスワードを要求し、設定したパスワードと一致すれば詳細ページにリダイレクトしましょう。

パスワードを認証させるページを作成します。

posts_with_password.html.erb
<div class="users-new-wrapper">
  <div class="container">
    <div class="row">
      <div class="col-md-offset-4 col-md-4 users-new-container">
        <h1 class="text-center text-white">password</h1>
        <%= form_for(:post, {controller: 'posts', action: "posts_with_password/#{@post}" }) do |f| %>
          <div class="form-group">
            <%= f.label :password %>
            <%= f.password_field :password, class: 'form-control' %>
          </div>
            <%= f.submit "送信", class: 'btn-block' %>
        <% end %>
      </div>
    </div>
  </div>
</div>

ルーティングも追加します。

qiita.rb
Rails.application.routes.draw do
  resources :posts
  get       'posts_with_password/:id', to: 'posts#posts_with_password'
  post      'posts_with_password/:id', to: 'posts#authenticate'
end

パスワードが一致すれば詳細ページにリダイレクト

最後に認証機能を作成します。

posts_controller.rb
(省略)
 def authenticate
    post_id = Post.find(params[:id])
    if post_id && post_id.authenticate(params[:post][:password])
      redirect_to post_path(post_id)
    else
      render 'posts_with_password'
    end
  end

  def posts_with_pass

投稿とパスワードが一致すれば詳細ページにリダイレクトされるようになりました。

昨日はこれで完成です。

ただこれだと全ての投稿にパスワードを設定する必要があるため、パスワードの設定は任意にします。

bcryptで設定したhas_secure_passwordは空の投稿が弾かれてしまうため、
空でも投稿できるように以下を追加します。

post.rb
class Post < ApplicationRecord
  has_secure_password(validations: false)
end

passwaord_digestにパスワードが入っている時と、入っていない時で
詳細ページのリンク先を変更します。

index.hetml.erb
(省略)
    <% @posts.each do |post| %>
      <tr>
        <td><%= post.description %></td><img width="311" alt="スクリーンショット 2020-07-13 14.02.37.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/637746/1591f219-10f0-9197-1633-ac908ace636d.jpeg">

        <% if post.password_digest.nil? %>
          <td><%= link_to 'Show', post_path(post) %></td>
        <% else %>
          <td><%= link_to 'Show', "posts_with_password/#{post.id}" %></td>
        <% end %>
        <td><%= link_to 'Edit', edit_post_path(post) %></td>
        <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

しかしこれだとせっかくパスワードを設定してもindexからDescriptionが見れてしまいます。

スクリーンショット 2020-07-13 14.02.37.jpeg

password_digestの有無でDescriptionの表示を変更します。

index.hetml.erb
  <tbody>
    <% @posts.each do |post| %>
      <tr>
        <% if post.password_digest.nil? %>
          <td><%= post.description %></td>
          <td><%= link_to 'Show', post_path(post) %></td>
        <% else %>
          <td>secret</td>
          <td><%= link_to 'Show', "posts_with_password/#{post.id}" %></td>
        <% end %>
        <td><%= link_to 'Edit', edit_post_path(post) %></td>
        <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

password_digestが空の場合は通常通り、空ではない場合は"secret"と表示させるように設定しました。

まとめ

以上で完成です。

ログイン以外にもbcrypt使ってあげてくださいね。

ポートフォリオなどの参考になれば嬉しいです。

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

ターミナルに Matrix が降ってくる Hack

1. ターミナルに雪を降らせるスクリプトというのがあるそうで

ruby -e 'C=`stty size`.scan(/\d+/)[1].to_i;S=["2743".to_i(16)].pack("U*");a={};puts "\033[2J";loop{a[rand(C)]=0;a.each{|x,o|;a[x]+=1;print "\033[#{o};#{x}H \033[#{a[x]};#{x}H#{S} \033[0;0H"};$stdout.flush;sleep 0.1}'

2. 実行したらこうなるよ

snow.gif

※ ネタ元は ここの コメント欄らしいです

3. ruby の部分を取り出して、読みやすくリファクタしてみました

puts "\033[2J" # clear screen
terminal_width = `stty size`.split(' ').last.to_i
positions = {}
loop do
  positions[rand(terminal_width)] = 0
  positions.each { |column, row|
    print "\033[#{row};#{column}H " # erase snow
    positions[column] += 1
    print "\033[#{positions[column]};#{column}H❃" # draw snow
  }
  sleep 0.1
end

※ 自分の環境(macOS Catalina)で不要そうなコードは削除しました

4. で、改造したくなりました

width = `stty size`.split(' ').last.to_i
positions = {}
print "\033[40m\033[32m" # black and green
puts "\033[2J" # clear screen
loop do
  positions[rand(width)] = 0
  positions.each { |column, row|
    positions[column] += 1
    print "\033[#{positions[column]};#{column}H#{[*' '..'z', *'ヲ'..'ン'].sample}"
  }
  sleep 0.1
end

5. ワンラインに戻すよ

ruby -e 'w=`stty size`.split(" ").last.to_i;p={};print"\033[40m\033[32m\033[2J";loop{;p[rand(w)]=0;p.each{|c,r|;p[c]+=1;s=[*" ".."z",*"ヲ".."ン"].sample;print"\033[#{p[c]};#{c}H#{s}";};sleep 0.1}'

6. 実行したらこうなるよ

matrix.gif

7. まとめ

タイトルはわざと頭悪そうにしてみました。釣りです ?

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

【Rails】クーポン機能の実装(バッチ処理を用いた自動削除機能付き)

目標

ezgif.com-video-to-gif (1).gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

Slim導入
Bootstrap3導入
ログイン機能実装
投稿機能実装

実装

1.カラムを追加

ターミナル
$ rails g model Coupon user_id:integer is_valid:boolean limit:integer
~__create_coupons.rb
class CreateCoupons < ActiveRecord::Migration[5.2]
  def change
    create_table :coupons do |t|
      t.integer :user_id
      t.boolean :is_valid, default: true # 「default: true」を追記
      t.integer :limit

      t.timestamps
    end
  end
end
ターミナル
$ rails db:migrate

2.モデルを編集

user.rb
# 追記
has_many :coupons, dependent: :destroy
coupon.rb
class Coupon < ApplicationRecord
  belongs_to :user

  enum is_valid: { '有効': true, '無効': false }

  def self.coupon_create(user)
    coupon = Coupon.new(user_id: user.id, limit: 1)
    coupon.save
  end

  def self.coupon_destroy
    time = Time.now
    coupons = Coupon.all
    coupons.each do |coupon|
      if coupon.created_at + coupon.limit.days < time && coupon.is_valid == '有効'
        coupon.is_valid = '無効'
        coupon.save
      end
    end
  end
end

【解説】

① クーポンの状態をenumで管理する。

enum is_valid: { '有効': true, '無効': false }

② クーポンを作成するメソッドを定義する。

def self.coupon_create(user)
  coupon = Coupon.new(user_id: user.id, limit: 1)
  coupon.save
end

③ クーポンを削除するメソッドを定義する。

def self.coupon_destroy
  time = Time.now
  coupons = Coupon.all
  coupons.each do |coupon|
    if coupon.created_at + coupon.limit.days < time && coupon.is_valid == '有効'
      coupon.is_valid = '無効'
      coupon.save
    end
  end
end

◎ クーポンを作成してから24時間経過かつ、クーポンの状態が有効の場合は、無効に変更して保存する。

if coupon.created_at + coupon.limit.minutes < time && coupon.is_valid == '有効'
  coupon.is_valid = '無効'
  coupon.save
end

3.coupons_controller.rbを作成・編集

ターミナル
$ rails g controller coupons index
coupons_controller.rb
class CouponsController < ApplicationController
  def index
    @coupons = Coupon.where(user_id: current_user.id, is_valid: '有効') 
  end
end

4.books_controller.rbを編集

今回は本を投稿成功した場合に、クーポンを発行するように実装します。

books_controller.rb
def create
  @book = Book.new(book_params)
  @book.user_id = current_user.id
  if @book.save
    Coupon.coupon_create(current_user) # 追記
    redirect_to books_path
  else
    @books = Book.all
    render 'index'
  end
end

5.日時設定を変更

application.rbを編集する。

application.rb
module Bookers2Debug
  class Application < Rails::Application
    config.load_defaults 5.2
    config.time_zone = 'Tokyo' # 追記
  end
end

②日時のフォーマットを設定するファイルを作成・編集

ターミナル
$ touch config/initializers/time_formats.rb
time_formats.rb
Time::DATE_FORMATS[:datetime_jp] = '%Y/%m/%d/%H:%M'

6.ビューを編集

coupons/index.html.slim
.row
  .col-xs-3

  .col-xs-6
    table.table
      thead
        tr
          th
            | クーポン番号
          th
            | タイトル

      tbody
        - @coupons.each.with_index(1) do |coupon, index|
          tr
            td
              = index
            td
              - limit = coupon.created_at + coupon.limit.minutes
              = limit.to_s(:datetime_jp)

  .col-xs-3

【解説】

① クーポン作成日時の1日後を、5で設定したフォーマットで表示する。

- limit = coupon.created_at + coupon.limit.minutes
= limit.to_s(:datetime_jp)

7.自動削除機能の実装

① Gemを導入

Gemfile
# 追記
gem 'whenever', require: false
ターミナル
$ bundle

「schedule.rb」を作成・編集

ターミナル
$ bundle exec wheneverize .
config/schedule.rb
env :PATH, ENV['PATH'] # 絶対パスから相対パス指定
set :output, 'log/cron.log' # ログの出力先ファイルを設定
set :environment, :development # 環境を設定

every 1.minute do
  runner 'Coupon.coupon_destroy'
end

4.cronを反映

ターミナル
$ bundle exec whenever --update-crontab

バッチ処理でよく使うコマンド

crontab -e ➡︎ cronをターミナル上で編集

$ bundle exec whenever ➡︎ cronの設定を確認

$ bundle exec whenever --update-crontab ➡︎ cronを反映

$ bundle exec whenever --clear-crontab ➡︎ cronを削除

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

[Rails]パンくずリストを作る

対象読者

  • Railsでパンくずリストを実装したい人。
  • 使い方忘れた人。
  • 初学者向けになっています。内容も初歩的なところを解説しています。

gretelって何?

パンくずリストです。ヘンゼルとグレーテルの話のやつ。
パンくず落としていって自分の辿って来た道がわかる。

Gemのインストール

gretelのgithubはこちらから

Gemfile
gem 'gretel'

bundle installしたら必要なファイルを生成します。

$ bundle install
$ rails g gretel:install

以下のようにファイルが生成されればOKです。

Running via Spring preloader in process 6675
      create  config/breadcrumbs.rb

これが中身。

breadcrumbs.rb
crumb :root do
  link "Home", root_path
end

# crumb :projects do
#   link "Projects", projects_path
# end

# crumb :project do |project|
#   link project.name, project_path(project)
#   parent :projects
# end

#
#
#以下省略
#
#
#

設定を書く

先ほどのbreadcrumbs.rbというファイルはパンくずを落としていく設定ができるファイルになります。
例えば、

Home > カテゴリ

のようなパンくずを落としていきたい場合は

breadcrumb.rb
crumb :root do
  link "Home", root_path
end

crumb :articles do
  link "記事一覧", articles_path #パスは該当ページのパスを書く(ここでは記事一覧)
  parent :root
end

カテゴリの前のページをHomeにしたいのでparentは:rootを指定します。

Viewに表示させる

あとはViewの方で出力してあげるだけです。

application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>パンくずアプリ</title>
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>
  <body>
    <%= breadcrumbs separator: " &rsaquo; " %> #ここを追加
    <%= yield %>
  </body>
</html>
articles/index.html.erb
<% breadcrumb :articles %>

これで

Home > 記事一覧

のパンくずリストが出来上がります。

登録してあるデータをパンくずに表示したい

Home > 記事一覧 > [記事のタイトル]

みたいにしたい場合は少し工夫が必要になります。

以下のように、Viewの方からbreadcrumb.rbへデータを送ってあげないといけません。
今回は記事のタイトルをパンくずとして出力してあげたいので、@articleを第2引数に指定してデータを渡してあげます。

articles/show.html.erb
<% breadcrumb :article_show, @article %>
breadcrumb.rb
crumb :root do
  link "Home", root_path
end

crumb :articles do
  link "記事一覧", articles_path #パスは該当ページのパスを書く(ここでは記事一覧)
  parent :root
end

crumb :article_show do |article| #ここで受け取ってる
  link article.title, article_path(article) #<表示する文字列>、<記事詳細のパス>
  parent :articles  #親を設定する
end

Home > 記事一覧 > パンくずリストを作ってみた

作成日時を出力させたいと思ったら、

breadcrumbs.rb
crumb :article_show do |article|
  link article.created_at, article_path(article) #変更(title => created_at)
  parent :articles
end

に変更すればOKです。

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

【Rails】ActiveRecord::Bitemporalの使い方(BiTemporalDataModel)

はじめに

RailsでBitemporalDataModelを扱いたかったのですが
ちょうどactiverecord-bitemporalという良いgemがあったので、
いろいろ触ってみた結果を書いていきたいと思います。

BitemporalDataModelとは何か

そもそもBiTemporalとはどういう意味でしょうか。
「Bi」は接頭辞で2つのという意味を表します、Bicycleとか言いますよね。
「Temporal」は時間のという意味の形容詞です。
つまりBiTemporalで二つの時間のという意味になり、
BiTemporalDataModelは二つの時間のデータモデルになります。

二つの時間は何かと言いますと、
「システム上の時間」「事実情報としての時間」です。

詳しくは説明すると長くなってしまうので、
こちらのスライドが参考になると思います。(特に33ページ目以降)

使い方

レコードの作成、更新

ではactiverecord-bitemporalを使ってレコードを作成したり更新したりをしていきたいと思います。

まずは準備です。詳しくはこちら

テーブルを用意しましょう

db/schema.rb
ActiveRecord::Schema.define(version: 1) do
  create_table :employees, force: true do |t|
    t.string :name #従業員名
    t.string :position #役職

    # ActiveRecord::BiTemporal に必要なカラムを追加する
    t.integer :bitemporal_id
    t.datetime :valid_from #適用日時
    t.datetime :valid_to #終了日時
    t.datetime :deleted_at #削除日時(論理削除)
  end
end

ActiveRecord::Bitemporalの読み込みの設定をします。

app/models/emoloyee.rb
class Employee < ActiveRecord::Base
  include ActiveRecord::Bitemporal
end
では、実際に使っていきたいと思います

employeesというテーブルを作って、従業員の情報をDBに保存していくような例を考えます。

具体的には、

2018年1月1日 田中さんが平社員として入社し、2018年1月3日にレコード作成

2020年1月1日 課長に昇進、 2020年1月10日にレコード作成

2020年1月20日 課長に更新したと思ったが、間違えて家長と入力していたので課長に修正

という例を考えます。

rails consoleで操作していきます。
※簡単のために〇〇時〇〇分〇〇秒は省略しています。(実際には秒まで入ります。)

まずは最初のレコード作成、

2018年1月1日田中さんが平社員として入社

Employee.create(name: "田中", position: "平社員", valid_from: "2018-01-01")

valid_fromには適用日時を指定できます。
何も指定しなければ入力時点現在の時刻となります。
valid_toも指定できますが、今回指定していないので9999年12月31日となっています。

この時DBは以下のようになります。

id bitemporal_id name position valid_from valid_to created_at deleted_at
1 1 田中 平社員 2018-01-01 9999-12-31 2018-01-3 NULL
次に、

2020年1月1日 課長に昇進

Employee.valid_at("2020-01-01".to_date).find_by(bitemporal_id: 1).update(position: "課長")

レコードの更新をするとき、適用日時を指定したい場合はvalid_atメソッドで適用日時を指定してからレコードを作成します。
valid_atメソッドで時間を指定しなかった場合、valid_fromは入力時点現在の時刻となります。

ちなみにEmployee.find_by(bitemporal_id: 1).update(position: "課長", valid_from: "2020-01-01")としても意味はなく、valid_fromは入力時点現在の時刻となります。
この辺りの仕組みは時間があればソースコードを確認しましょう。

このときDBは以下のようになります。

id bitemporal_id name position valid_from valid_to created_at deleted_at
1 1 田中 平社員 2018-01-01 9999-12-31 2018-01-3 2020-01-10
2 1 田中 平社員 2018-01-01 2020-01-01 2020-01-10 NULL
3 1 田中 課長 2020-01-01 9999-12-31 2020-01-10 NULL
次に、

2020年1月20日 課長に更新したと思ったが、間違えて家長と入力していたので課長に修正

前の部分では課長として更新しましたが誤字のため家長と入力し、それを修正していく場合の時を考えます。

Employee.find_by(bitemporal_id: 1, position: "家長").force_update do |employee| 
   employee.update(position: "課長") 
end

この場合は、updateではなく、force_updateを使います。
家長と入力したレコードは論理削除され、課長のレコードが新しく作られます。

このときDBは以下のようになります。

id bitemporal_id name position valid_from valid_to created_at deleted_at
1 1 田中 平社員 2018-01-01 9999-12-31 2018-01-3 2020-01-10
2 1 田中 平社員 2018-01-01 2020-01-01 2020-01-10 NULL
3 1 田中 家長 2020-01-01 9999-12-31 2020-01-20
4 1 田中 課長 2020-01-01 00:00:00 9999-12-31 2020-01-20 NULL

もしこのとき、論理削除をして新しいレコードを作成するのではなく、強制的に上書きしたい場合は

Employee.find_by(bitemporal_id: 1, position: "家長").force_update do |employee| 
  employee.update_column(position: "課長") 
end

としましょう。

レコードの検索

後日、執筆予定

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

[Rails]schema.rbのコンフリクト解消

はじめに

チーム開発をしている際に、プルリクエストを作成したところconflictが発生した。
他のファイルはメンバーに相談しながら解消できたが、
Railsで自動更新されるschema.rbファイルは勝手に修正して良いのだろうか?と詰まった。

解消

以下の記事を参考に修正を試みた。
コンフリクトしたschema.rbをきれいにマージする手順

ターミナル
$ git checkout master

を実行しようとしたところ、
error: you need to resolve your current index first
と出てしまい、ブランチの切り替えが出来なかった。

ターミナル
git merge --abort

これで一旦前の状態に戻すことで、ブランチの切り替えが可能になりました。以降は、上記の記事を参考にコンフリクトを解消し、マージすることが出来ました。

参考記事
【git】マージしたけどやっぱりやめたい時のやり方4種類

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

【Rails】ユーザー論理削除の実装

目標

ezgif.com-video-to-gif.gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

Slim導入
Bootstrap3導入
ログイン機能実装
devise日本語化

実装

1.カラムを追加

ターミナル
$ rails g migration AddIsValidToUsers is_valid:boolean
~_add_is_valid_to_users.rb
class AddIsValidToUsers < ActiveRecord::Migration[5.2]
  def change
    # 「default: true」と「null: false」を追記
    add_column :users, :is_valid, :boolean, default: true, null: false
  end
end
ターミナル
$ rails db:migrate

2.モデルを編集

user.rb
# 追記
enum is_valid: { '有効': true, '退会済': false }

def active_for_authentication?
  super && self.is_valid == '有効'
end

【解説】

① ユーザーの状態をenumで管理する。

enum is_valid: { '有効': true, '退会済': false }

② is_validが有効であればtrueを返すメソッドを定義する。

def active_for_authentication?
  super && self.is_valid == '有効'
end

3.session_controller.rbを編集

session_controller.rb
# 追記
protected

  def reject_user
    user = User.find_by(email: params[:user][:email].downcase)
    if user
      if (user.valid_password?(params[:user][:password]) && (user.active_for_authentication? == true))
        redirect_to new_user_session_path
      end
    end
  end

【解説】

① 入力されたメールアドレスに対応するユーザーが存在するかを確認する。

user = User.find_by(email: params[:user][:email].downcase)

② 入力されたパスワードが正しい場合かつ、2で定義したメソッドの返り値がtrueだった場合は、ログイン処理を行わずにログイン画面に遷移する。

if (user.valid_password?(params[:user][:password]) && (user.active_for_authentication? == true))
  redirect_to new_user_session_path
end

4.ビューを編集

Bootstrap3のアラートコンポーネントを使用してフラッシュメッセージを表示する。

sessions/new.html.slim
/ 追記
- if flash.present?
  .alert.alert-danger.alert-dismissible.fade.in role='alert'
    button.close type='button' data-dismiss='alert'
      span aria-hidden='true'
        | ×
    - flash.each do |name, msg|
      = content_tag :div, msg, :id => 'flash_#{ name }' if msg.is_a?(String)

      p
        a href='#' data-dismiss='alert'
          | 閉じる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】deviseを日本語化する方法

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

ログイン機能実装

実装

1.Gemを導入

Gemfile
# 追記
gem 'devise-i18n'

2.application.rbを編集

application.rb
module Bookers2Debug
  class Application < Rails::Application
    config.load_defaults 5.2
    config.i18n.default_locale = :ja # 追記
  end
end

devise.ja.ymlファイルを作成し、編集

$ touch config/locales/devise.ja.yml
devise.ja.yml
ja:
  activerecord:
    errors:
      models:
        user:
          attributes:
            email:
              taken: "は既に使用されています。"
              blank: "が入力されていません。"
              too_short: "は%{count}文字以上に設定して下さい。"
              too_long: "は%{count}文字以下に設定して下さい。"
              invalid: "は有効でありません。"
            password:
              taken: "は既に使用されています。"
              blank: "が入力されていません。"
              too_short: "は%{count}文字以上に設定して下さい。"
              too_long: "は%{count}文字以下に設定して下さい。"
              invalid: "は有効でありません。"
              confirmation: "が内容とあっていません。"
    attributes:
      user:
        current_password: "現在のパスワード"
        name: 名前
        email: "メールアドレス"
        password: "パスワード"
        password_confirmation: "確認用パスワード"
        remember_me: "次回から自動的にログイン"
        name: 氏名
        sex: 性別
        postcode: 郵便番号
        prefecture_code: 都道府県
        address_city: 市区町村
        address_street: 番地
        address_building: 建物名
    models:
      user: "ユーザー"
  devise:
    confirmations:
      new:
        resend_confirmation_instructions: "アカウント確認メール再送"
    mailer:
      confirmation_instructions:
        action: "アカウント確認"
        greeting: "ようこそ、%{recipient}さん!"
        instruction: "次のリンクでメールアドレスの確認が完了します:"
      reset_password_instructions:
        action: "パスワード変更"
        greeting: "こんにちは、%{recipient}さん!"
        instruction: "誰かがパスワードの再設定を希望しました。次のリンクでパスワードの再設定が出来ます。"
        instruction_2: "あなたが希望したのではないのなら、このメールは無視してください。"
        instruction_3: "上のリンクにアクセスして新しいパスワードを設定するまで、パスワードは変更されません。"
      unlock_instructions:
        action: "アカウントのロック解除"
        greeting: "こんにちは、%{recipient}さん!"
        instruction: "アカウントのロックを解除するには下のリンクをクリックしてください。"
        message: "ログイン失敗が繰り返されたため、アカウントはロックされています。"
    passwords:
      edit:
        change_my_password: "パスワードを変更する"
        change_your_password: "パスワードを変更"
        confirm_new_password: "確認用新しいパスワード"
        new_password: "新しいパスワード"
      new:
        forgot_your_password: "パスワードを忘れましたか?"
        send_me_reset_password_instructions: "パスワードの再設定方法を送信する"
    registrations:
      edit:
        are_you_sure: "本当に良いですか?"
        cancel_my_account: "アカウント削除"
        currently_waiting_confirmation_for_email: "%{email} の確認待ち"
        leave_blank_if_you_don_t_want_to_change_it: "空欄のままなら変更しません"
        title: "%{resource}編集"
        unhappy: "気に入りません"
        update: "更新"
        we_need_your_current_password_to_confirm_your_changes: "変更を反映するには現在のパスワードを入力してください"
      new:
        sign_up: "アカウント登録"
    sessions:
      new:
        sign_in: "ログイン"
    shared:
      links:
        back: "戻る"
        didn_t_receive_confirmation_instructions: "アカウント確認のメールを受け取っていませんか?"
        didn_t_receive_unlock_instructions: "アカウントの凍結解除方法のメールを受け取っていませんか?"
        forgot_your_password: "パスワードを忘れましたか?"
        sign_in: "ログイン"
        sign_in_with_provider: "%{provider}でログイン"
        sign_up: "アカウント登録"
    unlocks:
      new:
        resend_unlock_instructions: "アカウントの凍結解除方法を再送する"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Digdag公式ドキュメントからDigdagを学ぶ-Operators①Workflow control operators

目標

Digdagの公式サイトのドキュメントのOperatorsの翻訳+α
DigdagのRubyを使ってRailsにバッチを作るまでが最後の目標
http://docs.digdag.io/operators/workflow_control.html

目次

Getting started
Architecture
Concepts
Workflow definition
Scheduling workflow
Operators
Command reference
Language API -Ruby
REST API
Internal architecture
Release Notes

Operators

Workflow control operators

call>: Call another workflow

workfolw1.dig
timezone: Asia/Tokyo

+step1:
  call>: another_workflow.dig
+step2:
  call>: common/shared_workflow.dig
another_workflow.dig
+step1:
    sh>: echo hi! another_workflow.dig
/common/shared_workflow.dig
+step1:
    sh>: echo hi! ./common/shared_workflow.dig
結果
$ digdag run workflow1.dig --rerun
2020-07-12 13:38:54 +0900 [INFO] (0017@[0:default]+workflow1+step1): call>: another_workflow.dig
2020-07-12 13:38:54 +0900 [INFO] (0017@[0:default]+workflow1+step1^sub+step1): sh>: echo hi! another_workflow.dig
hi! another_workflow.dig
2020-07-12 13:38:54 +0900 [INFO] (0017@[0:default]+workflow1+step2): call>: common/shared_workflow.dig
2020-07-12 13:38:55 +0900 [INFO] (0017@[0:default]+workflow1+step2^sub+step1): sh>: echo hi! ./common/shared_workflow.dig
hi! ./common/shared_workflow.dig

call>: FILE
FILEにはワークフロー定義ファイルへのパスが入ります。
ァイル名は.digで終わる必要があります。
呼び出されたワークフローがサブディレクトリにある場合、ワークフローはサブディレクトリを作業ディレクトリとして使用します。
例)タスクにはcall>:common/called_workflow.digが定義されている。呼び出されたワークフローでquerys/data.sqlファイルを参照した場合は../queries/data.sqlで参照する。

call>: another_workfloww.dig

http_call>: Call workflow fetched by HTTP

http_call> オペレーターは、HTTP要求を作成し、応答本文をワークフローとして解析それをサブタスクとして埋め込みます。call>オペレーターに似ています。違いは、別のワークフローがHTTPからフェッチされることです。

この演算子は、返されたContent-Typeヘッダーに基づいて応答本文を解析します。 Content-Typeを設定する必要があり、次の値がサポートされています。

application/json: 応答をJSONとして解析します。
application/x-yaml: 返された本文をそのまま使用します。
適切なContent-Typeヘッダーが返されない場合は、content_type_overrideオプションを使用します。

Options
content_type_override:サーバーから返されたContent-Type応答ヘッダーをオーバーライドします。このオプションは、サーバーが適切なContent-Typeを返さないが、text/plainapplication/octet-streamなどの一般的な値を返す場合に役立ちます。

http_call>: https://api.example.com/foobar
content_type_override: application/x-yaml

require>: Depends on another workflow

require> オペレーターは、別のワークフローの完了を要求します。
このオペレーターはcall>オペレーターに似ていますが、このオペレーターは、既に実行されている場合、またはこのワークフローの同じセッション時間に実行されている場合、他のワークフローを開始しません。
ワークフローが実行中または新しく開始された場合、このオペレーターはワークフローが完了するまで待機します。さらにrequireオペレーターは別のプロジェクトのワークフローを開始することができます。

workflow1.dig
+step1:
  require>: another_workflow
another_workflow.dig
+step2:
  sh>: echo step2
実行結果
$ digdag run workflow1.dig --rerun
2020-07-12 14:55:34 +0900 [INFO] (0017@[0:default]+workflow1+step1): require>: another_workflow
2020-07-12 14:55:34 +0900 [INFO] (0017@[0:default]+workflow1+step1): Starting a new session project id=1 workflow name=another_workflow session_time=2020-07-11T15:00:00+00:00
2020-07-12 14:55:34 +0900 [INFO] (0017@[0:default]+another_workflow+step2): sh>: echo step2
step2

Options
project_id: project_id
project_name: project_name
project_idまたはproject_nameを設定することで、別のプロジェクトのワークフローを開始できます。プロジェクトが存在しない場合、タスクは失敗します。 project_idとproject_nameの両方を設定した場合、タスクは失敗します。

require>: another_project_wf
project_id: 12345

require>: another_project_wf
project_name: another_project

rerun_on: none, failed, all (default: none)
もし依存するワークフローの試行が存在したらrerun_onrequire>を実際開始するかどうかコントロールします。
none: 試行がすでに存在する場合、ワークフローを開始しません。
failed: 試行が存在し、その結果が成功しない場合、ワークフローを開始します。
all: require>試行の結果に関係なくワークフローを開始します。

ignore_failure:BOOLEAN
依存ワークフローがデフォルトでエラーで終了した場合、このオペレーターは失敗します。
ただし、ignore_failure:trueが設定されている場合、ワークフローがエラーで終了した場合でも、このオペーレーターは成功します。

require>: another_workflow
ignore_failure: true

params:MAP
このオペレーターはrequireに設定されたワークフローにパラメーターを渡します。
パ別のワークフローには渡しません。

workflow1.dig
+step1:
  require>: another_workflow
  params:
    param_name1: hello
another_workflow
+step2:
  sh>: echo step2:${param_name1}
実行結果
$ digdag run workflow1.dig --rerun
2020-07-12 15:19:34 +0900 [INFO] (0017@[0:default]+workflow1+step1): require>: another_workflow
2020-07-12 15:19:34 +0900 [INFO] (0017@[0:default]+workflow1+step1): Starting a new session project id=1 workflow name=another_workflow session_time=2020-07-11T15:00:00+00:00
2020-07-12 15:19:34 +0900 [INFO] (0017@[0:default]+another_workflow+step2): sh>: echo step2:hello
step2:hello

loop>: Repeat tasks

loop>オペレーターは、サブタスクを複数回実行します。

このオペレーターは、サブタスクの$ {i}変数をエクスポートします。その値は0から始まります。たとえば、countが3の場合、タスクはi = 0、i = 1、およびi = 2で実行されます。

workflow1.dig
+repeat:
  loop>: 7
  _do:
    +step1:
      echo>: ${moment(session_time).add(i, 'days')} is ${i} days later than ${session_date}
    +step2:
      echo>: ${moment(session_time).add(i, 'hours')} is ${i} hours later than ${session_local_time}.
結果
$ digdag run workflow1.dig --rerun

2020-07-12 16:19:04 +0900 [INFO] (0017@[0:default]+workflow1+repeat): loop>: 7
2020-07-12 16:19:05 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+loop-0+step1): echo>: "2020-07-11T15:00:00.000Z" is 0 days later than 2020-07-11
"2020-07-11T15:00:00.000Z" is 0 days later than 2020-07-11
2020-07-12 16:19:06 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+loop-0+step2): echo>: "2020-07-11T15:00:00.000Z" is 0 hours later than 2020-07-11 15:00:00.
"2020-07-11T15:00:00.000Z" is 0 hours later than 2020-07-11 15:00:00.
2020-07-12 16:19:06 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+loop-1+step1): echo>: "2020-07-12T15:00:00.000Z" is 1 days later than 2020-07-11
"2020-07-12T15:00:00.000Z" is 1 days later than 2020-07-11
2020-07-12 16:19:07 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+loop-1+step2): echo>: "2020-07-11T16:00:00.000Z" is 1 hours later than 2020-07-11 15:00:00.
"2020-07-11T16:00:00.000Z" is 1 hours later than 2020-07-11 15:00:00.
2020-07-12 16:19:07 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+loop-2+step1): echo>: "2020-07-13T15:00:00.000Z" is 2 days later than 2020-07-11
"2020-07-13T15:00:00.000Z" is 2 days later than 2020-07-11
2020-07-12 16:19:07 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+loop-2+step2): echo>: "2020-07-11T17:00:00.000Z" is 2 hours later than 2020-07-11 15:00:00.
"2020-07-11T17:00:00.000Z" is 2 hours later than 2020-07-11 15:00:00.
2020-07-12 16:19:08 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+loop-3+step1): echo>: "2020-07-14T15:00:00.000Z" is 3 days later than 2020-07-11
"2020-07-14T15:00:00.000Z" is 3 days later than 2020-07-11
2020-07-12 16:19:08 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+loop-3+step2): echo>: "2020-07-11T18:00:00.000Z" is 3 hours later than 2020-07-11 15:00:00.
"2020-07-11T18:00:00.000Z" is 3 hours later than 2020-07-11 15:00:00.
2020-07-12 16:19:08 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+loop-4+step1): echo>: "2020-07-15T15:00:00.000Z" is 4 days later than 2020-07-11
"2020-07-15T15:00:00.000Z" is 4 days later than 2020-07-11
2020-07-12 16:19:09 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+loop-4+step2): echo>: "2020-07-11T19:00:00.000Z" is 4 hours later than 2020-07-11 15:00:00.
"2020-07-11T19:00:00.000Z" is 4 hours later than 2020-07-11 15:00:00.
2020-07-12 16:19:09 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+loop-5+step1): echo>: "2020-07-16T15:00:00.000Z" is 5 days later than 2020-07-11
"2020-07-16T15:00:00.000Z" is 5 days later than 2020-07-11
2020-07-12 16:19:09 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+loop-5+step2): echo>: "2020-07-11T20:00:00.000Z" is 5 hours later than 2020-07-11 15:00:00.
"2020-07-11T20:00:00.000Z" is 5 hours later than 2020-07-11 15:00:00.
2020-07-12 16:19:10 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+loop-6+step1): echo>: "2020-07-17T15:00:00.000Z" is 6 days later than 2020-07-11
"2020-07-17T15:00:00.000Z" is 6 days later than 2020-07-11
2020-07-12 16:19:10 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+loop-6+step2): echo>: "2020-07-11T21:00:00.000Z" is 6 hours later than 2020-07-11 15:00:00.
"2020-07-11T21:00:00.000Z" is 6 hours later than 2020-07-11 15:00:00.

Options
_parallel:BOOLEAN
タスクを並列に実行
_parallel:true
_do: TASKS: loop内で実行されるタスク

for_each>: Repeat tasks for values

for_each> オペレーターは変数セットを使ってサブタスクを複数実行する

workflow1.rb
+repeat:
  for_each>:
    fruit: [apple, orange]
    verb: [eat, throw]
  _do:
    echo>: ${verb} ${fruit}

結果
$ digdag run workflow1.dig --rerun
2020-07-12 16:27:00 +0900 [INFO] (0017@[0:default]+workflow1+repeat): for_each>: {fruit=[apple, orange], verb=[eat, throw]}
2020-07-12 16:27:01 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+for-0=fruit=0=apple&1=verb=0=eat): echo>: eat apple
eat apple
2020-07-12 16:27:01 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+for-0=fruit=0=apple&1=verb=1=throw): echo>: throw apple
throw apple
2020-07-12 16:27:01 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+for-0=fruit=1=orange&1=verb=0=eat): echo>: eat orange
eat orange
2020-07-12 16:27:02 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+for-0=fruit=1=orange&1=verb=1=throw): echo>: throw orange
throw orange

Options
for_each>: VARIABLES
キーのループで使用される変数:[値、値、...]構文。
変数は、オブジェクトまたはJSON文字列です。

例1
for_each>: {i: [1, 2, 3]}
例2
for_each>: {i: '[1, 2, 3]'}

_parallel:BOOLEAN
反復処理のタスク後並列に実行

_do:TASKS
実行されるタスク

for_range>: Repeat tasks for a range

for_range> オペレーターは、変数のセットを使用してサブタスクを複数回実行します。

このオペレーターは、サブタスクの${range.from}${range.to} 、および${range.index} 変数をエクスポートします。インデックスは0から始まります。

workflow1.dig
+repeat:
  for_range>:
    from: 10
    to: 50
    step: 10
  _do:
    echo>: processing from ${range.from} to ${range.to}.
結果
$ digdag run workflow1.dig --rerun
2020-07-12 16:47:17 +0900 [INFO] (0017@[0:default]+workflow1+repeat): for_range>: {from=10, to=50, step=10}
2020-07-12 16:47:18 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+range-from=10&to=20): echo>: processing from 10 to 20.
processing from 10 to 20.
2020-07-12 16:47:18 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+range-from=20&to=30): echo>: processing from 20 to 30.
processing from 20 to 30.
2020-07-12 16:47:18 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+range-from=30&to=40): echo>: processing from 30 to 40.
processing from 30 to 40.
2020-07-12 16:47:19 +0900 [INFO] (0017@[0:default]+workflow1+repeat^sub+range-from=40&to=50): echo>: processing from 40 to 50.
processing from 40 to 50.

Options
for_range>:
slices: 反復をslicesで指定した数で分割して実行

for_range>:
  from: 0
  to: 10
  slices: 3
  # this repeats tasks for 3 times (size of a slice is computed automatically):
  #  * {range.from: 0, range.to: 4, range.index: 0}
  #  * {range.from: 4, range.to: 8, range.index: 1}
  #  * {range.from: 8, range.to: 10, range.index: 2}
_do:
  echo>: from ${range.from} to ${range.to}

_parallel:BOOLEAN
反復処理のタスク後並列に実行

_do:TASKS
実行されるタスク

if>: Conditional execution

tureの場合_doのサブタスクを実行する
falseの場合_else_doのサブタスクを実行

workflow1.dig
+run_if_param_is_false:
  if>: ${param}
  _do:
    echo>: ${param} == true
  _else_do:
    echo>: ${param} == false
param_true
$ digdag run workflow1.dig --rerun -p param=true
2020-07-12 17:01:32 +0900 [INFO] (0017@[0:default]+workflow1+run_if_param_is_false): if>: true
2020-07-12 17:01:33 +0900 [INFO] (0017@[0:default]+workflow1+run_if_param_is_false^sub): echo>: true == true
true == true
param_false
$ digdag run workflow1.dig --rerun -p param=false
2020-07-12 17:01:14 +0900 [INFO] (0017@[0:default]+workflow1+run_if_param_is_false): if>: false
2020-07-12 17:01:15 +0900 [INFO] (0017@[0:default]+workflow1+run_if_param_is_false^sub): echo>: false == false
false == false

fail>: Makes the workflow failed

検証に失敗した場合実行される

+fail_if_too_few:
  if>: ${count < 10}
  _do:
    fail>: count is less than 10!
count_11
$ digdag run workflow1.dig --rerun -p count=11
2020-07-12 17:05:52 +0900: Digdag v0.9.41
2020-07-12 17:05:54 +0900 [WARN] (main): Reusing the last session time 2020-07-11T15:00:00+00:00.
2020-07-12 17:05:54 +0900 [INFO] (main): Using session /Users/akira/Desktop/ruby/sample/workflows/.digdag/status/20200711T150000+0000.
2020-07-12 17:05:54 +0900 [INFO] (main): Starting a new session project id=1 workflow name=workflow1 session_time=2020-07-11T15:00:00+00:00
2020-07-12 17:05:55 +0900 [INFO] (0017@[0:default]+workflow1+fail_if_too_few): if>: false

count_9
$ digdag run workflow1.dig --rerun -p count=9
2020-07-12 17:05:46 +0900 [INFO] (0017@[0:default]+workflow1+fail_if_too_few): if>: true
2020-07-12 17:05:47 +0900 [INFO] (0017@[0:default]+workflow1+fail_if_too_few^sub): fail>: count is less than 10!
2020-07-12 17:05:47 +0900 [ERROR] (0017@[0:default]+workflow1+fail_if_too_few^sub): Task +workflow1+fail_if_too_few^sub failed.
count is less than 10!
2020-07-12 17:05:47 +0900 [INFO] (0017@[0:default]+workflow1^failure-alert): type: notify
error: 
  * +workflow1+fail_if_too_few^sub:
    count is less than 10!

echo>: Shows a message

メッセージ出力

+say_hello:
  echo>: Hello world!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Rspecでマクロを定義して処理を共通化する方法

目標

ログイン処理を共通化する。

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・rspec-rails: 4.0.1
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

実装

1.supportディレクトリを作成

$ mkdir support

2.supportディレクトリ内にファイルを作成し、編集

$ touch spec/support/login_macros.rb
login_macros.rb
module LoginMacros
  def login(user)
    fill_in 'メールアドレス', with: user.email
    fill_in 'パスワード', with: user.password
    click_button 'ログイン'
  end
end

3.rails_helper.rbを編集

rails_helper.rb
# 23行目をコメントアウト
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

RSpec.configure do |config|
  config.include LoginMacros # 追記
end

【解説】

supportディレクトリを読み込む。

Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

2で定義したモジュールを使用できるようにする。

config.include LoginMacros

4.メソッドを使用する

require 'rails_helper'

RSpec.describe '認証のテスト', type: :feature do
  let(:user) { create(:user) }
  subject { page }

  describe 'ユーザー認証のテスト' do
    context 'ユーザーログインのテスト' do
      it 'ログインできること' do
        visit new_user_session_path
        login(user) # メソッドを使用
        is_expected.to have_content 'ログアウト'
      end
    end
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

そのpreload、本当に必要ですか?〜遅延ロード活用〜

まずは下記のコードを見てください。

review = Review.preload(:user, :book).find_by(id: review_id)

このようなコードを見かけたとき、あなたはどうしますか?
私ならpreloadは付けなくて良いよ。と指摘すると思います。

この記事では、なぜこのpreloadは不要なのか説明したいと思います。

preloadとは

preloadをつけると指定した関連データを同時に取得することができます。
この例の場合、reviewを取得したときに関連するuserとbookも同時に取得します。

下記にirbで実行した結果を載せておきます。
reviewを取得したときにuserとbookもSELECTしており、実際に使うところではSQLが発行されていないことがわかります。

irb(main):011:0> review_id = 15
=> 15
irb(main):012:0> review = Review.preload(:user, :book).find_by(id: review_id)
  Review Load (0.8ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`id` = 15 LIMIT 1
  User Load (0.5ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1
  Book Load (0.8ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 1
=> #<Review id: 15, content: "hogehoge", user_id: 1, book_id: 1, status: "draft", created_at: "2020-06-15 14:21:23", updated_at: "2020-06-15 14:21:23">
irb(main):013:0> review.user
=> #<User id: 1, name: "1234567890", created_at: "2019-12-12 05:43:52", updated_at: "2019-12-12 05:43:52">
irb(main):014:0> review.book
=> #<Book id: 1, title: "book1", created_at: "2020-06-15 14:21:15", updated_at: "2020-06-15 14:21:15">

preloadはどういうときに使うのか?

主にN+1の対策で使われます。
N+1についてはここでは詳しくは述べませんが、下記のようにループなどで関連データの取得SQLが1件ずつ発行されるような事象のことです。

irb(main):022:0> Review.all.each do |review|
irb(main):023:1*   review.book
irb(main):024:1> end
  Review Load (0.6ms)  SELECT `reviews`.* FROM `reviews`
  Book Load (0.3ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 1 LIMIT 1
  Book Load (0.3ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 2 LIMIT 1
  Book Load (0.4ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 3 LIMIT 1
  Book Load (0.3ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 4 LIMIT 1
  Book Load (2.7ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 5 LIMIT 1

上記では、bookを事前に取得していないのでreview.bookのところで1件ずつSQLを発行しています。
prealodをつけて事前にbookを取得しておくと下記のようになります。

irb(main):025:0> Review.all.preload(:book).each do |review|
irb(main):026:1*   review.book
irb(main):027:1> end
  Review Load (0.8ms)  SELECT `reviews`.* FROM `reviews`
  Book Load (0.7ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` IN (1, 2, 3, 4, 5)

ループに入る前にReview.allで取得できたreviewに関連するbookを1つのSQLで取得していることがわかります。
ループ前にまとめで取得できているのでループ中にはSQLが発行されません。
一般的にSQL発行はコストがかかる処理なので、SQLが1回になることでパフォーマンスが向上します。
上記例でもSQLの合計実行時間をみるとパフォーマンスに差が出ていることがわかります。

なぜ今回は付けなくて良いのか?

では、最初の例の場合はどうでしょうか?
reviewを1件しか取得していないので先ほどのようにループでN+1になることはありえません。

preloadをしているということは少なくとものちに使う可能性があるということだと思います。
次の例を見てみましょう。

# userを取得
# あとでuserとreviewを使うのでpreloadしておく
review = Review.preload(:user, :book).find_by(id: review_id)

# userを使う
review.user

# bookを使う
review.book

preloadをつけているので、reviewを取得したときにuserやbookも取得されます。
実行結果は下記の通り。

irb(main):007:0> review = Review.preload(:user, :book).find_by(id: review_id)
  Review Load (0.8ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`id` = 36 LIMIT 1
  User Load (0.5ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1
  Book Load (0.4ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 1
=> #<Review id: 36, content: "", user_id: 1, book_id: 1, status: "draft", created_at: "2020-06-30 15:20:01", updated_at: "2020-06-30 15:20:01">
irb(main):008:0> review.user
=> #<User id: 1, name: "1234567890", created_at: "2019-12-12 05:43:52", updated_at: "2019-12-12 05:43:52">
irb(main):009:0> review.book
=> #<Book id: 1, title: "book1", created_at: "2020-06-15 14:21:15", updated_at: "2020-06-15 14:21:15">

では、もしpreloadをつけていなかったらどうなるでしょうか?

irb(main):010:0> review = Review.find_by(id: review_id)
  Review Load (0.7ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`id` = 36 LIMIT 1
=> #<Review id: 36, content: "", user_id: 1, book_id: 1, status: "draft", created_at: "2020-06-30 15:20:01", updated_at: "2020-06-30 15:20:01">
irb(main):011:0> review.user
  User Load (0.5ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, name: "1234567890", created_at: "2019-12-12 05:43:52", updated_at: "2019-12-12 05:43:52">
irb(main):012:0> review.book
  Book Load (0.6ms)  SELECT `books`.* FROM `books` WHERE `books`.`id` = 1 LIMIT 1
=> #<Book id: 1, title: "book1", created_at: "2020-06-15 14:21:15", updated_at: "2020-06-15 14:21:15">

reviewを取得したときにはuserとbookは取得されず、使っているところでSQLが発行されています。
ただ、reviewが一件しかないので発行されているSQLの数は一緒です。
この例の場合だと、preloadをつけてもつけなくても効率は同じですね

では、次の例ではどうでしょうか?

# userを取得
# あとでuserとreviewを使うのでpreloadしておく
review = Review.preload(:user, :book).find_by(id: review_id)

# ある条件の時はuserを使う
if hoge
  review.user
end

# ある条件の時はbookを使う
if fuga
  review.book
end

preloadをつけているので、reviewを取得したときにuserやbookも取得されます。
hogeやfugaがtrueの場合は、userもreviewも使うのでpreloadをしていてもしていなくてもSQLの数は一緒です。

では、falseの場合はどうでしょうか?
例えばhogeがfalseの場合はuserは使わないので、preloadで取得したuserを使うことはありません。
fugaがfalseの場合も同様にbookを使うことはありません。

今回はもしpreloadをつけていなかったらどうなるでしょうか?

# userを取得
review = Review.find_by(id: review_id)

# ある条件の時はuserを使う
if hoge
  review.user
end

# ある条件の時はbookを使う
if fuga
  review.book
end

reviewを取得したときはuserやbookは取得されません。
hogeやfugaがtrueの場合は使用する箇所でreviewやbookが取得されます。
もしfalseの場合は取得されません。

こちらの実装の場合は使用するときのみ取得することができます。
ちなみに、このように必要になったときにデータを取得する実装は遅延ロードと呼ばれています。

どちらの方が効率が良いかおわかりいただけたでしょうか?
1件のモデルに対してpreloadをした場合、preloadで取得したモデルを全部使った場合でもpreloadをつけていない場合とSQLの数は同じです。
もし1つでも条件によって使わないパターンがある場合はSQLの数が多くなります。

最初の例のように1件だけ取得する場合はpreloadをしても意味がなく、むしろ非効率になるので注意が必要です。

最後に

Railsを覚えたばかりの方などなんとなくpreloadやeager_loadを知っている場合、とりあえずつけておけばいいんでしょ?
と思っている方も多いと思います。

レビュアーとしてもN+1を指摘する人は多いけど、今回のような無駄なpreloadを指摘する人は少ないと感じています(個人の感想です)

N+1を倒してくれるpreloadやeager_loadはつけておいて悪いことはないと思われがちですが、今回のように非効率になってしまうパターンもあるので意識していなかった方は意識しておきましょう。

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

Railsでi18nを使った日本語化をする

環境:ruby 2.5.1 / rails 5.2.3

やりたいこと

  • エラーメッセージなど英語で表記される箇所を日本語の置き換えたい
  • DBのカラム名やclassの属性を表示する時に、予め日本語に置き換えたものを表示されるようにしたい

結論

  • gemのrails-i18nを導入する
  • 変換したい単語をja.ymlファイルに設定する

やり方

  • gemのrails-i18nを導入する
Gemfile
# 記述する場所はファイルの一番下か、group :development, group :development, :test 以外の場所に記述
gem 'rails-i18n'
  • gemをインストールする
  • config/application.rb内のデフォルトのlocale(ロケール)をjaにする
application.rb
# ↑これより上のコードは割愛
module App
  class Application < Rails::Application
    config.i18n.default_locale = :ja
    config.time_zone = 'Tokyo'
  end
end
  • config/locales/ja.ymlのファイルを作成する
  • ja.ymlの中に、日本語に変換したい設定をyml形式で記述する
    例:DBのカラムに関する文字 → activerecord: attributes: モデル名:
      viewに関する文字 → views: リソース名:
ja.yml
ja:
  activerecord:
    attributes:
      user:
        name: ユーザー名
        email: メール
        password: パスワード
        password_confirmation: パスワード(確認)
      tweet:
        name: 名前
        title: タイトル
        body: 本文
      comment:
        name: 名前
        comment: コメント
  views:
    pagination:
      first: 最初
      last: 最後
      previous: 
      next: 
      truncate: ...
  • 設定が完了したら、サーバーを立ち上げ直す(これをしないと反映されないため)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(Linux初心者)魔法の言葉auxとは

とりあえず使ってたけど、auxコマンドの意味とは

例題: ps aux | grep puma

以下、分解して見ていく。

まず、grepは特定の文字列を含むコマンドを検索したいときに使用する。

コマンド | grep 検索したい文字列

コマンドを詳しく見てみる。

ps ・・・ 自分のプロセスを簡単に表示
aux・・・ a、u、xオプションの組み合わせ
aオプション・・・ すべてのユーザーのプロセスを表示する
uオプション・・・ 各プロセスの実行ユーザーやCPU, メモリ等の情報も表示する
xオプション・・・ 端末を持たないすべてのプロセス(daemonなど)を表示する

daemon(デーモン)・・・バックグラウンドで動作するプロセス。コンピュータを使ってる人に見えない裏側で動作するもの。

要するにauxにすることですべての種類のプロセスの知り得るだけの情報がすべて網羅される。

解答 ps aux | grep pumaとは

【意味】pumaを含むコマンドの知りうる情報を全て表示する

つまり、迷ったらauxは間違いではなかった。

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

【Capistrano,Unicorn】ArgumentError: directory for pid=/var/www/badsuru/current/shared/tmp/pids/unicorn.pid not writableが書き込み権限の問題ではなかった

はじめに

Capistranoを用いた自動デプロイ中、タイトルのエラーが出てほぼ1日を費やしました・・・
結論、大したことではなく自分にがっかりしてしまいましたが、同じようなエラーで悩む方の手助けになれば幸いです。

対象者

  • 初学者
  • Capistrano設定中の方
  • Unicorn使用者

開発環境

  • Rails 6.0.3.1
  • ruby 2.7.1
  • unicorn 5.4.1
  • AWS Amazon Linux AMI 2018.03.0 (HVM), SSD Volume Type

この記事を通じて得られること

  • タイトルのエラーの原因・解決方法 ※あくまで1つのエラーの解決方法であることをご了承ください。エラーの原因によっては違う解決方法になることが考えられます。

結論(解決方法)

unicorn.rbの設定記述ミスです。以下の通り変更しました。
開発中のアプリのパスが違うために、unicorn.pidを作成する/var/www/badsuru/current/shared/tmp/pids/ディレクトリが見つからず、タイトルのエラーを吐き出していました。

変更前

unicorn.rb
//サーバ上でのアプリケーションコードが設置されているディレクトリを変数に入れておく
app_path = File.expand_path('../../', __FILE__)

//アプリケーションサーバの性能を決定する
worker_processes 1

// アプリケーションの設置されているディレクトリを指定
working_directory app_path

(以下省略)

変更後

unicorn.rb
//サーバ上でのアプリケーションコードが設置されているディレクトリを変数に入れておく
app_path = File.expand_path('../../../', __FILE__)

//アプリケーションサーバの性能を決定する
worker_processes 1

// アプリケーションの設置されているディレクトリを指定
// currentを指定
working_directory "#{app_path}/current"

(以下省略)

詳細

Capistarnoの自動設定ファイルを記述し、いざ実行したところ、以下のエラーが吐き出されました。

00:44 unicorn:start
      01 $HOME/.rbenv/bin/rbenv exec bundle exec unicorn -c /var/www/myapp/current/config/unicorn.rb -E deployment -D 
      01 bundler: failed to load command: unicorn (/var/www/myapp/shared/bundle/ruby/2.7.0/bin/unicorn)
      01 ArgumentError: directory for pid=/var/www/myapp/current/shared/tmp/pids/unicorn.pid not writable
      01   /var/www/myapp/shared/bundle/ruby/2.7.0/gems/unicorn-5.4.1/lib/unicorn/configurator.rb:100:in `block in reload'
      01   /var/www/myapp/shared/bundle/ruby/2.7.0/gems/unicorn-5.4.1/lib/unicorn/configurator.rb:96:in `each'
      01   /var/www/myapp/shared/bundle/ruby/2.7.0/gems/unicorn-5.4.1/lib/unicorn/configurator.rb:96:in `reload'
      01   /var/www/myapp/shared/bundle/ruby/2.7.0/gems/unicorn-5.4.1/lib/unicorn/configurator.rb:77:in `initialize'
      01   /var/www/myapp/shared/bundle/ruby/2.7.0/gems/unicorn-5.4.1/lib/unicorn/http_server.rb:77:in `new'
      01   /var/www/myapp/shared/bundle/ruby/2.7.0/gems/unicorn-5.4.1/lib/unicorn/http_server.rb:77:in `initialize'
      01   /var/www/myapp/shared/bundle/ruby/2.7.0/gems/unicorn-5.4.1/bin/unicorn:126:in `new'
      01   /var/www/myapp/shared/bundle/ruby/2.7.0/gems/unicorn-5.4.1/bin/unicorn:126:in `<top (required)>'
      01   /var/www/myapp/shared/bundle/ruby/2.7.0/bin/unicorn:23:in `load'
      01   /var/www/myapp/shared/bundle/ruby/2.7.0/bin/unicorn:23:in `<top (required)>'
      01 master failed to start, check stderr log for details
(省略)

当初、unicorn.pid not writableと記述があったので、権限周りのエラーかと思い、releases,current,shared...などなど様々なディレクトリに書き込み権限を与えても解決されず、途方にくれていました。

また、mkdir pidsコマンド等で予めディレクトリを作成しなければならないという情報をググって見つけて試したけど上手く行かず・・・
見直したつもりの設定ファイルの記述を丁寧に見直したら結論の間違えに気が付きました。

推測になってしまいますが、unicornの起動と共にunicorn.pidsファイルを設定ディレクトリ配下に作成するのですが、unicornを実行させるアプリケーションのディレクトリ設定が間違えている状態です。unicorn.pidsファイルを作成したいのだけど、そのディレクトリにも辿りつけないから、見つからないというメッセージの代わりに、タイトルのエラーが吐き出されるようです。確かに、エラーの解決法を探している時も、設定ファイルの記述を指摘する記事もあったなあ・・・

終わりに

エラーが出て、指摘通りの内容を修正してもまだ出る時は、エラー文と違うミスの可能性も十分に考えられること。自分が記述してきたファイルをしっかり見直ししようという教訓になりました。

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

Rails6でGemを使わずシェアボタンを実装する

Railsでページ内に共有シェアボタンがあればいいなっと思いネットでいろいろ調べたのですが
Gemに依存したものが多かったので、自分用のためにも記事を残すことにしました。

環境

Rails: 6.0.2.1

シェアボタン用のアイコンを用意

まずシェアボタン用のアイコンを用意しましょう。
今回はFont Awesomeを使います。

Font Awesomeはyarnからインストールします。

yarn add @fortawesome/fontawesome-free

まだプロジェクトにWebpackerをinstallしていない場合は下記のコマンド後にインストールを行ってください。

rails webpacker:install

インストールが完了したら、app/javascript/の配下のファイルにインストールしたFont Awesomeをインポートしていきます。

application.js

require("@fortawesome/fontawesome-free/js/all")
import '../stylesheets/application';

scss側にも読み込んでいきます。

mkdir app/javascript/stylesheets
touch app/javascript/stylesheets/application.scss

application.scss

$fa-font-path: "~font-awesome/fonts/";
@import '@fortawesome/fontawesome-free/scss/fontawesome';

views/application.html.erbのstyle_sheet_link_tagとjavascript_pack_tagの部分も下記のように変更しましょう。

  <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  <%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>   

最後にさきほどのapplication.scssのアイコンカラー用にcssを追加してあげて準備完了です。

/* Twitter icon */

.twitter{color: #1da1f2}

/* Facebook icon */

.facebook{color: #4267b2}

リンクの設置

今回はviews側に下記のようにシェアボタンを設定しました。

<%= link_to "https://twitter.com/intent/tweet?url=http://localhost:3000/" do %>
  <i class="fab fa-twitter-square fa-4x twitter"></i>
<% end %>

<%= link_to "https://www.facebook.com/share.php?u=http://localhost:3000/" do %>
  <i class="fab fa-facebook-square fa-4x facebook"></i>
<% end %>

TwitterのシェアボタンはTweet Web Intentを使ってシェアできるようにしています。
今回はurlのみの設定になっていますが、他にも下記の設定ができるみたいです。

option 内容
text 本文の設定
hashtag ハッシュタグの設定
url URLの設定

※複数設定する場合は&で繋げる

Facebook側のシェアボタンはhttps://www.facebook.com/share.php?u=末尾にシェアしたいURLを記載することでシェアボタンの実装ができるようになります。

最後に

今回設定してあるURLはどちらもlocalhostのURLになっています。
実際に実装するときは各自シェアしたいリンクに変えること忘れないように注意しましょう。

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