write ahead log

ロールフォワード用

RailsのI18n色々

Rails5触ってみようとチュートリアル読んでからRailsの入門メモが増えてる...

とりあえずサンプルプロジェクトを用意する

scaffoldでタスクリストのページを用意します.

$ rails new i18n_sample
$ rails g scaffold Task title:string done:boolean
$ rails db:migrate

バリデーションメッセージも見たいので適当にチェックをモデルへ追加します.

# app/models/task.rb
class Task < ApplicationRecord
  validates :title, presence: true
end

rails-i18nのgemを導入する

バリデーションメッセージなどを国際化したいのでgemを導入する.

Gemfileに以下を追記.

gem 'rails-i18n'

忘れぬよう.

$ bundle install

I18nのデフォルトロケールを日本語にする

config/application.rbにデフォルトロケールを設定してやります.

# config/application.rb
...
module I18nSample
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.1

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.
    
    # locale setting                   #<== 追加
    config.i18n.default_locale = :ja   #<== 追加
  end
...
end

この時点でサーバを再起動してやると基本的なメッセージは日本語化されています.

素晴らしいですね.

日本語の言語ファイルを作成する

モデルの属性名などは当然まだ日本語化されていないので, 言語ファイルを作成します.

config/locale/ja.ymlを作成します.

$ touch config/locale/ja.yml

基本と言語ファイルの構成

言語ファイルの構成はざっくりこんな感じのようです.

規約的に以下のようになっているだけなので, 共通で使うものを言語名(jaとか)の下の階層に差し込んで色々な箇所から使ったりも普通にできます.

[言語名]:
    activerecord:
        models:
            [モデル名]:
            [モデル名]:
            ...
        attributes:
            [モデル名]:
                [属性名]: [対応リテラル]
                [属性名]: [対応リテラル]
            [モデル名]:
                [属性名]: [対応リテラル]
                [属性名]: [対応リテラル]
            ...
    [コントローラ名]:
        [アクション名]:
            [リテラル]: [対応リテラル]
            [リテラル]: [対応リテラル]
            ...

プログラムから利用する際にはtというメソッドを使います.

このメソッドの引数は上記yamlの構成をパスの様に指定して使う.

t('activerecord.models.[モデル名]')

みたいな感じ.

モデルの言語を登録する

scaffoldの内容に合わせて以下にしてみました.

ja:
    activerecord:
        models:
            task: タスク
        attributes:
            task:
                title: タスク
                done: 完了

これだけで, form_with+labelを使って作られている部分も変わってたりしてて良い感じです.

バリデーションメッセージの属性名もちゃんと日本語化されます.

モデルの名前と属性名を得る

上述の規約通りに言語ファイルを作成しておくと, ActiveRecordのメソッドで簡単に国際化されたリテラルを取得する事ができます.

例えばタスクのモデル名は以下の様に得られます.

   Task.model_name.human

属性名も以下の様に簡単に. 引数が属性名ですね.

Task.human_attribute_name(:title)

最初, humanって?と思いましたが, 人間が読めるという意味なんでしょうね.

ネストしたモデルの場合

この例ではないけれど, accept_nested_attributes_forを利用してネストしたモデルを受け入れる場合には一工夫必要になる.

例えば今回利用している例でサブタスクという概念があった場合, Modelの定義では以下の様になると思う.

class Task < ApplicationRecord
  has_many :sub_tasks
end

class SubTask < ApplicationRecord
  belongs_to :task
end

こういう場合の言語ファイルは以下になります.

ja:
    activerecord:
        models:
            task: タスク
        attributes:
            task:
                title: タスク
                done: 完了
            task/posts:
                title: サブタスク名
                done: 完了

親モデル/子モデル(複数形)です.

ビューのリテラルを多言語化する

例として * activerecordのリテラルでビューを表示 * ビュー用に追加したリテラルを利用 をやってみる.

# config/locale/ja.yml
ja:
    activerecord:
        models:
            task: タスク
        attributes:
            task:
                title: タスク
                done: 完了
    tasks:
        index:
            are_you_sure: 本当に削除しますか?

