zed.0xff.me

Заметки о линуксе, Ruby, Rails & so on.

rails3: link_to + image_tag

было: (rails 2.x)

1
2
3
<%= link_to image_tag('rainbow.png'), '/' -%>

<%= link_to "#{image_tag('rainbow.png')}Главная", '/' -%>

стало: (rails 3.x)

1
2
3
<%= link_to image_tag('rainbow.png'), '/' -%>

<%= link_to "#{image_tag('rainbow.png')}Главная".html_safe, '/' -%>

Первый вариант (просто image_tag) не изменился, а вот второй вариант (image_tag внутри строки) теперь требует явного указания html_safe.

rails3: link_to_function

1
ActionView::Template::Error (undefined method `link_to_function' for #<Class>)

теперь link_to_function находится в плагине prototype_legacy_helper:

1
./script/rails plugin install git://github.com/rails/prototype_legacy_helper.git

возможно, ребята придумали чем-то заменить, а потом и задепрекейтить, но никаких постов на эту тему я в нете не нашел.

Rails 3.x "Crazy Loading" is awesome!

1
2
3
4
5
6
7
8
red_items = Item.where(:colour => 'red')
red_items.find(1)
item = red_items.new
item.colour #=> 'red'

red_items.exists? #=> true
red_items.update_all :colour => 'black'
red_items.exists? #=> false

// actually it’s “Lazy Loading” and stuff. Read more: Active Record Query Interface 3.0.

Rails streaming VS mongrel, thin, ebb and Passenger

С некоторых пор моим любимым веб-сервером для руби приложений является thin.
Но сегодня он меня конкретно разочаровал. Как, впрочем и mongrel.

Как нетрудно догадаться из заголовка, дело касается streaming
(про стриминг в рельсах читать тут, начиная с Streaming data and/or controlling the page generation)
(и почему они не автогенерят id для хидеров?? можно было бы ссылку сразу куда надо поставить..)

пример там приведен такой:

1
2
3
4
5
6
7
  # Streams about 180 MB of generated data to the browser.
  render :text => proc { |response, output|
    10_000_000.times do |i|
      output.write("This is line #{i}\n")
      output.flush
    end
  }

так вот, что thin, что mongrel, оба тупо забивают на этот стриминг, и пытаются всосать в себя всё что им рельсы отдают, а потом выплюнуть юзеру единым куском..
thin, например, делает так:

1
2
terminate called after throwing an instance of 'std::runtime_error'
  what():  no allocation for outbound data

а mongrel так:

1
2
Error calling Dispatcher.dispatch #<NoMemoryError: failed to allocate memory>
/usr/lib64/ruby/gems/1.8/gems/actionpack-2.3.5/lib/action_controller/cgi_process.rb:58:in `write'

Еще хотел быстренько попробовать ebb, но не обнаружил в дистрибутиве внятных инструкций по его установке и настройке.. ладно, запустил через Ebb.start_server("/path/to/rails/app"), но толку от этого оказалось мало – на порту он поднялся, но ни на один запрос отвечать не захотел.. тупо висел и думал о чем-то там своем..

And the winner is…

Passenger. Хоть я его раньше и не использовал, и довольно таки скептически к нему относился, но он успешно зарекомендовал себя в продакшене, легко поставился (потянув, естественно за собой апача, которого я тоже недолюбливаю..) (хмм.. хотя он есть и для nginx, это несколько меняет дело, на досуге поковыряю)

Так вот, Passenger легко отдал псевдофайлик размером 2 гига с локалхоста на локалхост со скоростью около 12 мегабайт в секунду. При этом не показав никакого значительного увеличения потребления памяти!

PS:

  • Linux zz 2.6.31-gentoo-r6-zz #1 SMP PREEMPT Tue Dec 22 01:38:46 YEKT 2009 x86_64 Intel(R) Core(TM)2 Duo CPU E8400 @ 3.00GHz GenuineIntel GNU/Linux
  • Rails 2.3.5
  • mongrel 1.1.5
  • thin 1.2.5
  • passenger 2.2.9

Phuby on Phails: PHP и Ruby в одном флаконе! жесть!

код и видео
видео в на порядок выразительнее кода :)

