Office 365 REST APIを利用したRuby on Railsアプリの作成

Office 365は、Outlook(Exchagne)、OneDriveおよびVideoにREST APIが用意されています。
この記事では、Office 365 REST APIを利用してOutlookのリソースにアクセスする手順について確認した内容をまとめてみました。

API関連の情報ソース

APIを利用を始めるにあたって、以下のようなドキュメントを参考にしました。

REST API全体のリファレンスについては、Office 365 API reference、利用可能なOutlookのリソースについては、Resource reference for the Mail, Calendar, Contacts, and Task REST APIsで確認できます。

日本語版は、Office 365 API リファレンスがあります。

その他の参考情報としては

以下の公式ブログなどが参考になります。

“Office 365 REST APIを利用したRuby on Railsアプリの作成” の続きを読む

RailsのActiveRecordのbefore_validationなどのコールバック関数で前の値を取得するには

例えば、Userモデルのemailを変更しようとしたときにbefore_validationなどのコールバック関数で前の値を取得したい場合、
User.emailはすでに新しい値なので、古い値を取るにはどうしたら良いのか?

答えは以下のように_wasをつける

User.email_was

_changed?は良く使いますが、こういった関数は、ActiveModel::Dirtyモジュールで実装されているようです。
参照: ActiveModel::Dirty

RSpec ActionMailterで送信したメールのテストの書き方

送信されたメールを取得
[ruby]
sent_mail = ActionMailer::Base.deliveries.last
[/ruby]

Subjectのテスト
[ruby]
expect(sent_mail.subject).to =~ /Hello/
[/ruby]

マルチパートのHTMLをテスト
[ruby]
html_part = sent_mail.body.parts.find{|p| p.content_type.match /html/ }
expect(html_part.body.raw_source).to include ‘Hello’
[/ruby]

マルチパートのテキストをテスト
[ruby]
text_part = sent_mail.body.parts.find{|p| p.content_type.match /text/ }
expect(text_part.body.raw_source).to include ‘Hello’
[/ruby]

Rails3 で コントローラからメールが送信されたか Rspec で確認のメモ – 牌語備忘録 – pygo
ruby – Testing ActionMailer multipart emails with RSpec – Stack Overflow

devise、cancanと連携して管理者ユーザーにのみRailsAdminを公開するには

RailsAdmin は、Railsで最も良く使われている管理画面のgemで、Modelのデータを管理する画面を自動生成してくれます。
RailsAdminを利用すれば、MySQLAdmin的なツールを使わずに安全にデータのメンテナンスができるようになります。

RailsAdmin は認証に devisesocery、認可(アクセスコントロール)に cancan と連携することができます。

Railsプロジェクトの新規作成から RailsAdmin および devise、cancan をセットアップするまでの手順をまとめました。
Rubyは 2.0.0、Railsは、4.0.2 で試しています。

確認用プロジェクト作成

プロジェクトを作成して適当にモデルを一つ追加しておきます。

$ rails new rails_admin
$ cd rails_admin
$ bundle install --path vendor/bundle
$ rails g scaffold post title:string message:text
$ rake db:migrate

RailsAdminのインストール

インストール手順は、Githubの READMEのInstallation に記述されている通り進めます。

1. Bundle the gem
2. Run rails g rails_admin:install
3. Provide a namespace for the routes when asked
4. Start a server rails s and administer your data at /admin. (if you chose default namespace: /admin)

Gemfile に rails_admin を追加して bundle install した後、インストールを実行します。

# Gemfile
gem 'rails_admin'
$ bundle install
...
Installing font-awesome-rails (4.0.3.1)
...
Installing haml (4.0.5)
...
Installing jquery-ui-rails (4.2.0)
...
Installing kaminari (0.15.1)
Installing mini_portile (0.5.2)
Installing nested_form (0.3.2)
Installing nokogiri (1.6.1)
Installing rack-pjax (0.7.0)
...
Installing remotipart (1.2.1)
Installing safe_yaml (1.0.1)
...
Installing rails_admin (0.6.1)
...
$ rails g rails_admin:install
           ?  Where do you want to mount rails_admin? Press <enter> for [admin] > 
       route  mount RailsAdmin::Engine => '/admin', :as => 'rails_admin'
      create  config/initializers/rails_admin.rb