activerecordに設定したリテラルを利用する部分.

...
    <tr>
      <th><%= t('activerecord.attributes.task.title') %></th>
      <th><%= t('activerecord.attributes.task.done') %></th>
      <th colspan="3"></th>
...

追加したビュー用のリテラルを利用する部分.

     <td><%= link_to 'Destroy', task, method: :delete, data: { confirm: t('.are_you_sure') } %></td>

規約に従っておけば

t('tasks.index.are_you_sure')

のように冗長な形ではなく

t('.are_you_sure')

でよくなるので便利.

日付と時刻の国際化

国際化で困るのは単に言語だけではなく日時フォーマットもだけど, ここら辺のサポートが行き届いているのは良いなぁと.

yamlに以下のフォーマットで書いておくと日付と時間がtメソッドでフォーマットされる.

# config/locale/ja.yml
ja:
    date:
        formats:
            default: "%Y/%m/%d"
            long: "%Y年%m月%d日(%a)"
            short: "%m/%d"
    time:
        formats:
            default: "%Y/%m/%d %H:%M:%S"
            long: "%Y年%m月%d日(%a) %H時%M分%S秒 %z"
            short: "%y/%m/%d %H:%M"

使う時はこんな感じ. よしなにしてくれてよい.

<%= l(task.created_at) %>
<%= l(task.created_at, format: :short) %>

ざっくり見てきたけど, 式展開など多様な機能があるようなので, これ以上は一度Railsガイドを読んだ方が良いのだろうなぁ.

Rails5でwill_paginateを使う

ただの初心者メモ.

とりあえずプロジェクトを作る

$ rails new pagination

Gemを導入する

Gemfileに以下を追記.

gem 'will_paginate'

いつも通りbundleで更新する.

$ bundle install

サンプルページの用意

とりあえずScaffoldで準備しておく

$ rails g scaffold user name:string email:string
$ rails db:migrate

テストデータを用意

fixtureで適当に量産する.

#test/fixtures/users.yml

<% 1000.times do |n| %>
user_<%= n %>:
  name: <%= "user_#{n}" %>
  email: <%= "user_#{n}@example.com" %>
<% end %>
$rails db:fixtures:load

コントローラを変更

paginateメソッド経由でインスタンスを得るようにしてやる.

何ページ目にいるかはpageパラメータに入っている.

  def index
    @users = User.all.paginate(page: params[:page])
  end

ページ単位の表示数も指定してやれる.

  def index
    @users = User.all.paginate(page: params[:page], per_page: 20)
  end

ビューを変更

ページネーションを出したい所へ以下を入れてやるだけ.

<%= will_paginate @users %>

とてもシンプルで良い.

Rails5にbootstrapを導入する

基本は公式に従うだけ.

Gemを導入する

Gemfileに以下を追記.

gem 'jquery-rails'
gem 'bootstrap-sass', '~> 3.3.7'

いつも通り更新.

$ bundle install

application.cssをリネーム

$ mv app/assets/stylesheets/application.css app/assets/stylesheets/application.scss

application.scssを編集

以下を削除.

 *= require_tree .
 *= require_self

以下を追記.

// 必ずこの順番でないとダメ
@import "bootstrap-sprockets";
@import "bootstrap";

application.jsを編集

以下を記述.

//= require jquery
//= require bootstrap-sprockets
//= require_tree .

試す

雑にscaffoldで準備して試す

$ rails g scaffold user name:string email:string
$ rails db:migrate
$ rails s

出てきたけどなんか変.

containerを書いておく

bootstrapってcontainer書いておかないとダメだった.

<!-- containerのdivで囲む -->
<div class="container">
    <%= yield %>
</div>

これで綺麗に出た.

Visual Studio Installer Projectで上書きインストールができない

インストーラを作りたい

昔々, Visual Studio にはInstaller Projectというインストーラを作成するためのプロジェクトを作成する機能がありました.

機能が少なくても標準でついてきていたので非常に便利だったようなのですが2005くらいから消えてしまったそうです.

(伝聞なのは私自身使ったことがないためです)

