- 投稿日:2020-07-13T23:11:31+09:00
Rails6:ActionTextの初期データをseedを使って投入する
はじめに
Rails6から使えるようになったActionTextを使ってみたのですが、初期データを入れるときにつまずいてしまったので、そのときのことを記事にしようと思いました。
前提
Rails:6.0.3.2
使うモデル名:Post
カラム:title
リッチテキストのフィールド名:contentseedの投入
まず簡単に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) endrecord_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側を確認すると改行と太文字が入った初期データを用意することができていると思います。
余談
画像つきの記事って初期データから用意できるのかな。。
- 投稿日:2020-07-13T22:54:58+09:00
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
- 投稿日:2020-07-13T22:52:57+09:00
CarrierWaveによって保存されたデータの中身。
調査した背景
CarrierWaveによって画像データをアップロードしたのですが、そのデータを更新したいとき、どのようにデータが入っているか調べました。
(この記事を見られている方ももしかすると、参照された方もいるかもしれません)データベースでは以下のとおり、ファイル名のみが表示されます(image列)
DBのUIは「sequelpro」を使っています。列値の詳細を参照しても、ファイル名のみです。
環境
項目 内容 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)>今回は以上です。
- 投稿日:2020-07-13T22:27:01+09:00
Ruby で任意個の整数の最大公約数・最小公倍数を求める
Ruby で整数
a
とb
の最大公約数,最小公倍数を得るには,それぞれ Integer#gcd,Integer#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といった具合。
では三つの整数
a
,b
,c
の最大公約数,最小公倍数は?
$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) # => 60Enumerable#inject にはブロックを与えずにシンボルを与える用法があったよね。
- 投稿日:2020-07-13T22:26:29+09:00
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:react3. 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で確認
「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' %>9. ルーティング設定
config/routes.rb
Rails.application.routes.draw do # ルートページ設定 root "top#show" end10. エントリーポイントの作成
・ルートページのコンロトーラー作成
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
で解決できる場合もある
- 投稿日:2020-07-13T22:10:00+09:00
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) /xgsubについて
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
- 投稿日:2020-07-13T20:23:46+09:00
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
- 投稿日:2020-07-13T20:07:28+09:00
個人アプリ作成#3
- 投稿日:2020-07-13T19:13:50+09:00
Rails5でECサイトを作る⑨ ~カート機能を作る~
はじめに
架空のベーカリーで買い物できるECサイトを作るシリーズ、Rails5でECサイトを作る⑧の続きです。
今回はカート機能を作っていきます。ようやくECサイトの体裁が整いますね。ソースコード
https://github.com/Sn16799/bakeryFUMIZUKI
Modelのアソシエーション
カート機能に関連するモデルは主にCustomerとProductです。カート機能ではカートの本体のようなものはなく、商品を選んで「カートに入れる」ボタンを押すと、各商品につき1件のデータが登録される仕組みになっています。CartItemモデルは、CustomerとProductのID、それと商品の個数のみを記憶する中間テーブル的な役割を果たします。
Controller
app/controllers/cart_items_controller.rbclass 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 endcreateアクションにおいてただ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.rbdef 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モデルを分かりやすく解説できるのか? 次回へ続く!
参考
- 投稿日:2020-07-13T14:52:22+09:00
【bcrypt】has_secure_passwordのpresence: trueを解除する方法
bcrptを利用しているけど、パスワードは任意で設定したい
bcrptで投稿にパスワードを設定できるようにしたのですが、
デフォルトで空の投稿は弾かれるように設定されているため、
それを解除する方法をご紹介します。※投稿にパスワードを設定する方法は以下で紹介してます。
バリテーションを無効にする
has_secure_passwordの横に(validations: false)をつけるだけです。
qiita.rbclass Post < ApplicationRecord has_secure_password(validations: false) endまとめ
バリテーションを無効にすることで空の投稿もできるようになりました。
- 投稿日:2020-07-13T14:18:27+09:00
【rails】投稿にパスワードを設定する方法
投稿にパスワードを設定する方法
投稿にパスワード機能を付けたのでそのアウトプット用に記事を書きました。
この記事のゴール
以下画像のようにパスワードを設定できるようにします。
投稿機能を作る
さくっと投稿機能を作成。
scaffoldを使えば1分で作れます。
$ rails new post_with_password
$ cd post_with_password
$ rails g scaffold Post description:text
$ rails db:migrate
bcryptを設定
以下投稿にパスワードを設定する流れ
1.投稿時にパスワードを設定
2.詳細ページを開く際にパスワードを要求
3.パスワードが一致すれば詳細ページにリダイレクトログイン機能以外で使われているところを見ないbcryptを使用します。
Gemfileに以下のgemがコメントアウトされているので#を削除。
gem 'bcrypt', '~> 3.1.7'
$ bundle install
次にモデルを少しだけいじります。
_form.html.erbclass 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.rbprivate # 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.rbRails.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.rbclass Post < ApplicationRecord has_secure_password(validations: false) endpasswaord_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が見れてしまいます。
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使ってあげてくださいね。
ポートフォリオなどの参考になれば嬉しいです。
- 投稿日:2020-07-13T13:48:25+09:00
ターミナルに 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. 実行したらこうなるよ
※ ネタ元は ここの コメント欄らしいです
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 end5. ワンラインに戻すよ
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. 実行したらこうなるよ
7. まとめ
タイトルはわざと頭悪そうにしてみました。釣りです ?
- 投稿日:2020-07-13T12:56:17+09:00
【Rails】クーポン機能の実装(バッチ処理を用いた自動削除機能付き)
目標
開発環境
・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.rbclass 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:migrate2.モデルを編集
user.rb# 追記 has_many :coupons, dependent: :destroycoupon.rbclass 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 end3.
coupons_controller.rb
を作成・編集ターミナル$ rails g controller coupons indexcoupons_controller.rbclass CouponsController < ApplicationController def index @coupons = Coupon.where(user_id: current_user.id, is_valid: '有効') end end4.
books_controller.rb
を編集今回は本を投稿成功した場合に、クーポンを発行するように実装します。
books_controller.rbdef 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 end5.日時設定を変更
①
application.rb
を編集する。application.rbmodule Bookers2Debug class Application < Rails::Application config.load_defaults 5.2 config.time_zone = 'Tokyo' # 追記 end end②日時のフォーマットを設定するファイルを作成・編集
ターミナル$ touch config/initializers/time_formats.rbtime_formats.rbTime::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.rbenv :PATH, ENV['PATH'] # 絶対パスから相対パス指定 set :output, 'log/cron.log' # ログの出力先ファイルを設定 set :environment, :development # 環境を設定 every 1.minute do runner 'Coupon.coupon_destroy' end4.
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を削除
- 投稿日:2020-07-13T12:26:49+09:00
[Rails]パンくずリストを作る
対象読者
- Railsでパンくずリストを実装したい人。
- 使い方忘れた人。
- 初学者向けになっています。内容も初歩的なところを解説しています。
gretelって何?
パンくずリストです。ヘンゼルとグレーテルの話のやつ。
パンくず落としていって自分の辿って来た道がわかる。Gemのインストール
Gemfilegem 'gretel'
bundle install
したら必要なファイルを生成します。$ bundle install $ rails g gretel:install以下のようにファイルが生成されればOKです。
Running via Spring preloader in process 6675 create config/breadcrumbs.rb
これが中身。
breadcrumbs.rbcrumb :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.rbcrumb :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: " › " %> #ここを追加 <%= 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.rbcrumb :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.rbcrumb :article_show do |article| link article.created_at, article_path(article) #変更(title => created_at) parent :articles endに変更すればOKです。
- 投稿日:2020-07-13T11:56:58+09:00
【Rails】ActiveRecord::Bitemporalの使い方(BiTemporalDataModel)
はじめに
RailsでBitemporalDataModelを扱いたかったのですが
ちょうどactiverecord-bitemporalという良いgemがあったので、
いろいろ触ってみた結果を書いていきたいと思います。BitemporalDataModelとは何か
そもそもBiTemporalとはどういう意味でしょうか。
「Bi」は接頭辞で2つのという意味を表します、Bicycleとか言いますよね。
「Temporal」は時間のという意味の形容詞です。
つまりBiTemporalで二つの時間のという意味になり、
BiTemporalDataModelは二つの時間のデータモデルになります。二つの時間は何かと言いますと、
「システム上の時間」と「事実情報としての時間」です。詳しくは説明すると長くなってしまうので、
こちらのスライドが参考になると思います。(特に33ページ目以降)使い方
レコードの作成、更新
ではactiverecord-bitemporalを使ってレコードを作成したり更新したりをしていきたいと思います。
まずは準備です。詳しくはこちら
テーブルを用意しましょう
db/schema.rbActiveRecord::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 endActiveRecord::Bitemporalの読み込みの設定をします。
app/models/emoloyee.rbclass 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としましょう。
レコードの検索
後日、執筆予定
- 投稿日:2020-07-13T10:31:19+09:00
[Rails]schema.rbのコンフリクト解消
はじめに
チーム開発をしている際に、プルリクエストを作成したところconflictが発生した。
他のファイルはメンバーに相談しながら解消できたが、
Railsで自動更新されるschema.rbファイルは勝手に修正して良いのだろうか?と詰まった。解消
以下の記事を参考に修正を試みた。
コンフリクトしたschema.rbをきれいにマージする手順ターミナル$ git checkout masterを実行しようとしたところ、
error: you need to resolve your current index first
と出てしまい、ブランチの切り替えが出来なかった。ターミナルgit merge --abortこれで一旦前の状態に戻すことで、ブランチの切り替えが可能になりました。以降は、上記の記事を参考にコンフリクトを解消し、マージすることが出来ました。
- 投稿日:2020-07-13T10:30:12+09:00
【Rails】ユーザー論理削除の実装
目標
開発環境
・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.rbclass 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:migrate2.モデルを編集
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 == '有効' end3.
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 end4.ビューを編集
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' | 閉じる
- 投稿日:2020-07-13T10:08:56+09:00
【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.rbmodule 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.ymldevise.ja.ymlja: 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: "アカウントの凍結解除方法を再送する"
- 投稿日:2020-07-13T10:01:20+09:00
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 NotesOperators
Workflow control operators
call>: Call another workflow
workfolw1.digtimezone: Asia/Tokyo +step1: call>: another_workflow.dig +step2: call>: common/shared_workflow.diganother_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.dighttp_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/plain
やapplication/octet-stream
などの一般的な値を返す場合に役立ちます。http_call>: https://api.example.com/foobar content_type_override: application/x-yamlrequire>: Depends on another workflow
require>
オペレーターは、別のワークフローの完了を要求します。
このオペレーターはcall>
オペレーターに似ていますが、このオペレーターは、既に実行されている場合、またはこのワークフローの同じセッション時間に実行されている場合、他のワークフローを開始しません。
ワークフローが実行中または新しく開始された場合、このオペレーターはワークフローが完了するまで待機します。さらにrequireオペレーターは別のプロジェクトのワークフローを開始することができます。workflow1.dig+step1: require>: another_workflowanother_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 step2Options
■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_on
はrequire>
を実際開始するかどうかコントロールします。
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: helloanother_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:helloloop>: 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 orangeOptions
■for_each>: VARIABLES
キーのループで使用される変数:[値、値、...]構文。
変数は、オブジェクトまたはJSON文字列です。例1for_each>: {i: [1, 2, 3]}例2for_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} == falseparam_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 == trueparam_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 == falsefail>: 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>: falsecount_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!
- 投稿日:2020-07-13T09:00:16+09:00
【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 support2.
support
ディレクトリ内にファイルを作成し、編集$ touch spec/support/login_macros.rblogin_macros.rbmodule LoginMacros def login(user) fill_in 'メールアドレス', with: user.email fill_in 'パスワード', with: user.password click_button 'ログイン' end end3.
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 LoginMacros4.メソッドを使用する
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
- 投稿日:2020-07-13T08:49:42+09:00
その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.bookpreloadをつけているので、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 endpreloadをつけているので、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 endreviewを取得したときは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はつけておいて悪いことはないと思われがちですが、今回のように非効率になってしまうパターンもあるので意識していなかった方は意識しておきましょう。
- 投稿日:2020-07-13T05:54:05+09:00
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.ymlja: activerecord: attributes: user: name: ユーザー名 email: メール password: パスワード password_confirmation: パスワード(確認) tweet: name: 名前 title: タイトル body: 本文 comment: name: 名前 comment: コメント views: pagination: first: 最初 last: 最後 previous: 前 next: 次 truncate: ...
- 設定が完了したら、サーバーを立ち上げ直す(これをしないと反映されないため)
- 投稿日:2020-07-13T02:01:36+09:00
(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は間違いではなかった。
- 投稿日:2020-07-13T00:44:00+09:00
【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ファイルを作成したいのだけど、そのディレクトリにも辿りつけないから、見つからないというメッセージの代わりに、タイトルのエラーが吐き出されるようです。確かに、エラーの解決法を探している時も、設定ファイルの記述を指摘する記事もあったなあ・・・
終わりに
エラーが出て、指摘通りの内容を修正してもまだ出る時は、エラー文と違うミスの可能性も十分に考えられること。自分が記述してきたファイルをしっかり見直ししようという教訓になりました。
- 投稿日:2020-07-13T00:27:08+09:00
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.scssapplication.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になっています。
実際に実装するときは各自シェアしたいリンクに変えること忘れないように注意しましょう。