2014年7月9日水曜日

selfの役割

 railsのselfというのがようわからん。
 結局はselfがあるとクラスメソッドとして、無ければインスタンスメソッドとして定義できるみたいな話なんだけど、この手の話って多いすね。もっと明確にdefにインスタンスです!とか書けた方がいいんじゃないのかね。素人考えですが。

参考:
modelクラスの中でのselfの使い方

2014年7月5日土曜日

6-4 イベントの登録機能を作る

 わからないながら、ずっとメモし続けていたら話が少し見えるようになってきた気がする。メモを簡略化して、メモにかける時間を少し短くしようと思います。

 ここでやりたいこと。

  • ヘッダーにイベント作成リンクを設置。
  • ログイン状態で押すとイベント登録フォームへ移る。
  • 未ログインならトップページへリダイレクト。(これなんか親切じゃないね。)
  • イベント登録で誤った入力があるとエラー表示。
  • 正しければ登録処理。

タイムゾーンを設定する

デフォルトではUTC(協定世界時)に設定されているため、config/application.rbでコメントアウトされているタイムゾーンの設定をTokyoに変更します。こうしないと、イベントの登録時間等が登録者の意図しない時間になってしまう。
    config.time_zone = 'Tokyo'

イベント用のモデルを作成する

rails g resource event owner_id:integer name:string place:string start_time:datetime end_time:datetime content:text
  む。rails g resourceは初めて見ました。モデルとコントローラとルーティングを同時に作成するのか。ビューのhtmlファイルとかは作らないみたい。

参考:
[Ruby][Rails] Railsのgenerate scaffoldとgenerate resource

 マイグレーションファイルを修正してNOT NULL制約とインデックスを追加。このインデックスってのはなんなの?って今頃聞いちゃいけないこと?
 つーかマイグレーションつーのが未だにボンヤリしてるな。もっかい説明読めばイメージつかめるかな。

 今回は文字数制限等をvalidationで決めるって。NOT NULL制約はここに書いちゃ行けないの?ここにもpresence: trueとか書いてるじゃん。両方あれば間違いないとかそんなことかい?ググるとマイグレーションファイルは触っていない人も多い様子。
 validationはapp/models以下、event.rbで設定。
class Event < ActiveRecord::Base
validates :name, length: { maximum: 50 }, presence: true
validates :place, length: { maximum: 100 }, presence: true
validates :content, length: { maximum: 2000 }, presence: true
validates :start_time, presence: true
validates :end_time, presence: true
validate :start_time_should_be_before_end_time
private
def start_time_should_be_before_end_time
return unless start_time && end_time
if start_time >= end_time
errors.add(:start_time, 'は終了時間よりも前に設定してください')
end
end
end
 あれ?validateって検査の定義とかするんだっけ?いいのかな。開始時間が終了時間の前にちゃんと設定されているかどうかチェックするんだな。

参考:

 合わせてビューにもイベント作成のリンクを追加。リンク先のnew_event_pathはrails g resourceと同時にルーティングが追加されているので何もしなくて良い。確かにあった。

ログイン状態を管理する処理を作る

 複数のコントローラで利用する汎用的な処理は、一般的にApplicationControllerに書くそうです。そうですって、まぁ、そうでしょうね。感覚としてはわかります。でも本見ずに進めていたら、思考停止しちゃうかもしれない。
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  helper_method :current_user, :logged_in?
  private
  def current_user
  return unless session[:user_id]
  @current_user ||= User.find(session[:user_id])
  end
  def logged_in?
  !!session[:user_id]
  end
  def authenticate
  return if logged_in?
  redirect_to root_path, alert: 'ログインしてください'
  end
end
 application_controller.rbをこんな風に書き換えろとまず言われたんだけど、||=ってなに?ググったら、左辺の変数が偽か未定義の場合に右辺の値を代入する、だって。ふーん。初めて変数使うときはいつでもこうするべきなのかね。説明してよ。=だけだとなんか不都合あるんだろうか。

参考:

 authenticateのアラートを表示させるためにレイアウトも修正。コードは前後省略。
 <div class="container">
  <% if flash[:notice] %>
  <div class="alert alert-success">
  <%= flash[:notice] %>
  </div>
  <% end %>
      <% if flash[:alert] %>
        <div class="alert alert-danger">
        <%= flash[:alert] %>
        </div>
      <% end %>
  <%= yield %>
 </div>
 まただ。このflashって変数、なんだよ…。