しかし未だにVS2017が出てもなお, 私はWindowsフォームアプリケーションをせっせと作るお仕事をしているので, インストーラが標準で作れないのは非常に不便です.

と思ったらVS2015からアドオンとして復活しています.

Visual Studio: Marketplace - Microsoft Visual Studio 2015 Installer Projects

具体的な使い方は他サイトにたくさんあるので参照してみてください.

おすすめです.

Microsoft Visual Studio 2015 Installer Projects をインストールする (Visual Studioの使い方 Tips)

Microsoft Visual Studio 2015 Installer Projects を利用してインストーラーを作成する

バージョンアップインストールが出来ない

上記で作れたのは良いのですが, インストール済みのプログラムを更新する方法がわからなくて困っていました.

解決したのでメモしておきます.

因みにこの問題, 海外でも結構困っている人が多いようです.

Visual Studio forum - Setup project does not uninstall previous version

解決法

スクリーンショットを取るのが面倒なのでテキストでだけ.

重要なのは以下の3つです.

  • インストーラプロジェクトのプロパティウィンドウで「RemovePreviousVersions」をtrueに設定する
  • リリースの度にインストーラプロジェクトのプロパティウィンドウで「Version」を向上させてやる
  • 入れ替えるDLLやexeのプロジェクトのプロパティウィンドウで「ファイルバージョン」を向上させてやる

最後のがハマりました.

アセンブリバージョンを一生懸命変えていたのですが, どうもインストーラプロジェクトが見るのは「ファイルバージョン」の様です.

アセンブリバージョンはビルドの度に簡単に番号を向上させる方法もあったりするのですが, ファイルバージョンはなさそうなので中々面倒です.

とはいえ, 目的は達せられたのでよしとします.

CentOS7上にApache + PassengerでSinatraの動く環境を作る

Rubyを入れる(rbenvの導入)

rbenvで入れます.

rootへrbenvを入れますが, まずは環境を整えます.

$ sudo yum install git gcc gcc-c++ openssl-devel readline-devel

rbenvのリポジトリをclone. システム全体へ導入するので/usr/localへ入れます.

$ sudo git clone https://github.com/sstephenson/rbenv.git /usr/local/rbenv

rootへ昇格します.

$ su -

rbenv.shを作成します.

# vi /etc/profile.d/rbenv.sh
# 以下を記述
  export RBENV_ROOT="/usr/local/rbenv"
  export PATH="${RBENV_ROOT}/bin:${PATH}"
  eval "$(rbenv init --no-rehash -)"

設定ファイルを読み直します.

# source /etc/profile.d/rbenv.sh

ruby-buildを導入していきます.

# mkdir -p /usr/local/rbenv/plugins
# cd /usr/local/rbenv/plugins
# git clone https://github.com/sstephenson/ruby-build.git ./ruby-build
# cd ./ruby-build/
# ./install.sh

ようやくrubyの導入.

まずはrbenvで入れられるrubyのリストを表示します.

# rbenv install -l

今回は2.5.0にします.

# rbenv install 2.5.0
# rbenv global 2.5.0

最後にバージョンを確認.

ruby -v

SELinuxを無効にする

ごめんなさいごめんなさいごめんなさい.

# vi /etc/selinux/config
# 以下を変更
#SELINUX=enforcing
SELINUX=disabled

Passengerを入れる

gemでとりあえず投入.

# gem install passenger

続いてapacheモジュールの方も入れる.

# passenger-install-apache2-module

TUIが表示されるので, 1 => Ruby でEnter

一度abortします.

不足しているライブラリが表示されるので(親切!)インストールします.

# yum -y install libcurl-devel httpd httpd-devel apr-devel apr-util-devel

再度挑戦.

# passenger-install-apache2-module

入りました.

apacheの設定のスニペットが表示されます.

見逃しても以下で再表示できます.

# passenger-install-apache2-module --snippet
LoadModule passenger_module /usr/local/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/passenger-5.2.1/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
  PassengerRoot /usr/local/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/passenger-5.2.1
  PassengerDefaultRuby /usr/local/rbenv/versions/2.5.0/bin/ruby