管理画面のパスはデフォルトの admin で良いので、そのまま

ここまで、確認してみます。

$ bundle exec rails s

http://localhost:3000/admin/ にアクセスすると以下のような画面を確認できます。

rails_admin

deviseのセットアップ

Gemfile に devise を追加して bundle install した後、deviseのインストールと認証用モデル user を作成します。

# Gemfile
gem 'devise'
$ bundle install
...
Installing bcrypt (3.1.7)
...
Installing orm_adapter (0.5.0)
Installing warden (1.2.3)
Installing devise (3.2.4)
...
$ rails g devise:install
$ rails g devise user
$ rake db:migrate

devise自体の設定は、今回はユーザの作成と認証ができれば良いので、デフォルトのまま次に進みます。

Authentication · sferik/rails_admin Wiki を参照すると、設定は config/initializer/rails_admin.rb の Devise の箇所のコメントを外すだけです。

# config/initializers/rails_admin.rb
RailsAdmin.config do |config|

  ### Popular gems integration

  ## == Devise ==
  config.authenticate_with do
    warden.authenticate! scope: :user
  end
  config.current_user_method(&:current_user)

  ## == Cancan ==
  # config.authorize_with :cancan

ここまでの動作を確認してみます。
未認証で /admin にアクセスすると以下のように認証画面にリダイレクトされるようになりました。

rails_admin_devise

cancanのセットアップ

上記まででは、認証されたユーザーがすべて /admin にアクセスできてしまうため、さらに管理者権限を持っているユーザーのみにアクセスを制限します。

アクセス制限は、Authorization · sferik/rails_admin Wiki および CanCan · sferik/rails_admin Wiki
を参考に cancan をインストールして設定します。

# Gemfile
gem 'cancan'
$ bundle install
...
Installing cancan (1.6.10)
...
$ rails g cancan:ability
      create  app/models/ability.rb

config/initializers/rails_admin.rb の Cancan の設定のコメントを外します。

# config/initializers/rails_admin.rb
RailsAdmin.config do |config|

  ### Popular gems integration

  ## == Devise ==
  config.authenticate_with do
    warden.authenticate! scope: :user
  end
  config.current_user_method(&:current_user)

  ## == Cancan ==
  config.authorize_with :cancan

この時点で /admin にアクセスするとすべてのユーザーで403になります。

rails_admin_cancan_all_deny

次に、Userモデルに admin フラグを追加して、admin? が true の時だけ /admin にアクセスできるように修正していきます。

admin フラグを Userモデルに追加

$ rails g migration AddAdminToUser user

~~ruby

db/migrate/20140322130042_add_admin_to_user.rb

class AddAdminToUser < ActiveRecord::Migration
def change
add_column :users, :admin, :boolean, :default => false
end
end


~~~bash $ rake db:migrate

Railsコンソールで、管理者ユーザーの admin を true にセットします。

$ rails c
> user = User.find(1)
> user.update_attribute(:admin, true)

参考: ruby on rails – how to make admin users using devise and cancan? – Stack Overflow

rails generateコマンドで生成された Abilityクラスのinitializeに権限ロジックを追加します。
以下、user#admin? が true の時に rails_admin へのアクセスとすべてのモデルの管理権限を許可します。

# app/models/ability.rb
class Ability
  include CanCan::Ability

  def initialize(user)
    if user && user.admin?
      can :access, :rails_admin   # grant access to rails_admin
      can :manage, :all           # allow superadmins to do anything
    end
  end
end

initializeに渡される user は、コントローラのcurrent_userメソッドが返すオブジェクトです。
つまり、deviseを利用している場合は、何もしなくても認証用のモデル(今回は User)と連携します。

