wataメモ

日々のメモをつらつらと書くだけ

Ruby on Railsでサービスを作ってみたpart2

 今回は前回の続きということで、画面や機能の開発周りの事を書いていく。画面開発自体は2日ぐらいでがっと作っただけなので、そんなに複雑でもないし画面数が多いわけではない。サイトマップは以下のようにシンプルな構成だ。

f:id:wata_htn:20150929232058p:plain

アンケート一覧

画面キャプチャ

f:id:wata_htn:20150929232255p:plain

ヘッダ周り

 URL的にはTOP画面となり、「survey!」の起点となる画面だ。ヘッダには左上がロゴ的なイメージでTOP画面へのリンクとなっている。右上のOctocatはsurveyのソースが公開されているGitHubリポジトリへのリンクとなっている。 「お申し込みはこちら」ボタンは現状では飾りだ。連打したければいくらでも連打出来る。作り的にはBacklogの様にサブドメイン毎にアンケートが別れるようになっている。例えば「aaa.survey.example.com」と「bbb.survey.example.com」は別々の一覧となり、互いのアンケートを見ることが出来ない様になっている。イメージとしてはIP制限や何かしらの認証を掛けて、認証が通った人のみに閲覧できるようにすれば良いなと考えた。(現状は実装してないけど)無記名とはいえステートレスなので同じユーザを特定しなければならない。その為にはCookie(本当は年単位のやつを発行したいが今はしていない)にuuidを保持させそれを持って同一ユーザとして特定する予定。

新規作成ボタン

 「新規作成」ボタンを押すとモーダルでアンケート新規作成を行う。いわゆる「登録画面」を設けなかったのは「新規登録」はAPIで行い、その後は必ず「編集画面」へ遷移させることで、登録と編集の画面を一元化させる目的がある。もちろん「partial」でフォームの内容を共有したりすることで一元化させることも可能だが、今回はこの形で進めることにした。

新規登録モーダル

f:id:wata_htn:20150929232934p:plain

 「survey!」は無記名を基本としているので、結果画面は途中だろうがなんだろうが誰でも見えてしまう。「最終的な結果が出るまでは途中経過も見せてくない」という場合野為に「非公開」という機能を用意した。これはチェックを付けて新規作成すると、ハッシュ値が生成され、それを知らないとアクセス出来ないようにしている。なので新規登録した人はこのURLを覚えておく必要がある。

実装について

 モーダル表示自体はSemantic UIの機能を使って表示しているだけ。「OK」ボタンを押すとAjaxでアンケート登録APIを叩き、OKなら編集画面へJavaScript側で遷移させている。かなりパワフルなコードなので参考にはしないほうが良いだろう。

  $('.ui.modal.reg').modal 'setting', {
    onHide: ->
      return unless reg
      h = if hash then "?hash_key=#{hash}" else ''
      location.href = "#{location.href}survey/#{sid}/edit#{h}"
    onApprove: ->
      s = $('#is_result_secret').prop('checked')
      $.post 'survey', {surveys: {name: $('#name').val(), is_result_secret: s, hash_key: ''}}
        .done (r) ->
          return alert 'アンケートのタイトルを入れてください' if r.is_success != 1
          reg = true
          survey = r.results
          sid = survey.id
          hash = r.results.hash_key if survey.is_result_secret
          $('.ui.modal.reg').modal 'hide'
      false
  }

 軽く解説すると、モーダルを表示させ、OKボタンが押された場合はonApproveコールバックが呼ばれるのでそこでAjaxを投げている。そのレスポンスデータが成功だった場合はhideを呼び出し、onHideイベントでlocation.hrefで移動している。ちゃんとエラーをハンドリングし、ユーザにエラーの内容を返すべきだが横着してこの辺はまだ出来ていない。

ページャリンク

 ページャの部分はKaminariのデフォルトの動きを使っているだけなので特に書くことは無い。

一覧

実装について

 Kaminariを使っての実装なので基本は簡単だが、サブドメイン対応を行うためにScopeを1つ作っていてそれを利用して絞り込んでいる。

app/controllers/welcome_controller.rb

@surveys = Surveys.page(params[:page])
  .joins(:group)
  .eager_load(:questions)
  .current_group(request.subdomain) # ここでsubdomainを渡している
  .order(:updated_at => :desc)

app/models/surveys.rb

scope :current_group, ->(subdomain) { joins(:group).where(groups: {name: subdomain}) }

サムネイル

 一覧部分に関しては、一番左にアンケートに画像が登録されていればそのサムネイルが表示される。後のpartで紹介する予定だが、画像の表示にはsmall_lightを使っている。画像自体はCarrierWave + fogにS3にアップロードさせて、nginxに組み込んだsmall_lightがリバースプロキシの形でS3にアクセスし、内部的に要求されたサイズの画像にリサイズして返却している。もちろんその結果はCloudFront経由で配信している。今回は最終的には幅と高さを自由に指定出来る形にしているが、本来は指定出来るサイズは制限させるべきだろう。

実装について

 画像のURLについてはCarrierWaveのデフォルト実装のpublic_urlの結果では、small_lightが求める形には出来ない。そこでinitializerでHelperに「image_tag_resize」を追加し、パワフルに解決している。

config/initializers/image_tag_helper.rb

