2010/04/01

accepts_nested_attributes_for : 親モデルのフォームで子モデルも編集する方法

Service モデルが ServiceFile モデルと association で繋がっている場合。。。 ※ Railscasts 参照

models/service.rb

has_many :service_files, :dependent => :destroy
accepts_nested_attributes_for :service_files, 
    :reject_if => lambda { |a| a[:uploaded_data].blank? }, 
    :allow_destroy => true

models/service_files.rb

belongs_to :service

/service/new.html.erb & /service/edit.html.erb

<% form_for([:pro, @service], :url => {:action => :create},:html => {:multipart => true})  do |f| %>
  <!-- 子モデルにアップロードデータがあるので、multipart を指定 -->
  <% f.fields_for :service_files do |builder| %>
    <%= render "service_file_fields", :f => builder %>
  <% end %>
  <p><%= link_to_add_fields "Add", f, :service_files %></p>
<% end %>

_service_file_fields.html.erb

<div class="fields">
 <%= f.file_field :uploaded_data %>
 <%= link_to_remove_fields "<img src='/images/icons/cross.png'/>", f %>
</div>
※ application.js, application_helper.rb のメソッドは Railscasts 参照

はまったこと

undefined method `reflect_on_association' for NilClass:Class
・・・が
問題は<%= link_to_add_fields t('common.add_obj', :obj => t('file.singular')), f, :service_files %> に正しいクラスを渡してなかった。

<% form_for(@service, :url => {:action => :create},:html => {:multipart => true}) do |f| %>

を下に変えたら問題なし。(service は pro のネームスペース下)

< <% form_for([:pro, @service], :url => {:action => :create},:html => {:multipart => true}) do |f| %>

2010/03/18

Reading in the product language table

While loading the list of products on Jzool.com I naturally wanted to load the associated translation which is stored in a different table.

The best way to do this to use eager loading which is a cool thingie you do when you want to read associated tables in one go.

So, in the Product model I do this:

 has_many :product_languages, :dependent => :destroy
 has_one :t, :class_name => 'ProductLanguage',
   :conditions => ['locale = ?', I18n.locale],
   :select => 'id, product_id, name, short'

The "t" stands for "translated". Just wanted to keep it short. I could very well do has_many :ts by the way.

Now, in my paginated search method I use the include.

 def self.search(search, page)
   paginate :include => :t, :per_page => 20, :page => page,
            :conditions => ['name like ?', "%#{search}%"], :order => 'created_at desc'
 end

And then I can just call the translated name by doing:

<%= @product.t.name %>
However, this actually ends up generating two sql statements. Here's an example using two models - Category and CategoryLanguage where the latter holds the localized name of the category.
def localized_children
Category.find :all, :conditions => ['categories.parent_id = ? and categories.display = ?', self.id, true],
   :order => 'categories.sort_id asc',
   :select => 'categories.id, categories.name, categories.parent_id', :include => :t
end
This results in the following two sql statements
[4;35;1mCategory Load (1.0ms) [0m   
[0mSELECT categories.id, categories.name, categories.parent_id FROM `categories` WHERE (categories.parent_id = 307 and categories.display = 1) ORDER BY categories.sort_id asc [0m
[4;36;1mCategoryLanguage Load (1.0ms) [0m    [0;1mSELECT id, category_id, name FROM `category_languages` WHERE (`category_languages`.category_id IN (308,313,480,310,309,311,504,312) AND (locale = 'en'))
So it seems like the best way to go is simple sql.
def localized_children
   Category.find_by_sql(["
     SELECT DISTINCT c.id, c.parent_id, cl.name
     FROM categories AS c JOIN category_languages AS cl ON c.id = cl.category_id
     WHERE c.display = ? AND c.parent_id = ? AND cl.locale = ?
     ORDER BY c.sort_id ASC",
     true, self.id, I18n.locale])
end
Which results in one clean sql
SELECT DISTINCT c.id, c.parent_id, cl.name
FROM categories AS c JOIN category_languages AS cl ON c.id = cl.category_id
WHERE c.display = 1 AND c.parent_id = 307 AND cl.locale = 'en'
ORDER BY c.sort_id ASC
But in the view Use <%= @product.name %> instead of <%= @product.t.name %>

2010/03/10

modx eform の内容を DB に入れる方法(ID 番号採番も)

またまた rails とは無関係ですが、modx の eform を使って、データベースに投稿内容を保存する方法をトライしてみました。一番やりたかったことは、テーブルのレコードIDを取得して、メールに表示させること。

一般的に使われそうな「お問合せフォーム」を例に、簡単なサンプルを作成してみました。

まず、MySQL からテーブルを作成

CREATE TABLE `modx_inquiries` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `name` varchar(255) NOT NULL,
  `subject` varchar(255) NOT NULL,
  `email` varchar(100) NOT NULL,
  `message` text NOT NULL,
  `created_at` int(20),
  PRIMARY KEY  (`id`)
);

複数の異なるフォームがある場合、それぞれテーブルを追加する必要があります。逆に、一つのmodx インスタンスで、いくらでも応用できるということ。
フォームのチャンクを作成 db_inquiries

<p class="error">[+validationmessage+]</p>
<form method="post" action="[~[*id*]~]" id="EmailForm" name="EmailForm" >
 <fieldset>
  <input name="formid" type="hidden" value="ContactForm" />
  <table>
  <tr>
  <th width="30%">お名前<em>*</em></th>
  <td><input name="name" id="cfName" class="text short" type="text" eform="お名前::1:" /></td>
  </tr>
  <tr>
  <th>メールアドレス<em>*</em></th>
  <td><input name="email" id="cfEmail" class="text long" type="text" eform="メールアドレス:email:1" /></td>
  </tr>
  <tr>
  <th>表題<em>*</em></th>
  <td>
   <select name="subject" id="cfRegarding" eform="Form Subject::1">
    <option value="一般のお問合せ">一般のお問合せ</option>
    <option value="その他">その他</option>
   </select>
  </td>
  </tr>
  <tr>
  <th>内容<em>*</em></th>
  <td><textarea name="message" id="cfMessage" rows="4" cols="20" eform="内容:textarea:1"></textarea></td>
  </tr>
  <tr>
  <th></th>
  <td><em>*</em>は必須項目です。</td>
  </tr>
  <tr>
  <th></th>
  <td><input type="submit" name="contact" id="cfContact" class="button" value="メッセージを送る" /></td>
                </tr>
                <tr>
                <td colspan="2">ご登録される個人情報はお問い合わせへのご返答及びそれに付随する内容に関してのみ、利用させていただきます。詳細は<a href="[~133~]">プライバシーポリシー</a>をご覧ください。</td>
                </tr>
                </table>
 </fieldset>
</form>
snippet dbInquiries の作成 (function 名は何でもOK)
<?php
function dbInquiries(&$fields)
 {
  global $modx;
  // Init our array
  $dbTable = array();
  $dbTable['name'] = $modx->db->escape($fields['name']);
  $dbTable['subject'] = $modx->db->escape($fields['subject']);
  $dbTable['email'] = $modx->db->escape($fields['email']);
  $dbTable['message'] = $modx->db->escape($fields['message']);
  $dbTable['created_at'] = time();

         // Run the db insert query
  $dbQuery = $modx->db->insert($dbTable, 'modx_inquiries' );
                $fields['record_id'] = $dbQuery; // so that eform can access this field
                $fields['subject'] = $dbTable['subject']." (".$fields['record_id'].")";
  return true;
 }
?>

$fields['record_id'] = $dbQuery; は、eform が新しく生成されたレコードの ID を取得を可能にするための追加。
$fields['subject'] = $dbTable['subject']." (".$fields['record_id'].")"; はメールの表題に subject とレコード ID を追加する。

問い合わせフォームでの eform コール

[!dbInquiries!]
[!eForm? &subject=`上書きされます` &to=`[(emailsender)]` &formid=`ContactForm` &eFormOnBeforeMailSent=`dbInquiries` &tpl=`db_inquiries` &thankyou=`3` &report=`db_inquiries_report` &automessage=`db_inquiries_thanks` !]
こちらのスレッドのまとめ

2010/03/01

Rails でSSLを使用する

英語のチュートリアルですが、かなりグッド。
ウィンドウズのローカルマシーンで SslRequirement プラグインをインストール
ruby script/plugin install ssl_requirement

application_controller.rb
include SslRequirement

# Below to disable ssl unless system is production
# 下は、本番環境のみ ssl を使う場合。
protected
def ssl_required? 
  ((self.class.read_inheritable_attribute(:ssl_required_actions) || []).include?(action_name.to_sym)) && RAILS_ENV == 'production' 
end  
コントローラーで、ssl を使うアクションを指定
ssl_required  :index, :show, :edit

正しく指定していないページにSSLでアクセスしようとすると次のようなエラーが。。。
Redirected to http://cafetalk.com/en/user/reqs/confirm_req
Filter chain halted as [:ensure_proper_protocol] rendered_or_redirected.
こちらにベターなバージョンあり。 /2010/04/namespace-applicationcontroller.html
上の例だと、application_controller で ssl_required :all ができない

2010/02/25

rjs でflash を書き換える

flash[:notice] = t('request.confirm_success')
page.replace_html :notice, flash[:notice]
flash.discard

2010/02/18

calendar_date_select plugin doesn't accept minute intervals

calendar_date_select で、分のインターバルを00、30等に設定していても、最初に設定されるデフォルト分数が現在時刻の分の値をとってきてしまう問題の解消法
if (isNaN(this.date)) this.date = new Date();
を、以下に変更
if (isNaN(this.date)) {
   this.date = new Date();
   this.date.setMinutes(Math.ceil(this.date.getMinutes() / this.options.get("minute_interval")) * this.options.get("minute_interval"));
}