参考:

 説明読んでもイマイチ。flash無しではダメ?前のデータが残っちゃうからか。そういうことかな。
 続いて、authenticateメソッドをEventsControllerに対して設定する。そろそろわかんなくなってきたぞ。
 authenticateをbefore_actionに設定して、未ログインユーザーがEventsControllerのアクションにアクセスしたとき、トップページにリダイレクトする。と打ち込んで3回くらい読む。
 ログインユーザーだけしかアクセスできないアクションにアクセスがあった場合には、そのアクションを起こす前にユーザーがログインしているかチェックするわけだね。未ログインならトップページに飛ばす。やっとかよ。これの理解に何分もかかっちゃうのかよ。
 今日はただでさえ頭痛が酷いのに、そこへこういったテキストを読むなんて馬鹿げているけど、気が焦ってしまって仕方ない。最近気になり始めた武藤彩未さんのアルバムでも小音量で聴いて癒されようかな。つーかBABYMETALのファンになったら次から次へと関連アイドルの知識が耳に入ってくるので困る。

 events_controller.rbについては以下の通り。
class EventsController < ApplicationController
  before_action :authenticate
  def new
  @event = current_user.created_events.build
  end
  def create
  @event = current_user.created_events.build(event_params)
  if @event.save
  redirect_to @event, notice: '作成しました'
  else
  render :new
  end
  end
  private
  def event_params
  params.require(:event).permit(
  :name, :place, :content, :start_time, :end_time
  )
  end
end
 newとcreateアクションではapplication_controllerに追加したcurrent_userメソッドを使ってログインユーザーを取得している。んー。んー。わかんね。
 あとは、ここで出てきたcreated_eventsをmodel/user.rbで定義します。
class User < ActiveRecord::Base
has_many :created_events, class_name: 'Event', foreign_key: :owner_id
以下略
 またこの項は何度も読み直す必要がありそうだな。日本語が読めていないのか、説明が悪いのか。いや、一応ベストセラーらしいし、原因は自分だよな。多分。

イベント登録用のフォームを作る

 次はイベント登録用フォームを作る。events/new.html.erbを新規作成。はい作った。
<% now = Time.zone.now %>
<div class="page-header">
<h1>イベント作成</h1>
</div>
<%= form_for(@event, class: 'form-horizontal', role: 'form') do |f| %>
<% if @event.errors.any? %>
 <div class="alert alert-danger">
  <ul>
  <% @event.errors.full_messages.each do |msg| %>
   <li><%= msg %></li>
  <% end %>
  </ul>
 </div>
<% end %>
<div class="form-group">
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :place %>
<%= f.text_field :name, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :start_time %>
<div>
<%= f.datetime_select :start_time, start_year: now.year, end_year: now.year + 1 %>
</div>
</div>
<div class="form-group">
<%= f.label :end_time %>
<div>
<%= f.datetime_select :end_time, start_year: now.year, end_year: now.year + 1 %>
</div>
</div>
<div class="form-group">
<%= f.label :content %>
<%= f.text_area :content, class: 'form-control', row: 10 %>
</div>
  <%= f.submit '作成', class: 'btn btn-default', data: { disable_with: '作成中…' } %>
<% end %>
 で、随分いろいろ打ち込みましたが、名前/場所/開始時間/終了時間/内容の入力フォームとイベント作成ボタンを作っています。
 ここで実際にブラウザで動作を確認したら打ち間違いによるエラーが死ぬほど出て死んだ。数時間かけてやっと原因がわかってげんなり。
 入力フォームが出るようにはなったけど、登録するとPlace can't be blankとか言われるな。空欄ではないんだけど…。
 再度ハマって、更に打ち間違いとコピペミスがあって、マイグレーションし直したりしてなんとかなった。酷いなオレ…。

i18nの設定をする

 なんすか、i18nって。ラベルとエラーの修正、ですか。ラベルやvalidationのエラーを日本語にしようよ、ということみたい。
 設定はconfig/spplication.rb。以下の部分をコメントアウトして、最後の右辺を:deから:jaに書き換える。
config.i18n.default_locale = :ja
汎用のエラー辞書データはrails-i18nのリポジトリにまとめられている。下記からダウンロード。
~/work/awesome_events$curl -o config/locales/ja.yml -L https://raw.github.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml
以下はファイルの内容の一部。へーほー。
  errors:
    format: ! '%{attribute}%{message}'
    messages:
      accepted: を受諾してください。
      blank: を入力してください。
      present: は入力しないでください。
      confirmation: と%{attribute}の入力が一致しません。
      empty: を入力してください。
      equal_to: は%{count}にしてください。
      even: は偶数にしてください。
      exclusion: は予約されています。
  更に、同じくconfig/localesに、ar-ja.ymlとして今回のアプリ向けの辞書データを作ります。よく書式がわからないけど、スルーしとこうかな。