module ActionView
  module Helpers
    module AssetTagHelper
      def image_tag_resize src, opt = {}
        path = src.url.gsub Survey::Application.config.image_asset_host, ''
        opt[:src] = File.join Survey::Application.config.image_asset_host, "/resize/w/#{opt[:dw]}/h/#{opt[:dh]}/", path
        tag("img", opt.except(:dw, :dh))
      end
    end
  end
end

 テンプレート側で表示すべきサイズを渡したいのでoptionとして「dw」と「dh」というのを追加した。大元のtagに渡す時には不要なので使った後は削除はしている。テンプレートでの使い方のイメージとしては以下になる。dwが幅で、dhが高さだ。

= image_tag_resize s.image, dw: 50, dh: 50, class: 'ui image'

アンケート回答リンク

 次がアンケートのタイトルに回答画面へのリンクが貼られている。「非公開」なアンケートは背景が黄色く(GitHubのprivateリポジトリイメージ)なり、タイトルの横に鍵マークが付く。「非公開」アンケートでも回答自体は誰でも、いつでも行うことが出来る。

作成、更新

 あまり出してもそこまで意味が無い情報だが、通常の日時と「time_ago_in_words」を使いたい為だけに表示。軽くレスポンシブ対応もしているので、スマフォのポートレートモドで見るとサムネイルや作成、更新カラムが消えるようにした。

f:id:wata_htn:20150929234250p:plain

編集、結果リンク

 それぞれ「編集画面」、「結果画面」へ遷移するリンクとなっている。「非公開」なアンケートはdisabledになっており押すことは出来ない。もちろん直リンクしてもハッシュ値を知らないと404を返すようになっている。

アンケート回答

画面キャプチャ(質問画像無し)

f:id:wata_htn:20150929234918p:plain

画面キャプチャ(質問画像有り)

f:id:wata_htn:20150929235024p:plain

ヘッダー周り

 アンケートのタイトルが表示されている。その下のStepはアンケートの質問数と現在自分がどこまで答えているかを表している。答え終わった質問については緑のチェックマークに変わり、それはリンクとなっていて、いつでもその質問に戻ることも出来る。

アンケート画像

 各質問毎に画像を登録することが出来、登録されている場合は表示される。ここも同じくsmall_lightとCloudFrontでの配信となっている。

回答

 複数選択か単数選択、フリーテキスト入力、日付選択(調整さんの様な機能)がある。

フリーフォーマット

f:id:wata_htn:20150929235844p:plain

日付選択

f:id:wata_htn:20150930000131p:plain

実装について

 まずはルーティングについてだが、今回はresourcesのネストをすることで指定を行っている。アンケート(survey)には複数の質問(question)があり、それには回答(answer)がある形になっている。具体的なルーティングは以下のようになっている。

config/routes.rb

resources :survey do
  resources :question, only: :show do
    resources :answer, only: [:create, :update]
  end
end

 上記により生成されるルーティングは以下となる。

                      Prefix Verb   URI Pattern                                                   Controller#Action
survey_question_answer_index POST   /survey/:survey_id/question/:question_id/answer(.:format)     answer#create
      survey_question_answer PATCH  /survey/:survey_id/question/:question_id/answer/:id(.:format) answer#update
                             PUT    /survey/:survey_id/question/:question_id/answer/:id(.:format) answer#update
             survey_question GET    /survey/:survey_id/question/:id(.:format)                     question#show

 テンプレートについては質問の基本的な部分は同じだが、回答の部分はタイプによって違う。テンプレートの中で分岐は入れず、activerecordenumを使ってテンプレートファイルを出し分けている。

= render partial: "question/#{@question.type}"

 @question.typeが「single」「multiple」「free」「date」となるのでそれぞれのpartialようのテンプレートを準備した。例えば単一選択の場合は「app/views/question/_single.html.slim」となる。
 この様に実装することによりif分岐を無くし、テンプレートファイルを分割管理出来ることで、1つのでかいテンプレートにならず、開発保守しやすくしている。ファイル名は「partial」の場合は「_」から始める。

戻る、次へ、完了ボタン

 2問目以降だと「戻る」ボタンが表示され、1問前に戻ることが出来る、次の問がある場合は「次へ」ボタン、今回の質問が最後の場合は「完了」ボタンが表示される。

実装について

 大した話ではないが、1つ前が存在し、そのidが必要な場合はテンプレート側には実装せずにmodel側にロジックを持たせる。いわゆる「Fat Model」にならないように気をつけなければならないが、テンプレート側に持たせるよりmodelに持たせたほうがオブジェクト指向的には正しいし、テンプレート側がシンプルになる。

- if @question.prev
  = link_to survey_question_path(@survey, @question.prev), :class => 'ui small button' do

 リンクを張る時もベタでURLを書かずにパス(この例だとsurvey_question_path)を利用して記述する。

アンケート完了

 単なる環境画面でほぼ静的画面。

画面キャプチャ

f:id:wata_htn:20150930000306p:plain

実装について

 基本的にはPRGパターン実装をしているのでリダイレクトで完了画面は表示している。

# 次がある場合は次の画面へリダイレクト
return redirect_to survey_question_path(s, q.next) if q.next

# 最後の場合はアンケート回答完了ステータスにし、完了画面へリダイレクト
c.done!
redirect_to finish_survey_path

次回について

 長くなってきたので今回はこの辺で。次回も引き続き残りの画面の作り等を書いていく。