読者です 読者をやめる 読者になる 読者になる

アトラシエの開発ブログ

株式会社アトラシエのブログです

default_scopeよりaround_actionとscopingを使おう

default_scope is evil は浸透してきたが...

qiita.com

最近では Railsでdefault_scope使うのはやめよう! という意見が強くなりましたね。 default_scope

default_scope -> { where(removed: false) }

こんなふうに論理削除を実装するときに便利です。 しかし管理画面では論理削除されたものも含めて集計したいなど、細かい欲求が出てくると意外とグローバルにスコープがセットされるのは都合が悪い。しかもdefault_scopeはmodelの初期値をセットしてしまう(これだとremoved = false)とか、解除するためにunscopedを使うとリレーションからのチェーンができない(@user.blogs.unscopedでuserのscopeごと消える)など残念な点も多いです。

しかしながら、エンドユーザー向けの画面では論理削除させたものを間違っても絶対に出したくない という強い意志があるとき、たしかにdefault_scopeを使いたくなる気持ちにも分はあるのです。「default_scopeを使わないなら、じゃあ毎回 Lesson.active.find(x) とか User.active.find(x)をつけなきゃいけないの? というのもごもっともだと思います。

around_actionとscopingを使おう

こういうときにaround_actionscopingが便利です。 scopingはRailsであまり活用されていないが便利なメソッドで、次のようにブロックの中限定でdefault_scope的な挙動を実現できます。

Comment.where(post_id: 1).scoping do
  Comment.first
end
# => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1

around_actionbefore_actionに比べると使い所がわかりづらいフィルタですが、scopingを組み合わせるとこのようなコードが書けます。

around_action :set_scope

def set_scope
  Blog.where(removed: false).scoping do
    yield
  end
end

非表示フラグが立ったPostモデルで考える

ブログがあり、一つ一つの記事がPostというモデルだとします。ここにhiddenというフラグがあり、trueの場合は非公開の記事だとします。 Post.visiblehidden=falseのPostを返すものとします。

このとき、よくやってしまうのが

def index
  @posts = Post.visible.order(created_at: :desc).limit(10) #=> ちゃんとvisibleをつけている
end

def show
  @post = Post.find(params[:id]) #=> visibleをつけていない!
end

のように、うっかりフラグの確認を抜かしてしまうミスです。これはやりがちなうえ、秘匿性の高い情報をあっさり公開してしまう大事故につながるので絶対に避けたいところです。default_scopeを使ってhidden=falseとしたくなるのも気持ちは分かります。

こういうとき、上の方法を使いましょう。 controllerを

├─application_controller.rb
├─front_controller.rb (ブログ閲覧者用の画面)
├─front
│  └─posts_controller.rb
├─admin_controller.rb (ブログ管理者用の画面)
├─admin
│  └─posts_controller.rb

のような構造にします。こうするとFront::PostsControllerAdmin::PostsControllerができます。 これだとFrontのほうでは常にhidden=falseが保証されればいいので、以下のようにすればいいのです。

app/views/front_controller.rb

class FrontController < ApplicationController
  around_action :set_scope

  def set_scope
    Post.visible.scoping do
      yield
    end
end

これでFront側でhidden=trueな記事が誤って露出する心配はありません。

スコープや閲覧権限の確認は入念に。

セキュリティの話でXSSやSQLインジェクションはコードだけで判別できるのですが、アプリケーションの仕様通りの閲覧権限・スコープになっているかはビジネスロジックなので、入念に確認しないとバグや脆弱性がわかりづらいです。上の方法はある程度有効なのですが、最終的には個々のactionごとに手を抜かず確認しましょう。

Railsでfull_messagesを加工したい

基本的にRailsのvalidationは

class Blog < AR::Base
  validates :title, presence: true
  validate :title_or_content_needed
end

のようなとき、@blog.errors.full_messagesにはattribute名 + エラー本文に加工されます。 ところで、title_or_content_neededのようにattributeをまたいだり、特定のattributeによらないバリデーションの場合メッセージはattribute名を含まないでほしいことがあります。

地味ですがこういうときにbaseという特別なattributeを指定でき、

def title_or_content_needed
  if title.blank? && content.blank?
    errors.add(:base, :title_or_content_needed)
  end
end