</IfModule>

/etc/htpd/conf.dの下にpassenger.confを作ります. 上記のスニペットをコピペします.

# passenger-install-apache2-module --snippetの内容
LoadModule passenger_module /usr/local/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/passenger-5.2.1/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
  PassengerRoot /usr/local/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/passenger-5.2.1
  PassengerDefaultRuby /usr/local/rbenv/versions/2.5.0/bin/ruby
</IfModule>

# これはついで
# Passengerが追加するHTTPヘッダを削除する
Header always unset "X-Powered-By"
Header always unset "X-Rack-Cache"
Header always unset "X-Content-Digest"
Header always unset "X-Runtime"

# virtualhostの設定
<VirtualHost *:80>
  # ドメイン
  ServerName ドメイン名
  # アプリのルート
  DocumentRoot "/var/www/apps/sample_app/public"
  RackEnv production
</VirtualHost>

設定ファイルをチェック.

# apachectl configtest
AH00112: Warning: DocumentRoot [/var/www/apps/sample_app/public] does not exist
Syntax OK

apacheを再起動

# apachectl restart

アプリの設定

とりあえずbundlerを入れます.

# gem install bundler

passenger.confで指定したディレクトリを作ります.

tmpはpassengerのリロードに使います.

# mkdir -p /var/www/apps/sample_app/public
# mkdir -p /var/www/apps/sample_app/tmp

アプリのディレクトリへ移動して以下の様にアプリを作っておきます.

# cd /var/www/apps/sample_app

アプリ本体.

# app.rb
require 'sinatra'

get '/' do
  'Hello World!'
end

rackupファイル.

# config.ru
require File.expand_path(File.dirname(__FILE__)) + '/app'
run Sinatra::Application

Gemfile

source "https://rubygems.org"

gem 'sinatra'

最終的にディレクトリ構成はこんな感じ.

# pwd
/var/www
# tree .
.
├ apps
│   └ sample_app
│       ├ app.rb
│       ├ config.ru
│       ├ Gemfile
│       ├ Gemfile.lock
│       ├ public
│       └ tmp
├ cgi-bin
└ html

bundleを実行

# bundle install

ブラウザでアクセスすれば挨拶されます.

いやー, これ長い.

Sinatraを使う

久しぶりにRubyを触っているこの頃ですが, Railsはやはり覚える事が多いのでまだまだSinatraの方が私には楽です.

遠い記憶になっているので少しまとめました.

基本的にClassicスタイルで書いていきます.

書いてて思ったのは大体は公式に記載があるということです.

基本

ルーティング

普通のルーティング

HTTPのメソッド名 + ルート + BlockというSinatraが有名になった美しい構文.

Rubyらしく最後に評価された式の値でレスポンスになります.

get '/' do
  'ここは/にgetリクエストを送ったときに呼び出される場所'
end

post '/' do
  ...
end

put '/' do
  ...
end

delete '/' do
  ...
end

URLパラメータでのルーティング

URL内の

:変数名

がパラメータになります.

require 'sinatra'

get '/:hoge' do      #=> localhost:4567/fooへアクセス  -> fooと表示
  params[:hoge]
end

公式を読むと他にも条件付きマッチなどかなり色々できます.

リダイレクト

URL指定
get '/' do
  redirect 'https://www.google.com'
end
パス指定
get '/' do
  redirect to('/topath')
end
元いた場所へ戻る
get '/back' do
  redirect back
end

変わり種

正規表現でのルーティングやhalt/passというメソッドが面白いです.

halt

haltは途中で処理を止めてレスポンスを返します.

以下の例ではクエリパラメータhiにhiが渡された時だけhiと表示します.

require 'sinatra'

get '/' do
  halt "hi" if params['hi'] == "hi"
  "Hello"
end

他にもステータスコードを渡したりもできるようです.

正規表現でのルーティングとpass

ルーティングのURLには正規表現が使えます.

また, passメソッドを使うと他のマッチするルートへ移ります.

以下の例ではgreeting以下のhiにアクセスする時以外は全て2番目のルートで処理しますが, greeting/hiへのアクセス時にクエリパラメータでhi=hiが設定されているときはやはり2番目のルートで処理します.

