Ruby on Railsでサービスを作ってみたpart2
今回は前回の続きということで、画面や機能の開発周りの事を書いていく。画面開発自体は2日ぐらいでがっと作っただけなので、そんなに複雑でもないし画面数が多いわけではない。サイトマップは以下のようにシンプルな構成だ。
アンケート一覧
画面キャプチャ
ヘッダ周り
URL的にはTOP画面となり、「survey!」の起点となる画面だ。ヘッダには左上がロゴ的なイメージでTOP画面へのリンクとなっている。右上のOctocatはsurveyのソースが公開されているGitHubリポジトリへのリンクとなっている。 「お申し込みはこちら」ボタンは現状では飾りだ。連打したければいくらでも連打出来る。作り的にはBacklogの様にサブドメイン毎にアンケートが別れるようになっている。例えば「aaa.survey.example.com」と「bbb.survey.example.com」は別々の一覧となり、互いのアンケートを見ることが出来ない様になっている。イメージとしてはIP制限や何かしらの認証を掛けて、認証が通った人のみに閲覧できるようにすれば良いなと考えた。(現状は実装してないけど)無記名とはいえステートレスなので同じユーザを特定しなければならない。その為にはCookie(本当は年単位のやつを発行したいが今はしていない)にuuidを保持させそれを持って同一ユーザとして特定する予定。
新規作成ボタン
「新規作成」ボタンを押すとモーダルでアンケート新規作成を行う。いわゆる「登録画面」を設けなかったのは「新規登録」はAPIで行い、その後は必ず「編集画面」へ遷移させることで、登録と編集の画面を一元化させる目的がある。もちろん「partial」でフォームの内容を共有したりすることで一元化させることも可能だが、今回はこの形で進めることにした。
新規登録モーダル
「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」を使いたい為だけに表示。軽くレスポンシブ対応もしているので、スマフォのポートレートモドで見るとサムネイルや作成、更新カラムが消えるようにした。
編集、結果リンク
それぞれ「編集画面」、「結果画面」へ遷移するリンクとなっている。「非公開」なアンケートはdisabledになっており押すことは出来ない。もちろん直リンクしてもハッシュ値を知らないと404を返すようになっている。
アンケート回答
画面キャプチャ(質問画像無し)
画面キャプチャ(質問画像有り)
ヘッダー周り
アンケートのタイトルが表示されている。その下のStepはアンケートの質問数と現在自分がどこまで答えているかを表している。答え終わった質問については緑のチェックマークに変わり、それはリンクとなっていて、いつでもその質問に戻ることも出来る。
アンケート画像
各質問毎に画像を登録することが出来、登録されている場合は表示される。ここも同じくsmall_lightとCloudFrontでの配信となっている。
回答
複数選択か単数選択、フリーテキスト入力、日付選択(調整さんの様な機能)がある。
フリーフォーマット
日付選択
実装について
まずはルーティングについてだが、今回は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
テンプレートについては質問の基本的な部分は同じだが、回答の部分はタイプによって違う。テンプレートの中で分岐は入れず、activerecordのenumを使ってテンプレートファイルを出し分けている。
= 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)を利用して記述する。
アンケート完了
単なる環境画面でほぼ静的画面。
画面キャプチャ
実装について
基本的にはPRGパターン実装をしているのでリダイレクトで完了画面は表示している。
# 次がある場合は次の画面へリダイレクト return redirect_to survey_question_path(s, q.next) if q.next # 最後の場合はアンケート回答完了ステータスにし、完了画面へリダイレクト c.done! redirect_to finish_survey_path
次回について
長くなってきたので今回はこの辺で。次回も引き続き残りの画面の作り等を書いていく。