ja:
  acriverecord:
    models:
      event: イベント
    attributes:
      event:
        name: 名前
        place: 場所
        start_time: 開始時間
        end_time: 終了時間
        content: 内容
  あ、確かにエラーが日本語で表示される。

6-3 OAuthを利用して "Twitterでログイン" 機能を作る

OAuthとは

OAuthは、Twitter等の大手サービスを利用する認証方法。利用するサービスのパスワードを又貸しするようなことをせずに認証を実現できるので、手軽で現在は広く使われているんですってよ、奥さん。

Twitterアプリケーションの登録

p168に沿ってその通り登録。Application detailsのNameは重複が許されないとのことで、適当にふざけた名前を付けた。

Twitterアカウントでログインする機能の作成

とりあえず以下がやりたい。
ヘッダーにログインリンクを作る
クリックするとTwitter認証画面
Twitter認証画面で適切なユーザー名とパスワードの入力でログイン
ログイン中はログインリンクの代わりにログアウトのリンク
ログアウトをクリックするとログアウトする
認証に実際に使用するgemはOmniAuth。Twitterを認証に利用する場合は更にOmniAuth Twitterというgemが必要。
 今回はテキストに合わせるため、バージョンをpatchレベル以外は固定。ポリシーはアプリケーションにより決める。

 次にTwitter管理画面上のAPI keyとAPI secretをOmniAuthに設定する。Rails4.1からはconfig/secrets.ymlが設定ファイル。
default_twitter: &default_twitter
  twitter_api_key: 'tjEBXXXXXXXXXXXXXdjhBo'
  twitter_api_secret: 'mZKR0XXXXXXXXXXXXXXXXXXXXXX45e4fyO73nt'
development:
  secret_key_base: tjEBZXXXXXXXXXXXX4djhBo
  <<: *default_twitter
test:
  secret_key_base: mZKR0vXXXXXXXXXXXXXXXXXXu9t45e4fyO73nt
  <<: *default_twitter
production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
  twitter_api_key: <%= ENV["TWITTER_CONSUMER_KEY"] %>
  twitter_api_secret: <%= ENV["TWITTER_CONSUMER_SECRET"] %>
 次、もう一つ。config/initializers/omniauth.rbを作成して下記を入れておく。
Rails.application.config.middleware.use OmniAuth::Builder do |
provider :twitter,
 Rails.application.secrets.twitter_api_key,
 Rails.application.secrets.twitter_api_secret|
end
  これで設定完成。説明はp173を参照。まだこのキーがいつどのように使われるかよくわかっていない。

 続いてログインユーザーのモデルを作成する。
rails g model user provider:string uid:string nickname:string image_url:string
マイグレーションファイルにNOT NULL制約とユニークインデックス追加しておく。これはなんだ。validationじゃなくて、ここに書くんだ。うーん。ちょっとわからんぞ。なんにしても、未記入は許しまへんで、ということですよね?
      t.string :provider, null: false
      t.string :uid, null: false
      t.string :nickname, null: false
      t.string :image_url, null: false
参考:

 で、マイグレーションする、と。

 続いては、Twiterにログインというリンクを作る。コードはp175を参照して、application.html.erbに書いた。認証先のリンクはOmniAuthの仕様で、/auth/:providerになる。
 この後サイトへアクセスしてログインボタンを押すと、見慣れたTwitterの「連携アプリの認証」ボタンの画面に移った。おー、なんか今っぽいすね。
 認証ボタンを押すと、まだ後処理が用意されていないためエラーになりました。ルーティングエラー。

 さて、ログイン兼ユーザー登録用のコントローラを作成します。
rails g controller sessions
 この辺のコントローラの名前とか、自分にセンスがなさそうなので心配。単数系にするときとかもどうもぼんやり。今回はユーザーを扱うので当然複数形ですよね。こういう単純な考え方がそもそもいけないのか。

 createアクションの作成。