(例が悪かったかな)

require 'sinatra'

get '/greeting/hi' do
  pass if params['hi'] == 'hi'
  "Hi"
end

get '/greeting/*' do
  "Hello"
end

パラメータ(params)

GetパラメータもPostパラメータもURLパラメータも以下だけでOKです.

params[:パラメータ名のシンボル]

ルーティングのブロックから取ったりもできるようですが.

外部テンプレート(Views)

viewsディレクトリを作っておくといい感じにしてくれます.
(erbの例)

.
├── server.rb
└── views
    └── index.erb

Rubyコードからはerbメソッドで拡張子抜きのファイル名のシンボルを呼び出すとviews以下のerbが呼ばれます.

require 'sinatra'

get '/' do
  erb :index
end

erbはこんな感じで普通に書きます.

<h1>Hello</h1>

<%= DateTime.now.strftime('%D %X') %>

共通レイアウト

共通レイアウトをまとめるlayoutも使えます.

viewsディレクトリの中にlayout.erbで配置するだけです.

.
├── server.rb
└── views
    ├── index.erb
    └── layout.erb

layout.erbの中ではyieldで個々のテンプレートへ移譲します.

<!DOCTYPE html>
<html>
    <head>
       <title>layout Sample</title>
   </head>
    <body>
        <%= yield %>
    </body>
</html>

Rubyのコードでテンプレートを呼び出すときにlayoutの有無を選択できるようです.

get '/' do
  erb :index, layout: false
end

JSONを返す

content_typeを明示的に指定する必要がある.

require 'sinatra'
require 'json'

get '/' do
  content_type :json
  data = { message: 'msg' }
  data.to_json
end

ヘルパーメソッド

ビューで使いたいメソッドはhelpersメソッドのブロックに書いておくとテンプレート内から呼び出せるようになります.

require 'sinatra'

get '/' do
  erb :index
end

helpers do
  def help
    "Help!"
  end
end

HTMLはこんな感じ.

<%= help %>

これで"Help!"と表示されます.

静的ファイル配信

デフォルトでpublicディレクトリが公開されます.

こんな配置にしておけば

.
├── public
│   └── css
│       └── style.css
├── server.rb
└── views
    └── index.erb

HTMLからはこれで呼び出せます.

<html>
    <head>
       <link rel="stylesheet" href="css/style.css">
   </head>
    <body>
    Link style
    </body>
</html>

ファイルアップロード

params[:fileのinput名]にファイルに関する情報が入っているのでこれを使います.

params[:fileのinput名][:filename] # ファイル名
params[:fileのinput名][:tempfile] # ファイルそのもの

Rubyのコードはこんな感じ.

require 'sinatra'

get '/' do
  erb :index
end

post '/upload' do
  if params[:file1]
    # カレントのimagesディレクトリへ保存する
    path = "images/#{params[:file1][:filename]}"
    File.open(path, 'wb') { |f|
      f.write params[:file1][:tempfile].read
    }
  end
  redirect back
end

indexのアップロードフォームも適当に.
enctype指定は忘れがち.

<form action="/upload" method="POST" enctype="multipart/form-data">
    <input type="file" name="file1">
    <input type="submit">
</form>

ファイルダウンロード

send_fileにカレントからのファイルパスを書けば送信できます.

typeでmimeも指定できる.

get '/' do
  send_file 'images/img.jpg', type: :jpg
end

フィルタ

beforeとafterメソッドがあります. そのままですね.

before do
  ...
end

after do
  ...
end

条件付きマッチもできます.

before '/loggerarea/*' do
  logger.info "logging"
end

DB接続

Railsの様に永続化機能がフレームワークで用意されているわけではないので, 自由に選べます.

ActiveRecordの例が多いですが, 個人的にはそのレベルならRails使う方が良い気がするのでSequelが簡単かなと思います.

セキュリティ系

この話題は避けられない割にキリがないのですが, とりあえず最低限だけ.

XSS対策

helperにRackのescape_htmlの呼び出しを書いておくとviewでも使えるようになります.