// thanks to Rail0rz

uninitialized constant Test::Unit::TestResult::TestResultFailureSupport

после очередного апдейта гемов возникла сабжевая бага.
лечится просто и эоегантно :)

1
sudo gem uninstall test-unit

как сказано вот тут, проблема связана с постепенным внедрением ruby 1.9, а для 1.8.x пока этот гем в принципе нужен не особо.

новый gem: Яндекс.Метрика

Установка

Добавьте в config/environment.rb:

1
    config.gem "yandex_metrika", :lib => "yandex/metrika", :source => "http://gemcutter.org"

и выполните команду:

1
    rake gems:install

Описание

Быстрая интеграция Яндекс.Метрики в ваше Rails-приложение.

По умолчанию код метрики автоматически вставляется в каждую страницу перед
закрывающим тэгом </body>.
Но сначала нужно корректно сконфигурировать плагин, иначе он будет ругаться.

Конфигурация

Для этого добавьте следующий код в config/environment.rb:

1
2
3
    if defined?Yandex::Metrika
        Yandex::Metrika.counter_id = '123456'
    end

А для избежания замусоривания environment.rb всякими плагинами -
можно добавить этот конфиг в config/initializers/yandex_metrika.rb

Вместо ‘123456’ нужно вставить ваш личный COUNTER_ID, который можно вытащить
из javascript-кода, предоставляемого Яндексом: “new Ya.Metrika(123456)”,
тут 123456 и есть искомый код.

По умолчанию код метрики вставляется в страницы только при использовании
production окружения. Для активации кода и в development нужно сделать так:

1
    Yandex::Metrika.environments = %w'production development'

Если есть необходимость для каких-то страниц выключить код Яндекс.Метрики – то
добавть следующий код в соответствующий класс контроллера:

1
    skip_after_filter :add_yandex_metrika_code

discovered the bug in Rails sqlite3 adapter

example migrations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class M1 < ActiveRecord::Migration
  def self.up
    create_table 'users' do |t|
      t.string 'name'
      t.decimal 'v1', :precision => 10, :scale => 3
    end
  end

  def self.down
    drop_table 'users'
  end
end

class M2 < ActiveRecord::Migration
  def self.up
    change_column :users, :v1, :decimal, :precision => 12, :scale => 5
  end

  def self.down
  end
end

expected resulting schema:

1
2
3
4
5
6
7
8
9
10
11
12
13
ActiveRecord::Schema.define(:version => 20090706090649) do
  create_table "users", :force => true do |t|
    t.string  "name"
    t.decimal "v1",   :precision => 12, :scale => 5
  end
end</code></pre>

*actual resulting schema:*<pre><code>ActiveRecord::Schema.define(:version => 20090706090649) do
  create_table "users", :force => true do |t|
    t.string  "name"
    t.decimal "v1"
  end
end

PATCH:

1
2
3
4
5
6
7
8
9
10
11
12
diff -ru activerecord-2.3.2-orig/lib/active_record/connection_adapters/sqlite_adapter.rb activerecord-2.3.2/lib/active_record/connection_adapters/sqlite_adapter.rb
--- activerecord-2.3.2-orig/lib/active_record/connection_adapters/sqlite_adapter.rb 2009-07-06 15:43:43.000000000 +0600
+++ activerecord-2.3.2/lib/active_record/connection_adapters/sqlite_adapter.rb 2009-07-06 15:20:07.000000000 +0600
@@ -285,6 +285,8 @@
self.limit = options[:limit] if options.include?(:limit) self.default = options[:default] if include_default self.null = options[:null] if options.include?(:null)
+ self.precision = options[:precision] if options.include?(:precision)
+ self.scale = options[:scale] if options.include?(:scale)
end end end

submitted to Rails’ lighthouse

Особенности кэширования классов в Rails

Задача

Вот есть например у нас некий самописный класс для взаимодействия со сторонним API:

1
2
3
4
5
6
7
class SomeAPIClient
  def initialize params = {}
    @host  = params[:host]  || 'some.api.host'
    @port  = params[:port]  || 12345
  end
