Railsで、どんなモデルに対してもメモを取れるようにする


今回は営業の人が顧客だったり、商品だったり、会社だったりのモデルに対して共有できるメモを取るというニーズを想定し、SalesNoteというモデル名を使うことにする。

## モデル生成

どのモデルにでも紐付けられるようにPolymorphic Associationで作る。

```bash:bash
rails g model SalesNote target_id:integer target_type:string content:text
```

して、生成したMigrationファイルにインデックスを張る命令だけ付け加える。

```ruby:
add_index :sales_notes, [:target_id, :target_type]
```

結果、以下のMigrationファイルが出来上がる。

```ruby:db/migrate/201301234543_create_sales_notes.rb
class CreateSalesNotes < ActiveRecord::Migration
  def change
    create_table :sales_notes do |t|
      t.integer :target_id
      t.string :target_type
      t.text :content

      t.timestamps
    end
    add_index :sales_notes, [:target_id, :target_type]
  end
end
```

## モデル

モデルは自動で生成されるはずだが、ここではvalidationだけ軽くつけておくことにする。

```
class SalesNote < ActiveRecord::Base
  belongs_to :target, polymorphic: true, inverse_of: :notes
  attr_accessible :content
  validates_presence_of :target_id, :target_type
end
```

## Noteをつける側のモデル

```ruby:app/models/concerns/sales_note_takable_concern.rb
module SalesNoteTakableConcern
  extend ActiveSupport::Concern
  included do
    has_one :sales_note, as: :target, inverse_of: :target
  end
end
```

を作って、

```ruby:app/models/user.rb
class User
  include SalesNoteTakableConcern
end
```

というように使う。

## ルート

```config/routes.rb
namespace :admin do
  resources :sales_notes, only: [:create, :update]
end
```


## コントローラー

```ruby:app/cntrollers/admin/sales_notes_controller.rb
class Admin::SalesNotesController < Admin::BaseController
  before_filter :find_note  # before_action

  def create
    create_or_update
  end

  def update
    create_or_update
  end

  private
  def find_note
    @sales_note = SalesNote.where(target_id: params[:sales_note][:target_id], target_type: params[:sales_note][:target_type]).first_or_initialize
  end

  def create_or_update
    params[:sales_note].delete(:target_id)
    params[:sales_note].delete(:target_type)
    respond_to do |format|
      if @sales_note.update_attributes(params[:sales_note])
        format.html { redirect_to :back, notice: "ノートが更新されました" }
        format.js { head :ok }
      else
        format.html { redirect_to :back, alert: "ノートの更新に失敗しました" }
        format.js { head :not_acceptable }  # 406
      end
    end
  end
end
```


## ビュー

一般ユーザーには見えないようにAdmin用のViewで行う。
`Admin::BaseController`にUserがAdminかの判定がある想定。

```haml:app/views/admin/base/_sales_note_form.html.haml
.sales_memo
  %h3 メモ
  - sales_note = target_model.sales_note || target_model.build_sales_note
  = form_for [:admin, sales_note], remote: true, html: { id: "sales_note_form" } do |f|
    = f.hidden_field :target_id
    = f.hidden_field :target_type
    = f.text_area :content, style: "height: 60px; width: 290px;"
    = f.submit 'save', id: 'sales_note_save', class: "btn blue", style: "width: 100px; height: 30px; padding: 0", disabled: :disabled
    %span#sales_note_save_notification{ style: "margin: 0 10px; font-size: 20px; display: hidden; color: red;"} saved!

:javascript
  var memoContent = $('#sales_note_content').val();
  $('#sales_note_content').on('input propertychange', function() {
    if ($(this).val() == memoContent) $('#sales_note_save').attr('disabled', 'disabled');
    else $('#sales_note_save').attr('disabled', null);
  });
  $('#sales_note_form').on('ajax:success', function(event, data, status, xhr) {
    memoContent = $('#sales_note_content').val();
    $('#sales_note_save').attr('disabled', 'disabled');
    $('#sales_note_save_notification').show('slow').delay(1000).hide('fast');
  });
  
  $(function(){
    $('#sales_note_content').autosize();
  });
```

とPartialViewを作っておいて、

```ruby:app/views/admin/user/show.html.haml
= render 'sales_note_form', target_model: @user
```

や

```ruby:app/views/user/show.html.haml
= render 'admin/base/sales_note_form', target_model: @user if current_user.admin?
```

の用に使う。

Javascriptの部分は、メモが編集されている時のみ、ボタンがEnableされて保存することができるようにしているコードが大半。ココらへんは最悪必要ないし、適当でいいと思う。
テキストは適宜大きくなるように、jqueryのautosizeをつけている。(http://www.jacklmoore.com/autosize/)
autosizeはもともと有名だったAutoResizeがメンテされなくなって使われるようになった。AutoResizeよりちょっと英語が変だけど気にしない。