default_scopeよりaround_actionとscopingを使おう
default_scope is evil は浸透してきたが...
最近では 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_action
とscoping
が便利です。
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_action
もbefore_action
に比べると使い所がわかりづらいフィルタですが、scoping
を組み合わせるとこのようなコードが書けます。
around_action :set_scope def set_scope Blog.where(removed: false).scoping do yield end end
非表示フラグが立ったPostモデルで考える
ブログがあり、一つ一つの記事がPost
というモデルだとします。ここにhidden
というフラグがあり、true
の場合は非公開の記事だとします。
Post.visible
はhidden=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::PostsController
とAdmin::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つの重要事項に分類して分析・発展させていくための考え方がまとめられています。
スタートアップ・マニュアル
初期のスタートアップで非常に重要な顧客開発について詳細に解説されています。顧客開発とはなにかはこちらの記事などが参考になります。
リーンスタートアップ
言わずと知れたベストセラーです。手法には賛否両論ありますが、ここ数年のベンチャー業界を席巻した考えであることは間違いありません。
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を付与して、サーバサイドでは通常のユーザ同様に扱ってしまうケースが多いと思う。この場合困るのはこういったメアド・パスワードを持っていないユーザに対して別機種やブラウザからのログインをどうやって許可するかという方法である。
本記事ではパスワードフリーかつ安全なセッション引き継ぎ方法を考察してみる。
モンスターストライク
Googleアカウントで引き継いでいる。これはGoogleアカウントを持っているならもっともカンタンで安全なセッション移行方法だろう。
パズドラ
AndroidはGoogleアカウントで、iOSはユーザIDと機種変コード(期限付き)を発行している。 IDがなんなのかよくわからないけど、そのあとにゲーム内の名前も入力させているので内部のIDでしょう。
LINE(2016年2月以前)
LINEはもともとメールアドレス、パスワードの認証に加えてPINコードという二段階認証。といってもよくある、「別端末でログインされたのでこのPINコードを入力してください」みたいなのが旧端末に届くわけではなく、もともと設定してある暗証番号なので要するにパスワードが1個多いだけ。
ちなみに最近の変更ではいくつかの条件下で、引き継ぎ許可が旧端末で必要になった。
メルカリ
シンプルに、Googleかfacebookでのログインとメール・パスワードのログインを提供。モンストとかと違うのはユーザに認証が必須になっていること。(余談だけどGoogle/Facebookの組み合わせでtwitterがないのはtwitterはメールアドレス取れないから。ちなみにyahooはOAuthであんま見ないけどメール取れるしけっこうユーザいる)
まとめ
以上のようにOAuthを使うか、アプリ内の固有IDにシステムから期限付きパスワードを発行してあげるのが一般的。
ちなみにセキュリティに詳しいわけではないので間違ったことかもしれないが、私の考える簡易的かつ安全な実装はこんな感じ。
①アプリからセッション発行する際にtouch idを使う ②アプリで6桁くらいの数字を発行。期限は60秒 ③ブラウザで6桁の数字を入力してログインボタンを押す ④アプリに許可/不許可の選択をさせる
これならスマホで文字を打つこともないし、入力もたかだか6桁なのでカンタンだと思う。期限60秒なら6桁でもかぶることはないだろうし(1分以内にこの機能を100万人が使うことはない)。
ランダムでアタックを常にされて4のステップまでまともにたどり着かないことも考えられるので、それなら②をもっと複雑に。ユーザIDを出すとか。
Google Cloud Vision APIを使ってみる
この記事が随分バズりましたが、去年後半に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キーの取得(申請後)
上の画面からCloud Vision APIを開き、APIを有効化する。
あとはタイプがサーバーキーのAPIキーを認証情報の中から作成。
このキーはサーバ用なので、ローカルで使う分には問題ありませんがクライアントアプリなどに組み込んで配布してはいけません。
サンプル実行
1.壁紙
結果
{"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以上は間違ってるわけではないのでまぁまぁ。
- 切り株に土を盛る作業の画像
結果
{"responses"=>[{"labelAnnotations"=>[{"mid"=>"/m/01m3tw", "description"=>"animal shelter", "score"=>0.58235466}, {"mid"=>"/m/09qqq", "description"=>"wall", "score"=>0.53854597}]}]}
今度は動物の檻、壁
という結果に。うーん、金網を檻と判断したか・・・。
- フローリング
結果
{"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. 棚
結果
{"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}]}]}
家具、床、硬木、フローリング、手すり、木、棚、サイドボード
。
間違ってるわけではないが、フローリングが棚より上位になっている。おそらく形状よりもテクスチャで評価されてるんだろう。(棚に貼ってあるシートだけを見るとフローリングにも見えなくもない)
- 蜜を吸う蜂
{"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}]}]}
花、クワ、木、植木、フルーツ、葉、ニワトコ
一番目立つものについて注力して分析した感じ。
- 家の流し
{"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. 参考
別のサイトの画像です。今度は商品画像的な綺麗な写りのもの。
{"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以上のものにそのままタグ付けする、というような用途ができればなぁと思いましたが、この結果を見る限り厳しいですね。 例えば記事の見出しになるような写真なら間違えないんですが、素人がぱっと撮った写真はダメな印象です。
実際問題としてはもう一段階ステップがあって、例えば想定される画像を絞って判定パターンを限定してあげる(何が写っているかより、壁なのかトイレなのか風呂なのかなどを判定するほうが確実に精度がいい)などが必要になってくるのかなと思います。