以上で、RailsAdminのダッシュボードへは、管理者権限を持つユーザーのみが利用可能になります。

rubyzipでzipファイルにストリームデータを追加する

Rubyでzipファイルを扱う場合は、Gem rubyzip を利用します。zipruby など他のGemは更新されていないので、現在は rubyzip 一択のようです。

ファイルシステムを使わずに、S3などから取得したファイルをまとめたzipファイルの作成を rubyzip を使って実装をしたので、その手順についてメモしておきます。

Railsで、zipファイルをダウンロードするところまでのコードになります。
[ruby highlight=”1,6-9″]
require ‘zip’

class ZipController < ApplicationController
def download
t = Tempfile.new("my-temp-filename-#{Time.now}")
Zip::OutputStream.open(t.path) do |z|
z.put_next_entry("images/polyvore1.jpg")
z.print Net::HTTP.get URI.parse(‘http://ak1.polyvoreimg.com/cgi/img-set/cid/114613528/id/yAcS_3ae4xGVrWQvSbYPRA/size/l.jpg’)
end

send_file t.path, :type => ‘application/zip’,
:disposition => ‘attachment’,
:filename => "Images.zip"
t.close
end
end
[/ruby]

TUTORIAL: Zip file downloads with Ruby on Rails 4.0 and Rubyzip
Rubyzip: Export zip file directly to S3 without writing tmpfile to disk?

Rails 3.2 Mongoid devise メール確認によるサインナップ :confirmable、招待 :invitable

前提: Rubyおよびrailsとbundlerのgemはインストール済み

古いバージョンのRailsを指定して、アクティブレコードを外してRailsアプリケーションを作成
[bash]
$ rails _3.2.17_ new app –skip-active-record
$ cd app
[/bash]
–skip-active-recordは-OでもOK

Mongoidとdevise関連のgemをインストール
[ruby title=”Gemfile”]
gem "mongoid"
gem "devise"
gem "devise_invitable"
[/ruby]

[bash]
$ bundle install –path vendor/bundle

Installing devise (3.2.3)
Installing devise_invitable (1.3.4)

Installing mongoid (3.1.6)

[/bash]

Mongoidの設定ファイルを生成
http://mongoid.org/en/mongoid/docs/installation.html
[bash]
$ rails g mongoid:config

create config/mongoid.yml
[/bash]

認証テスト用のコントローラを作成
[bash]
$ rails generate controller home index
create app/controllers/home_controller.rb
route get "home/index"
invoke erb
create app/views/home
create app/views/home/index.html.erb
invoke test_unit
create test/functional/home_controller_test.rb
invoke helper
create app/helpers/home_helper.rb
invoke test_unit
create test/unit/helpers/home_helper_test.rb
invoke assets
invoke coffee
create app/assets/javascripts/home.js.coffee
invoke scss
create app/assets/stylesheets/home.css.scss
[/bash]

deviseのセットアップ
[bash]
$ rails generate devise:install
create config/initializers/devise.rb
create config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven’t yet:

1. Ensure you have defined default url options in your environments files. Here
is an example of default_url_options appropriate for a development environment
in config/environments/development.rb:

config.action_mailer.default_url_options = { :host => ‘localhost:3000’ }

In production, :host should be set to the actual host of your application.

2. Ensure you have defined root_url to *something* in your config/routes.rb.
For example:

root :to => "home#index"

3. Ensure you have flash messages in app/views/layouts/application.html.erb.
For example:

<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

4. If you are deploying on Heroku with Rails 3.2 only, you may want to set:

config.assets.initialize_on_precompile = false

On config/application.rb forcing your application to not access the DB
or load models when precompiling your assets.

5. You can copy Devise views (for customization) to your app by running:

rails g devise:views

===============================================================================
[/bash]
表示された手順に従ってセットアップしていきます。

1. メール送信設定
動作確認なので、とりあえずsendmailコマンドでメールを送信
[ruby title=”vconfig/environments/development.rb” highlight=”2,3″]
# config.action_mailer.raise_delivery_errors = false
config.action_mailer.delivery_method = :sendmail
config.action_mailer.default_url_options = { :host => ‘localhost:3000’ }
[/ruby]
postfixが設定して無い場合はこの辺りを参考にしてセットアップしておく
Configuring Postfix to Send Mail from Mac OS X Mountain Lion

2. rootの設定
手順の指示通りに、先ほど作成したhome_controller#indexを指定
[ruby title=”config/routes.rb” highlight=”3″]
# You can have the root of your site routed with "root"
# just remember to delete public/index.html.
root :to => ‘home#index

[/ruby]
welcomeページは削除しておく
[bash]
$ rm public/index.html
[/bash]

3. レイアウトにflashメッセージの表示を追加
[ruby title=”app/views/layouts/application.html.erb” hightlight=”10,11″]
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<%= stylesheet_link_tag "application", :media => "all" %>
<%= javascript_include_tag "application" %>
<%= csrf_meta_tags %>
</head>
<body>
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
<%= yield %>

</body>
</html>
[/ruby]

4. developmentで試すのでスキップ

5. viewのカスタマイズをしないのでスキップ

認証ユーザ用モデルを生成
[bash]
$ rails generate devise user
invoke mongoid
create app/models/user.rb
invoke test_unit
create test/unit/user_test.rb
create test/fixtures/users.yml
insert app/models/user.rb
insert app/models/user.rb
route devise_for :users
[/bash]

home_controllerに認証をかける
[ruby title=”app/controller/home_controller.rb” hightlight=”2″]
class HomeController < ApplicationController
before_filter :authenticate_user!
def index
end
end
[/ruby]

メール確認(:confirmable)を有効にする
[ruby title=”app/model/user.rb” language=”7,”]
class User
include Mongoid::Document
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:confirmable

## Confirmable
field :confirmation_token, :type => String
field :confirmed_at, :type => Time
field :confirmation_sent_at, :type => Time
field :unconfirmed_email, :type => String # Only if using reconfirmable

[/ruby]
:confirmableを追加して、上記の4つのフィールドのコメントを外します。

ここまでで、ユーザのサインナップはメール確認が必須になります。

最後にdevise-invitableで、他のユーザを招待できるようにします。
コマンドで必要なファイルを生成し、userモデルに設定を追加します。
[bash]
$ rails generate devise_invitable:install
insert config/initializers/devise.rb
create config/locales/devise_invitable.en.yml
$ rails generate devise_invitable user
insert app/models/user.rb
invoke mongoid
[/bash]
deviseメソッドの引数に:invitableが追加されています。
fieldは手動で追加する必要があります。
[ruby]
class User
include Mongoid::Document
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :invitable, :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:confirmable

## Invitable
field :invitation_token, type: String
field :invitation_created_at, type: Time
field :invitation_sent_at, type: Time
field :invitation_accepted_at, type: Time
field :invitation_limit, type: Integer

index( {invitation_token: 1}, {:background => true} )
index( {invitation_by_id: 1}, {:background => true} )
[/ruby]
インデックスを反映します。
[bash]
$ rake db:mongoid:create_indexes
[/bash]
Scope view を有効にします。
[ruby title=”config/initializers/devise.rb” highlight=”5″]
# ==> Scopes configuration
# Turn scoped views on. Before rendering "sessions/new", it will first check for
# "users/sessions/new". It’s turned off by default because it’s slower if you
# are using only default views.
config.scoped_views = true
[/ruby]

ユーザを招待するには、ログイン後、http://localhost:3000/users/invitation/new にアクセスします。

補足

sign_outをGETメソッドで実行したい
[ruby title=”config/initializers/devise.rb”]
# The default HTTP method used to sign out a resource. Default is :delete.
# config.sign_out_via = :delete
config.sign_out_via = :get
[/ruby]

Ruby on Railsの公式ドキュメントリンク集まとめ

Rails Guide: Railsの公式ガイド
http://guides.rubyonrails.org/
わからないことがあったらまずはこれを読む

API リファレンス
http://api.rubyonrails.org/
ここで、情報が見つからなければソース読んだほうが早い。

Railsドキュメント: Railsの日本語ガイド、日本の有志で運営されている。
http://railsdoc.com/
翻訳箇所にムラと若干情報が見つけにくいので、普段はRails Guideを読んでいます。

Rubyist Magazine: Ruby全般の情報が掲載されています。
http://magazine.rubyist.net/
情報は古いものも多いですが、RSpec関連の記事はとても参考になりました。

Unicornをデーモンとして登録する

Rails の unicorn をデーモンとして登録するには unicornのGithubに公開されている init.sh が利用できます。

https://github.com/defunkt/unicorn/blob/master/examples/init.sh

ほぼオリジナルと同じですが、デプロイをCapistranoで実行するプロジェクト用に修正したものを載せておきます。

修正した点は、PIDファイルがAPP_ROOT以下に置いてあるとCapistranoでデプロイした時にPIDを取得できなくなってしまうので、/var/runに移動しています。

#!/bin/sh
set -e
# Example init script, this can be used with nginx, too,
# since nginx and unicorn accept the same signals

# Feel free to change any of the following variables for your app:
TIMEOUT=${TIMEOUT-60}
APP_ROOT=/var/www/app/current
RAILS_ENV=production
PID=/var/run/unicorn.pid
CMD="/usr/local/bin/bundle exec unicorn_rails -c $APP_ROOT/config/unicorn.rb -E $RAILS_ENV -D"
INIT_CONF=$APP_ROOT/config/init.conf
action="$1"
set -u

test -f "$INIT_CONF" && . $INIT_CONF

old_pid="$PID.oldbin"

cd $APP_ROOT || exit 1

sig () {
    test -s "$PID" && kill -$1 `cat $PID`
}

oldsig () {
    test -s $old_pid && kill -$1 `cat $old_pid`
}

case $action in
start)
    sig 0 && echo >&2 "Already running" && exit 0
    $CMD
    ;;