という指定が可能です。辞書ファイル側でtitle_or_content_neededを"タイトルかコンテンツは必須です"のように定義すると、full_messagesでもそのまま出力されます。

Railsでbase64エンコードされた画像を使う方法

例えば必須の入力フォームの左側に小さい※を画像で出したい時、画像を使ったほうが楽なケースがあります。ただ、小さい画像でも画像数が増えてしまうと当然Webサーバのリクエスト頻度が増えるので、このような画像はbase64エンコードした上でcssに埋め込みたいケースがあります。

https://github.com/rails/sass-rails のレポジトリでちゃんと書いてありますが、標準でasset-data-url(path)という便利なメソッドが用意されていました。

これを使えば

.hoge
  background: asset-data-url('shared/form-must-star.png') no-repeat 4px     8px

のようにBase64画像を扱えます。

欲を言えば画像サイズや画像数に応じて自動的にエンコードすべきかどうか判定してくれるといいかもしれません。

定番 スタートアップとは何か・スタートアップを知るための本

ビジネスモデル・ジェネレーション

ビジネスモデルの構造をパートナーや顧客セグメントといった9つの重要事項に分類して分析・発展させていくための考え方がまとめられています。

スタートアップ・マニュアル

スタートアップ・マニュアル ベンチャー創業から大企業の新事業立ち上げまで
スティーブン・G・ブランク ボブ・ドーフ
翔泳社
売り上げランキング: 48,880

初期のスタートアップで非常に重要な顧客開発について詳細に解説されています。顧客開発とはなにかはこちらの記事などが参考になります。

リーンスタートアップ

言わずと知れたベストセラーです。手法には賛否両論ありますが、ここ数年のベンチャー業界を席巻した考えであることは間違いありません。

How Google Works

IT企業の王様、Googleエリック・シュミットらによってGoogle文化が紹介されています。Googleがいかに会社の文化づくりを重要視しているかがわかります。

ビジネスモデル全史

ベンチャーにかぎらず、ハーバード・ビジネス・レビュー誌上で2014年ベスト経営書に輝いた、ビジネスモデルの変遷を綴った本です。古典的なものから近年の発展の流れがわかります。

社内共有ツールのまとめ

t-kot です。社内共有ツールは多くの企業でも使われていると思います。一般に社内の重要な情報が特定の人にしか知られていない場合、その人が退職するとフローが回らなくなるなどのリスクがあるので暗黙知は減らすべきとされています。

弊社ではQiita team を使っています。最大の特徴はMarkdownでサクサク書けることで、これはエンジニアにとってはウケがいいのですがそうでない人は少しむずかしいかもしれません。

そこで社内共有ツールを調べてみたので、まとめていきたいと思います。

gamba!

gamba!は日報共有ツールです。

日報に大きく機能を絞って、その他に目標への進捗などを共有する機能があります。

Connect

Connectは社内SNSのようなもので、海外のYammerに近い作りになっています。

Chatwork

チャット機能がメインですが、タスク管理やファイル共有など、一般的な社内ツールの機能が含まれています。マルチデバイス対応しているのも特徴です。

Basecamp

創業者がRuby on Railsの開発者であることでも知られるBasecampはカンバンのようなツールです。私も使用したことがありますが、アジャイル開発の1つのイテレーション(1〜2週間程度の機能開発)をスレッドとして、掲示板感覚で使うのが最も適した使い方でした。逆に、バグ一覧のような感じで使用したスレッドはどんどん下に追いやられてしまい、厳密な責任分担やフローの構築には向かない印象です。

Confluence

Hipchatなどを提供するatlassian社による社内wikiサービスです。ドキュメント作成フォームのインターフェースがかなり作りこまれていて、複雑なドキュメントも簡単に作成できるようです。

Direct

LineのようなUIのチャットツールです。社内コミュニケーションでスタンプが使えるのは斬新です。

Slack

Slackはチャットツールです。チャットツールは乱立していますが、スタートアップ・テクノロジー業界で今年Hipchatにかわり急激にシェアを伸ばしたサービスです。

キーボードだけでの操作が可能、検索機能が豊富、UIが美しいなど、とくにキラー機能があるわけではないですが全体的にHipchatよりも完成度が高いそうです。

まとめ