helpers do
  def h(text)
    Rack::Utils.escape_html(text)
  end
end

erbなら以下.

<%= h "<script>alert()</script>" %>

Sessionを使う

戸惑うことはないでしょう.

# 有効化
enable :sessions

# 記録
session[:message] = "This line use session"

# 読出
session[:message]

CSRF対策

Rack::ProtectionへSinatra本体が依存しているので, 既に利用がマージされてるようです.

コードの先頭で呼び出せばさらに色々使えます.

# server.rb
require 'sinatra'
require 'rack/protection'   #ここ

use Rack::Protection        #ここ
enable :sessions

全機能使おうとするとsessionが有効でないと使えないので注意.

Basic認証

StackOverFlowまんまですが.

  def authorized?
    @auth ||=  Rack::Auth::Basic::Request.new(request.env)
    @auth.provided? && @auth.basic? && @auth.credentials && @auth.credentials == ["ユーザ名","パスワード"]
  end

  def protected!
    unless authorized?
      response['WWW-Authenticate'] = %(Basic realm="Restricted Area")
      throw(:halt, [401, "認証が必要です\n"])
    end
  end

これで以下のように使えます.

get '/' do
  "認証なし"
end

get '/auth' do
  protected!
  "認証あり"
end

古いですが, Sinatra RecipesにはDigest認証の方法についても記述がありました.

エラーページ

not foundはその名の通りnot_foundメソッドでキャッチできます.

他のエラーはerrorメソッドでキャッチできます.

errorメソッドは開発環境ではデバッグの表示が出るので, プロダクションに設定してやると分かりやすいです.

require 'sinatra'

configure do
  set :environment, :production
end

get '/' do
  "Hello"
  raise "not found"
end

not_found do
  "not found"
end

error do
  "sorry"
end

ちなみにREADMEを読むとerrorは引数もとれるらしいです.

ロギング

普通にRubyのloggerです.

get '/' do
  logger.info 'access to root'
end

Sinatra(というかRack?)がデフォルトで用意している出力先はenv['rack.logger']の様.

プロダクションではアプリケーションサーバが担当するのか?

テスト

Classicスタイルの場合. Modulerスタイルの場合はappのアプリケーションを変えてやればよいそうで.

require './app.rb'
require 'test/unit'
require 'rack/test'

class MyAppTest < Test::Unit::TestCase
  include Rack::Test::Methods

  def app
    Sinatra::Application
  end

  def test_root
    get '/'
    assert last_response.ok?
    assert_equal last_response.body, "Hello"
  end
end

デプロイ

長くなるので別記事にしよう.

便利ライブラリがあります.

GitHub - sinatra/sinatra-contrib

参考

公式

StackOverFlow - Display Sinatra Basic HTTP Auth On One Page Only

Rubyのlogger

UbuntuにrbenvでRubyを入れる

手っ取り早くaptでも入れられるんだけど, かなり古くてMRIだとバージョンが2.0.0のrc版しかない. (14.04.5 LTS, Trustyで試した自分が悪いとは思うけど)

これはいかがなものかということで, GitHubから入れる事にしたが, ハマっちゃったのでメモ.

rbenvを入れる

$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv

ruby-buildを入れる

$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build

pathを通す

.profileに書くのかと思ったら違った.

$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile

rbenv用にディレクトリを作る

$ sudo mkdir -p /usr/local/rbenv/shims
$ sudo mkdir -p /usr/local/rbenv/versions

.profileを読み直す

$ source .profile

確認する

$ rbenv install -l

インストール可能なRubyの一覧が出る.

インストールしてみる

$ rbenv install 2.5.0

エラーが出た

/tmp/ruby-build.[日付].log

を見ると以下の感じ.

 94% [821/871]  object.c
 94% [822/871]  pack.c
 94% [823/871]  parse.c
Killed
make: *** [rdoc] Error 137

rdocを無視してみる

$ export RUBY_CONFIGURE_OPTS=--disable-install-doc

ビルドできたので使ってみる

$ rbenv local 2.5.0
$ ruby -v
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]