stop)
    sig QUIT && exit 0
    echo >&2 "Not running"
    ;;
force-stop)
    sig TERM && exit 0
    echo >&2 "Not running"
    ;;
restart|reload)
    sig HUP && echo reloaded OK && exit 0
    echo >&2 "Couldn't reload, starting '$CMD' instead"
    $CMD
    ;;
upgrade)
    if sig USR2 && sleep 2 && sig 0 && oldsig QUIT
    then
        n=$TIMEOUT
        while test -s $old_pid && test $n -ge 0
        do
            printf '.' && sleep 1 && n=$(( $n - 1 ))
        done
        echo

        if test $n -lt 0 && test -s $old_pid
        then
            echo >&2 "$old_pid still exists after $TIMEOUT seconds"
            exit 1
        fi
        exit 0
    fi
    echo >&2 "Couldn't upgrade, starting '$CMD' instead"
    $CMD
    ;;
reopen-logs)
    sig USR1
    ;;
*)
    echo >&2 "Usage: $0 <start|stop|restart|upgrade|force-stop|reopen-logs>"
    exit 1
    ;;
esac

Capistranoのrestartにはupgradeを指定します。

logrotatedでRailsのproductionログをローテーションするには

[bash]
$ sudo vi /etc/logrotate.d/rails_production
/var/log/co-meeting/production.log
{
rotate 7
daily
missingok
notifempty
delaycompress
compress
}
[/bash]

動作確認
[bash]
logrotate -dv /etc/logrotate.d/rails_production
reading config file /etc/logrotate.d/rails_production
reading config info for /var/log/co-meeting/production.log

Handling 1 logs

rotating pattern: /var/log/co-meeting/production.log
after 1 days (7 rotations)
empty log files are not rotated, old logs are removed
considering log /var/log/co-meeting/production.log
log does not need rotating
[/bash]

参照 Redmine.JP production.logのローテート