多くあるサービスも基本的には・情報共有ツール、・チャットツール、・タスク管理 など、それぞれの役割に分けることができます。

基本的にはこの中から被らないように取捨選択すれば良いと思いますが、最も重要なのはツール以上に会社でそれを使っていく文化を作ることです。ある会社では絶賛されたツールも、別の会社では全く使われないということがよくありますが、文化の違い、使用方法の根付きなどが違いに出るでしょう。今後はその辺りの運用カルチャーについてもまとめたいと思います。

スマホネイティブ時代のログインUXを考える

最近の若い世代はパスワードを打ち込むことに全く慣れていなくて、facebookなんかのパスワードも覚えてないらしい。 というのは、スマホならアプリは常時ログイン状態だし、うまく作りこんであるサービスなら別アプリでログインする際もfacebookアプリが一度立ち上がって、すぐに戻ってくる。これでfacebookログインが完了するというわけ。だからメールアドレスを打ち込んで、パスワードを打ち込んで会員登録・ログインなんていうUXはかなり古い。

というわけで、最近はネイティブアプリの場合起動時に端末ユーザIDを付与して、サーバサイドでは通常のユーザ同様に扱ってしまうケースが多いと思う。この場合困るのはこういったメアド・パスワードを持っていないユーザに対して別機種やブラウザからのログインをどうやって許可するかという方法である。

本記事ではパスワードフリーかつ安全なセッション引き継ぎ方法を考察してみる。

モンスターストライク

www.monster-strike.com

Googleアカウントで引き継いでいる。これはGoogleアカウントを持っているならもっともカンタンで安全なセッション移行方法だろう。

パズドラ

pad.gungho.jp

AndroidGoogleアカウントで、iOSはユーザIDと機種変コード(期限付き)を発行している。 IDがなんなのかよくわからないけど、そのあとにゲーム内の名前も入力させているので内部のIDでしょう。

LINE(2016年2月以前)

official-blog.line.me

LINEはもともとメールアドレス、パスワードの認証に加えてPINコードという二段階認証。といってもよくある、「別端末でログインされたのでこのPINコードを入力してください」みたいなのが旧端末に届くわけではなく、もともと設定してある暗証番号なので要するにパスワードが1個多いだけ。

ちなみに最近の変更ではいくつかの条件下で、引き継ぎ許可が旧端末で必要になった。

メルカリ

シンプルに、Googlefacebookでのログインとメール・パスワードのログインを提供。モンストとかと違うのはユーザに認証が必須になっていること。(余談だけどGoogle/Facebookの組み合わせでtwitterがないのはtwitterはメールアドレス取れないから。ちなみにyahooはOAuthであんま見ないけどメール取れるしけっこうユーザいる)

まとめ

以上のようにOAuthを使うか、アプリ内の固有IDにシステムから期限付きパスワードを発行してあげるのが一般的。

ちなみにセキュリティに詳しいわけではないので間違ったことかもしれないが、私の考える簡易的かつ安全な実装はこんな感じ。

①アプリからセッション発行する際にtouch idを使う ②アプリで6桁くらいの数字を発行。期限は60秒 ③ブラウザで6桁の数字を入力してログインボタンを押す ④アプリに許可/不許可の選択をさせる

これならスマホで文字を打つこともないし、入力もたかだか6桁なのでカンタンだと思う。期限60秒なら6桁でもかぶることはないだろうし(1分以内にこの機能を100万人が使うことはない)。

ランダムでアタックを常にされて4のステップまでまともにたどり着かないことも考えられるので、それなら②をもっと複雑に。ユーザIDを出すとか。

Google Cloud Vision APIを使ってみる

qiita.com

この記事が随分バズりましたが、去年後半にGoogleが画像認識APIを公開しました。 画像認識APIにはいくつか種類があって、安全な画像か(アダルトや暴力が含まれないか)を判定するAPI、画像に何が写っているか判定するAPI、画像中の文字列を解析するAPIなどがあります。

今回は画像に何が写っているかのAPI(LABEL_DETECTION)を使ってみます。

サンプルコード