class SessionsController < ApplicationController
def create
user = User.find_or_create_from_auth_hash(request.env['omniauth.auth'])
session[:user_id] = user.user_id
redirect_to root_path, notice: 'ログインしました'
end
end
OmniAuthによりrequest.env['omniauth.auth']にHashに似たOmniAuth::AuthHashが格納される。Twitterから渡されたユーザー情報やOAuthのアクセストークンも含まれる。
 find_or_create_from_auth_hashはまだ未定義。方針としては、引数に関連するユーザーがいればそれを返し、いなければ新規作成して返すという動きをさせる。
 最終的にはUser.find_or_create_from_auth_hashが返すUserのIDをセッションに格納することでログインしたという扱いにする。こういうの難しいわ〜。論理的に説明だけされるとボーッとなりますね。ちょいちょい具体的な話や面白話がないと。本が何倍にもなりそうですが。
 当然次はUser.find_or_create_from_auth_hashの実装。/models/user.rbで。
class User < ActiveRecord::Base
def self.find_or_create_from_auth_hash(auth_hash)
provider = auth_hash[:provider]
uid = auth_hash[:uid]
nickname = auth_hash[:info][:nickname]
image_url = auth_hash[:info][:image]
User.find_or_create_by(provider: provider, uid: uid) do |user|
user.nickname = nickname
user.image_url = image_url
end
end
end
 find_or_create_byはActiveRecordのメソッド。
 内容としては、引数で渡したproviderとuidを持つレコードが存在していればそのオブジェクトを返し、存在しなければprovider、uid、nickname、image_urlを設定してレコードを作成し、そのオブジェクトを返す、と。

 ルーティングの追加。
get '/auth/:provider/callback' => 'session#create'
  これはどういうことだ。/auth/:provider/callbackで帰ってくるってのは自明?あ、さっき出たルーティングエラーにアクセス先が/auth/twitter/callbackとは書かれているな。OAuthとやらの仕様か。それともTwitterの仕様?
 application.html.erbについてもp179の通り追記する。これでcreateアクションからの返り値により、ログインしましたというメッセージが表示されるようになる。できた。

 最後はログアウト処理。まずはdestroyアクションの追加。
def destroy
reset_session
redirect_to root_path, notice: 'ログアウトしました'
end
 次は、ログイン中は画面右上にログアウトのリンクを表示させたい。logged_in?メソッドを定義して、ログイン状態が把握できるようにする。
 application.html.erbではp180の通りlogged_in?の条件分岐でログイン/ログアウトを表示し分ける。ログアウトのリンク先のlogout_pathはあとでroutes.rbで設定する。
 logged_in?の定義はapplication_controller.rbで下記の通り。
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  helper_method :logged?
  private
  def logged_in?
  !!session[:user_id]
  end
end
 hyper_methodってなんだよ。コントローラのメソッドをビューでも使う場合に使うってよくわからない。

参考:

 あと、!!ってなに?基本過ぎ?

参考:

 はぁ、!が論理演算のnotだって。すると、sessionがユーザーidを持っていれば!によってfalse、もう一つの!でtrue、で結果としてこれがログイン状態になるんだ。頭いいねぇ。感心した。
 未ログインでsessionがnullなら!でtrue、もう一回!でfalse。だから未ログイン。

 最後にルーティングを追加。
 get '/logout' => 'sessions#destroy', as: :logout
 プログラミングでごく一般的なことがわかっていないから、どの説明も本筋から外れたところで新たな発見がたくさんある。ありすぎて調べ続けているとなにやっていたかわからなくなったりする。

 この深夜に仕事のLineがきて、確かに自分が悪いところもあるけどものすごく不快な気持ちになってしまった。寝よう寝よう。

2014年7月4日金曜日

railsコマンドが動作しない

 ググるとspring周辺が悪さをしているみたいなことが事例としてたくさんあって、あれこれやっても直らず、先に結論を申し上げますとMacのFinderのマイファイルからファイルをいくらか消していましてね、それが原因でした。Macを知らない私が原因でした。
 FinderでUndoできたのでことなきを経ましたが、これはなかなか大変なことです。今始めていたプロジェクトはgithubで管理もしていませんでしたので。結構サイテーなことだとは思います。

 まぁ、でもよかった。直って。

 マイファイルって、Windowsで言うライブラリフォルダみたいなもんでしょうかね。ややこしいですね。

 68020くらいの頃はMacとはお友達だったはずなんだけど、仕事ではろくなことが無い。

Gemfileでのバージョン管理

 gemのバージョンはGemfileへ厳密に書くことで固定、管理が可能。

 書式は下記で確認。

 バージョンのX.X.Xは、メジャー.マイナー.パッチと呼ばれる。

参考:
Gemfile