[coreserve] 管理画面

管理画面、つまりセッションの作成、修正、削除は、jascaffoldベースで作りました。追加した機能としては以下の通りです。

  • カウンセラー以外にはアクセスできないようにした
  • 新規作成だけは常時使用するので、初期値の表示等、使いやすくする工夫をした
  • 一覧表示をステータス別に表示できるようにした
  • セッションとクラアントの関連づけを操作できるようにした
  • データのチェックの機能を追加した

この部分は、使う人間が限られているので、多少の問題があってもなんとかなるという判断で、テストを後回しにして実装してしまいました。だから、多少問題が残っているかもしれません。結果的に見ると、Railsの機能がわからなくて迷走した所もあったので、かえって時間がかかったかもしれません。

アクセス制限は、LoginEngineの機能を利用して次のように実装しました。

class ReservationController < ApplicationController
  before_filter :login_required, :must_be_counselor
  .....
private
  def must_be_counselor
    unless current_user and current_user.kind_of?(Counselor)
      flash[:warning] = 'カウンセラー以外にはアクセスできません'
      return false
    end
  end
end

ステータス別一覧表示は、こんな感じです。

  def list
    @status = (params[:status] or session[:status] or "all").to_s.intern
    list_param = {
      :per_page => 10,
      :order=>'start desc',
    }
    if @status == :all
      list_param[:conditions] = ['counselor_id = ?',
        current_user.id
      ]
    else
      list_param[:conditions] = ['counselor_id = ? and status_code = ?',
        current_user.id,
        CounselingSession::StatusToStatusCode[@status]
      ]
    end

    @counseling_session_pages, @counseling_sessions = paginate :counseling_sessions, list_param
    session[:status] = @status
  end

新規作成の時は、既存データの最後の次の1時間を初期値として表示するようにしました。

  def new
    @counseling_session = CounselingSession.new
    newest = CounselingSession.find(:first,
                                    :order => " start desc",
                                    :conditions => ['counselor_id = ?',
                                      current_user.id
                                    ])
    if newest
      @counseling_session.start = newest.end
    else
      @counseling_session.start = Time.now
    end
    @counseling_session.end = @counseling_session.start + 60.minutes
  end


セッションとクラアントの関連づけは、まず、クライアントの情報を表示するビューに、以下を追加します。

      <tr>
        <td class="<%= localized_label_class_on(@counseling_session, 'status_code') %>">
           クライアント
        </td>
        <td>
           <%= select("counseling_session", "client_id",
               Client.find_all.collect  {|p| [ p.lastname, p.id ] },
               :include_blank => true) %>
        </td>
      </tr>

これだけで次のようなHTMLが生成され、クライアントの名前を選択するコンボボックスが表示されます。

<select id="counseling_session_client_id" name="counseling_session[client_id]">
  <option value=""></option>
  <option value="2">aaa</option>
  <option value="3">bbb</option>
</select>

value="2"という所は、クライアントのIDです。あとは、これをセッションに保存しておいて、更新で利用します。

  def update
    @counseling_session = CounselingSession.find(params[:id])
    if params[:btn_cancel]
      @counseling_session.attributes = params[:counseling_session]
      render :action => 'edit'
    elsif @counseling_session.update_attributes(params[:counseling_session])
      begin
        client = Client.find(session[:client_id])
        if client
          client.counseling_sessions << @counseling_session
        end
      rescue
        @counseling_session.client = nil
      end
      flash[:notice] = localize(:model, 'counseling_session') + localize(:command, :successfully_updated)
      redirect_to :action => 'show', :id => @counseling_session
    else
      render :action => 'edit'
    end
  end

データのチェックは、モデルの validate というメソッドに実装すると、新規作成の時も修正の時も自動的にそのメソッドが呼ばれて、エラーメッセージの表示等も(jascaffold生成ロジック等が)かなり自動的に処理をしてくれます。

ここは次のような感じでテストから作成しました。

  def test_validate_status
    # ステータスが空きなのに、クライアントが関連づけられていたらおかしい
    assert_raise(ActiveRecord::RecordInvalid) do
      @s.status = :free
      @s.client = @cl1
      @s.save!
    end
  end

実装はこうです。

  def validate
    unless self.counselor_id
      errors.add_to_base("カウンセラーIDが不正です")
    end
    if self.end == nil or self.start >= self.end
      errors.add_to_base("日付が不正です")
    end
    check_double_booking
    errors.add_to_base("不正なステータスです") unless StatusCodeToStatus[status_code]
    case status
    when :free, :not_done
      errors.add_to_base("このステータスではクライアントに関連づけられません") if client_id
    when :done, :reserved
      errors.add_to_base("このステータスではクライアントが必要です") unless client_id
    end
  end

validateにチェックを組み込んでおくと、不正なデータがデータベースに格納できないことは、ほぼ保証できます。それだけでは、エラーがうまく表示されなかったり、アプリケーションエラーになったりしますが、データが壊れなければ、運用しながら少しづつ対応していくこともできます。

scaffoldで枠組みを作って、validate で必要なチェックだけ(テストファーストで)実装して、後は動かしながら考えるという方法は、すごく実用的だと思います。