class CloudVision
  class MissingError < StandardError;end

  ENDPOINT_URL = "https://vision.googleapis.com/v1alpha1/images:annotate?key=#{Settings.services.google.server_api_key}"

  attr_reader :result

  def initialize(url)
    @image_url = url
  end

  def request
    uri = URI.parse(ENDPOINT_URL)
    https = Net::HTTP.new(uri.host, uri.port)
    https.use_ssl = true

    image = open(@image_url).read
    content = Base64.strict_encode64(image)
    req = Net::HTTP::Post.new(uri.request_uri)
    req.body = request_json(content)
    req['Content-Type'] = 'application/json'

    res = https.request(req)
    if res.code.to_i == 200
      @result = JSON.parse(res.body).with_indifferent_access
    else
      raise MissingError.new('cannot fetch cloud vision result')
    end
  end

  private

  def request_json(content)
    {
      requests: [{
        image: {
          content: content
        },
        features: [{
          type: 'LABEL_DETECTION',
          maxResults: 10
        }]
      }]
    }.to_json
  end
end

APIはいくつかあるといいましたが、エンドポイントとしては一つで本文のパラメータで何のAPIを使っているかを指定しています。 このAPIはまだアルファ版なので、申請しないと使えません。が、フォームから簡単に登録できたので難しい審査があるわけではないようです。

APIキーの取得(申請後)

f:id:attracie:20160204190701p:plain

上の画面からCloud Vision APIを開き、APIを有効化する。

あとはタイプがサーバーキーのAPIキーを認証情報の中から作成。

このキーはサーバ用なので、ローカルで使う分には問題ありませんがクライアントアプリなどに組み込んで配布してはいけません。

サンプル実行

1.壁紙

f:id:attracie:20160204191103j:plain

結果

 {"responses"=>[{"labelAnnotations"=>[{"mid"=>"/m/083jv", "description"=>"white", "score"=>0.92508286}, {"mid"=>"/m/02rfdq", "description"=>"interior design", "score"=>0.829825}, {"mid"=>"/m/09qqq", "description"=>"wall", "score"=>0.77109945}, {"mid"=>"/m/03gfsp", "description"=>"ceiling", "score"=>0.57515627}, {"mid"=>"/m/06244p", "description"=>"window film", "score"=>0.57092083}, {"mid"=>"/m/03rszm", "description"=>"curtain", "score"=>0.56402957}, {"mid"=>"/m/0hwky", "description"=>"pattern", "score"=>0.52074558}]}]}

どうやらスコア度が高いものから順番に送ってくれるようです。結果は 白、インテリアデザイン、壁、天井、窓フィルム、カーテン、パターン?となりました。まぁ約0.7以上は間違ってるわけではないのでまぁまぁ。

  1. 切り株に土を盛る作業の画像

f:id:attracie:20160204191448j:plain

結果

{"responses"=>[{"labelAnnotations"=>[{"mid"=>"/m/01m3tw", "description"=>"animal shelter", "score"=>0.58235466}, {"mid"=>"/m/09qqq", "description"=>"wall", "score"=>0.53854597}]}]}

今度は動物の檻、壁という結果に。うーん、金網を檻と判断したか・・・。

  1. フローリング

f:id:attracie:20160204191642j:plain

結果

{"responses"=>[{"labelAnnotations"=>[{"mid"=>"/m/0l7_8", "description"=>"floor", "score"=>0.96207434}, {"mid"=>"/m/020g49", "description"=>"hardwood", "score"=>0.91214508}, {"mid"=>"/m/083vt", "description"=>"wood", "score"=>0.87281144}, {"mid"=>"/m/01c34b", "description"=>"flooring", "score"=>0.83906168}, {"mid"=>"/m/07j7r", "description"=>"tree", "score"=>0.74765408}, {"mid"=>"/m/0hlzt", "description"=>"deciduous", "score"=>0.6577369}, {"mid"=>"/m/05t06b", "description"=>"wood stain", "score"=>0.57129413}]}]}

床、硬材、木材、フローリング、木、落葉樹、木材着色剤

まぁだいたい合ってますね!

4. 棚

f:id:attracie:20160204191914j:plain

結果