...
end

И кладем мы его в #{RAILS_ROOT}/lib/, чтобы его можно было легко юзать из классов рельсового приложения.

Дальше мы задумываемся о том, что из нашего Rails-приложения, мы будем всегда обращаться к одним и тем же хосту и порту апи-сервера. И надо где-то их вконфигурить. А приложение-то у нас opensource, и хостится на github-e, поэтому хост и порт (и возможно какие-то другие параметры типа api-key) у нас для разных юзеров приложения могут быть разными.

Отсюда вопрос: Как бы это сконфигурить так, чтобы и либу не особо корежить, и чтоб наиболее Rails-way было?..

Мне вот лично пришла в голову следующая мысль:

1
2
3
4
5
6
7
8
9
class SomeAPIClient
  cattr_accessor :default_port, :default_host

  def initialize params = {}
    @host  = params[:host] || default_host || 'some.api.host'
    @port  = params[:port] || default_port || 12345
  end
...
end

Проблема

Всё вроде бы красиво, свои значения для default_host & default_port мы назначаем в #{RAILS_ROOT}/config/initializers/my_api_client.rb, который исключаем из git-репозитария путём помещения в .gitignore.

Но! Но не зря в топике написано про кэширование классов.. в development окружении первый запрос к API выполняется отлично, а вот уже на втором и всех следующих – вступает в действие config.cache_classes = false, рельсы перечитывают декларацию нашего апи, при этом удаляя старый класс из памяти через remove_const. Ну а initializers естественно не перечитывают.

В production всё ОК, потому что там обычно config.cache_classes = true.

Решение

Копаясь во внутренностях Rails обнаружил вот это:
( actionpack-2.3.2/lib/action_controller/dispatcher.rb )

1
2
3
4
5
6
7
8
9
10
11
      # Add a preparation callback. Preparation callbacks are run before every
      # request in development mode, and before the first request in production
      # mode.
      #
      # An optional identifier may be supplied for the callback. If provided,
      # to_prepare may be called again with the same identifier to replace the
      # existing callback. Passing an identifier is a suggested practice if the
      # code adding a preparation block may be reloaded.

      def to_prepare(identifier = nil, &block)
...

Таким образом, чтобы решение стало работоспособным надо обернуть нашу инициализацию в do_prepare:
( #{RAILS_ROOT}/config/initializers/my_api_client.rb )

1
2
3
4
ActionController::Dispatcher.to_prepare(:my_api) do
  SomeAPIClient.default_host       = 'my.api.host'
  SomeAPIClient.default_port       = '23456'
end

И в development mode этот код будет срабатывать перед обработкой каждого запроса, а в production – только перед обработкой самого первого запроса.

Done.

rspec views & params

Пытаюсь тестить спеку вьюшки(view). Причем внутри нее такая логика:

1
<%= verbose_search_results(user) if params[:verbose] %>

И я не нашел более вменяемого способа передать в нее params, чем следующий:

1
2
3
4
it "should do what I said"
  @controller.stub!(:params).and_return({:verbose => true})
  render 'admins/users/list'
end

И тут есть белая сторона медали, а есть черная.

Белая – при этом стандартные параметры 'action' и 'controller' также нормально передаются, т.е. мой and_return действует недеструктивно.

Черная – params, переданный внутри такой спеки, становится невосприимчив к ключам-символам!

т.е. if params[:verbose] работать НЕ будет (хотя на живых рельсах работает прекрасно).

А вот if params['verbose'] будет работать и там и там.

PS: может быть, конечно, есть более вменяемый способ передать params, но пока я его не обнаружил.

UPD: как оказалось rspec инициализирует параметры 'action' и 'controller' уже на готовом хэше params, т.е. чтобы всё работало также как и на живых рельсах, надо делать так:

1
2
3
4
it "should do what I said"
  @controller.stub!(:params).and_return(HashWithIndifferentAccess.new({:verbose => true}))
  render 'admins/users/list'
end

т.е. явно инициализировать params как HashWithIndifferentAccess, а не просто хэш.

IMHO как-то это всё-таки кривовато, и должен быть более прямой и очевидный способ..