{"responses"=>[{"labelAnnotations"=>[{"mid"=>"/m/0c_jw", "description"=>"furniture", "score"=>0.88417172}, {"mid"=>"/m/0l7_8", "description"=>"floor", "score"=>0.82036376}, {"mid"=>"/m/020g49", "description"=>"hardwood", "score"=>0.81980395}, {"mid"=>"/m/01c34b", "description"=>"flooring", "score"=>0.78891808}, {"mid"=>"/m/07p_zw", "description"=>"handrail", "score"=>0.78310829}, {"mid"=>"/m/083vt", "description"=>"wood", "score"=>0.74275166}, {"mid"=>"/m/0gjbg72", "description"=>"shelf", "score"=>0.69625175}, {"mid"=>"/m/0h8n982", "description"=>"shelving", "score"=>0.6897167}, {"mid"=>"/m/03vf67", "description"=>"sideboard", "score"=>0.61258972}]}]}

家具、床、硬木、フローリング、手すり、木、棚、サイドボード

間違ってるわけではないが、フローリングが棚より上位になっている。おそらく形状よりもテクスチャで評価されてるんだろう。(棚に貼ってあるシートだけを見るとフローリングにも見えなくもない)

  1. 蜜を吸う蜂

f:id:attracie:20160204192316j:plain

{"responses"=>[{"labelAnnotations"=>[{"mid"=>"/m/0c9ph5", "description"=>"flower", "score"=>0.90215278}, {"mid"=>"/m/016gq4", "description"=>"morus", "score"=>0.84567684}, {"mid"=>"/m/07j7r", "description"=>"tree", "score"=>0.82456774}, {"mid"=>"/m/04sjm", "description"=>"flowering plant", "score"=>0.80608088}, {"mid"=>"/m/05s2s", "description"=>"plant", "score"=>0.78441721}, {"mid"=>"/m/02xwb", "description"=>"fruit", "score"=>0.77949041}, {"mid"=>"/m/09t49", "description"=>"leaf", "score"=>0.72556472}, {"mid"=>"/m/01c2vn", "description"=>"elderberry", "score"=>0.69951051}, {"mid"=>"/m/0dxb5", "description"=>"berry", "score"=>0.69494152}, {"mid"=>"/m/084z41", "description"=>"viburnum opulus", "score"=>0.69100761}]}]}

花、クワ、木、植木、フルーツ、葉、ニワトコ

一番目立つものについて注力して分析した感じ。

  1. 家の流し

f:id:attracie:20160204192550j:plain

{"responses"=>[{"labelAnnotations"=>[{"mid"=>"/m/0130jx", "description"=>"sink", "score"=>0.78636473}, {"mid"=>"/m/07yv9", "description"=>"vehicle", "score"=>0.77838421}, {"mid"=>"/m/032rk", "description"=>"gun", "score"=>0.5884015}, {"mid"=>"/m/01kcd", "description"=>"brass instrument", "score"=>0.55859768}]}]}

シンク、乗り物、銃、金管楽器・・・。

誤判定が目立ちます。とくに少しテストした感じでは光物、金属的なものが含まれる画像はやたら乗り物(vehicle)に含まれてしまう印象でした。

6-2. 参考

別のサイトの画像です。今度は商品画像的な綺麗な写りのもの。

http://www.amick.net/hikone/setubi/KitchenSink/3821.jpg

{"responses"=>[{"labelAnnotations"=>[{"mid"=>"/m/02pkr5", "description"=>"plumbing fixture", "score"=>0.99995482}, {"mid"=>"/m/0130jx", "description"=>"sink", "score"=>0.99663955}, {"mid"=>"/m/080sjd", "description"=>"farmhouse", "score"=>0.85322839}, {"mid"=>"/m/06ht1", "description"=>"room", "score"=>0.75796545}, {"mid"=>"/m/01jwgf", "description"=>"product", "score"=>0.74807149}]}]}

今度はだいぶいいですね。当たり前ですが、amazonとかの商品画像みたいな、綺麗な画像が一番はっきり判定できます。

総評

例えばPinterestのようなサイトで、投稿画像を自動的に判定してscoreが0.8以上のものにそのままタグ付けする、というような用途ができればなぁと思いましたが、この結果を見る限り厳しいですね。 例えば記事の見出しになるような写真なら間違えないんですが、素人がぱっと撮った写真はダメな印象です。

実際問題としてはもう一段階ステップがあって、例えば想定される画像を絞って判定パターンを限定してあげる(何が写っているかより、壁なのかトイレなのか風呂なのかなどを判定するほうが確実に精度がいい)などが必要になってくるのかなと思います。