Generic-user-small Fora User 0 posts

So you’ve been reading Rails Recipes and you’ve got the bug to share your own tips and tricks?

Write your own recipes for the world to benefit from.

What is a Rails Recipe? It’s got to be short and to the point. It’s got to show someone, step by step, how to do something they might not know how to do. It should answer the question, “How do I….?” or “How would I…?”

It should follow this basic structure:
  • Problem
  • Extra Ingredients
  • Credits (anyone deserve credit for this idea?)
  • Solution
  • Discussion
  • Further Reading

Fame and fortune await. Let’s see what you’ve got!

Who knows? You may even end up (with permission) in the book!

 
Generic-user-small Fora User 0 posts

I’d like to see a recipe where you can send a complete HTML message including all graphic bells-n-whistles. My need didn’t go that far yet. The only thing I needed I blogged at the Caboo.se about this. Maybe you or someone else can expand that?

 
Generic-user-small Fora User 0 posts

Problem

Your the envy of the IT shop for getting such a cool web app up in record time using Rails. But some critics still roll their eyes and say “another trend”. They’re waiting to poke holes in such a new technology.

(Corallary:) Even though it says clearly in database.yml …

Do not set this db to the same as development or production.

...you’ve gone and done it. More than once. And Murphy’s Law says that someone will do it in production one day as well. You need a backup and recovery strategy.

Credit

Geoffrey Grosenbach has a great plug-in called ar_fixtures that inspired these scripts. Also much credit to Jim Weirich not just for creating rake, but also actively helping newcomers online. I’ve gotten many tips from his posts .

Ingredients

Make sure that zip and unzip commands are in the PATH of your OS.

Solution

Rake already has two built in tasks to handle restoring the database, :db_schema_dump, and :db_schema_import. By adding code to export and import your data to and from YAML, and code to zip it up along with any files created at runtime, you’ve got yourself a Six Sigma Sabanes-Oxley buzzword-compliant y2k disaster recovery plan.

backup.rake

The backup script needs some basic file utilities, and the packagetask.
require 'fileutils'
require 'rake/packagetask'
First we’ll get all the model names that need backing up. Omitting anything with an underscore eliminates _mailer or _observer files.
MODELS = FileList['app/models/*'].exclude(/.*_.*/)

Then we loop through all the models and dump them to YAML. (See Recipie #23)

task :backup_app_data => [:db_schema_dump] do
  MODELS.each do |m|
    model = File.basename(m,'.rb')
    dump_to_yaml(nil,model)
  end
end

Now let’s zip it all up. Amazingly enough the PackageTask can create a zip file instead of a gem just by setting the need_zip switch. The FileList can be customized to include any additional data that may be created by your app while running.

Rake::PackageTask.new("#{THISAPP}","0.0.1") do |z|
  # customize this list to include any files created by you app at runtime. 
  z.package_files = FileList["db/*"] #,"public/ad/**/*"]
  z.need_zip = true
end  

There. Now you can report to management that there’s a backup strategy in place. There will be much rejoicing. Unless of course disaster strikes. Then you’ll realize you need a recovery script as well.

restore.rake

For recovery we’re rebuilding the app from scratch. We’re assuming that the correct version of your app can be retrieved from source control. Once the code is in place, we can unzip our data package.

Unzipping a .zip is a little trickier. You need the zipfilesystem library. It’s documented at http://rubyzip.sourceforge.net/classes/ .

require 'zip/zipfilesystem'

After opening the .zip, you need to extract each file by name. Notice the last line of this task. We’re copying everything we zipped up to the RAILS_ROOT.

task :unzip_app do
  FileUtils.rm_rf(THISAPP_ZIP.sub('.zip',''))
  Zip::ZipFile.open(THISAPP_ZIP, Zip::ZipFile::CREATE) {   |zipfile|
    Zip::ZipFile.foreach(THISAPP_ZIP) { |f|
     if f.file?
       mkdirs RAILS_ROOT+"/#{BACK}/"+f.parent_as_string if f.parent_as_string
       zipfile.extract(f,BACK+'/'+f.name)
     end
    }
  }  
  FileUtils.cp_r(Dir.glob("#{THISAPP_ZIP.sub('.zip','')}/."),RAILS_ROOT)
end 
Now it’s time to import our data. Once again the comments warn about the destructive nature of this task. If your schema already exists, you can change the dependency of restore_app to ’:environment’.
# CAUTION!!! executing ':db_schema_import' blows away existing tables and data
# This script is meant only for a recovery from scratch not for migration.
task :restore_app =>  [:unzip_app, :db_schema_import] do
  MODELS.each do |m|
    model = File.basename(m,'.rb')
    # Since some models contain validation that fails on import a 
    # new blank model is generated just for the sake of the import.
    make_temp_model(m)
    load_from_yaml("db/#{model.downcase.pluralize}.yml",'test',model)
 # Alternatively, use this line with the ar_fixtures plugin.
 #  eval("#{model.camelize}.load_from_file") if File.exists?("db/#{model.downcase.pluralize}.yml")
    restore_model(model)
  end
end

Discussion

Notice the two methods make_temp_model and restore_model. This is a brute force solution to an interesting problem. load_from_yaml creates instances of classes on the fly, but often validation in the model objects fails. The most common being validates_presence_of another class.

There’s no need to enforce all the business rules for creating objects here because we know our backup data came from an identical application. So we simply rely on ruby’s power and back the models up , create empty models on the fly, and then put the old one’s back. A lot of work in some languages, but just a few lines of code for us Ruby enthusiasts.

def make_temp_model(file_name)
  FileUtils.mv(file_name,file_name+'.bak')    
  Dir.chdir(RAILS_ROOT) 
  system("ruby script/generate model #{File.basename(file_name,'.rb')} -s >>mod.txt")
end

def restore_model(model)
  file_name = RAILS_ROOT+'/app/models/'+"#{model}.rb" 
  FileUtils.mv(file_name+'.bak',file_name) if File.exists?(file_name+'.bak')
end

Lastly here’s the code from the ar_fixtures plugin, modified slightly to work here.

# code from http://wiki.rubyonrails.org/rails/pages/AR+Fixtures+Plugin
def load_from_yaml(yaml_file,app,obj,save_id = true)
  if File.exists?(yaml_file) 
    records = YAML::load( File.open(yaml_file))  
    records.each do |record|
      new_obj = nil;
      eval ("new_obj = #{obj.camelize}.new(record.attributes)")
      new_obj.id = record.id if save_id
      # For Single Table Inheritance
      klass_col = record.class.inheritance_column.to_sym
      if record[klass_col]
         new_obj.type = record[klass_col]
      end
      new_obj.save
    end
  end
end
 
Generic-user-small Fora User 0 posts

Using legacy databases

Problem

Your company is running some software that rely on an existing MySQL database.

Would you like to use Rails at work, your only solution is to teach Rails the convention used in this legacy database.

These conventions are basically:

  • table names are singular, prefixed by my_ and suffixed by _table
  • the primary key, although named id, is not auto-incremented but rely on sequences: a table foo will have a table my_foo_sequence_table associated.

Solution

Although you can use the set_table_name to indicate for each of your Foo model to look at table my_foo_table, it’s not very … DRY compliant.

We’re going to use the class methods table_name_prefix, table_name_suffix and pluralize_table_names to automate things a little bit.

So let’s create a file, named adapter.rb in the lib directory of your rails application. This file will hold the following:


    ActiveRecord::Base.table_name_prefix = "my_" 
    ActiveRecord::Base.table_name_suffix = "_table" 
    ActiveRecord::Base.pluralize_table_names = false

With this all the computed table names will begin with my_ and ends with _table with the middle name not pluralized. So far, so good.

Just adds a


  require 'lib/adapter.rb'

to boot/environment.rb file.

Now let’s fix this sequence problem. Rails by default assumes that your primary key is auto-incremented, which is one of th eMySQL property. However in some case this auto-increment is not used and simulated through the use of sequence tables. A sequence table just acts as a counter that allows to predict the next integer to use as a primary key.

So we want our sequence names to be deduced from our table name. Let’s rewrite the reset_sequence method of ActiveRecord::Base: this function is used to set the name of the sequence table the first time this name is needed.


   module ActiveRecord
     class Base
       class << self
         def reset_sequence_name
           "#{table_name}_sequence" 
         end
       end
     end
   end

We now have to instruct the MySQL adapter that we’re going to rely on sequences: we need to request a prefetch of the primary key:


    module ActiveRecord
      module ConnectionAdapters
        class MysqlAdapter
          def prefetch_primary_key?(table_name = nil)
            true
          end
        end
      end
    end

The documentation for prefetch_primary_key? says that if its set to true, then the next_sequence_value function will be called to get a new value so it seems we need this one too:


   module ActiveRecord
     module ConnectionAdapters
       class MysqlAdapter

         def prefetch_primary_key?(table_name = nil)
           true
         end

         def next_sequence_value(sequence_name)
           sql  = "UPDATE #{ sequence_name} SET Id=LAST_INSERT_ID(Id+1);" 
           update(sql, "#{sequence_name} Update")
           select_value("SELECT Id from #{ sequence_name}",'Id')
         end

     end
   end

Were basically telling to mySQL to get a new value based on the last one, using the LAST_INSERT_ID MySQL function..

And we’re done :)

 
Generic-user-small Fora User 0 posts

Problem

Your company is running some software that rely on an existing MySQL database.

Would you like to use Rails at work, your only solution is to teach Rails the convention used in this legacy database.

These conventions are basically:

  • table names are singular, prefixed by my_ and suffixed by _table
  • the primary key, although named id, is not auto-incremented but rely on sequences: a table foo will have a table my_foo_sequence_table associated.

Solution

Although you can use the set_table_name to indicate for each of your Foo model to look at table my_foo_table, it’s not very … DRY compliant.

We’re going to use the class methods table_name_prefix, table_name_suffix and pluralize_table_names to automate things a little bit.

So let’s create a file, named adapter.rb in the lib directory of your rails application. This file will hold the following:


    ActiveRecord::Base.table_name_prefix = "my_" 
    ActiveRecord::Base.table_name_suffix = "_table" 
    ActiveRecord::Base.pluralize_table_names = false
   

With this all the computed table names will begin with my_ and ends with _table with the middle name not pluralized. So far, so good.

Just adds a


   require 'lib/adapter.rb'
   

to boot/environment.rb file.

Now let’s fix this sequence problem. Rails by default assumes that your primary key is auto-incremented, which is one of th eMySQL property. However in some case this auto-increment is not used and simulated through the use of sequence tables. A sequence table just acts as a counter that allows to predict the next integer to use as a primary key.

So we want our sequence names to be deduced from our table name. Let’s rewrite the reset_sequence method of ActiveRecord::Base: this function is used to set the name of the sequence table the first time this name is needed.


   module ActiveRecord
     class Base
       class << self
         def reset_sequence_name
           "#{table_name}_sequence" 
         end
       end
     end
   end
   

We now have to instruct the MySQL adapter that we’re going to rely on sequences: we need to request a prefetch of the primary key:


    module ActiveRecord
      module ConnectionAdapters
        class MysqlAdapter
          def prefetch_primary_key?(table_name = nil)
            true
          end
        end
      end
    end
   

The documentation for prefetch_primary_key? says that if its set to true, then the next_sequence_value function will be called to get a new value so it seems we need this one too:


   module ActiveRecord
     module ConnectionAdapters
       class MysqlAdapter

         def prefetch_primary_key?(table_name = nil)
           true
         end

         def next_sequence_value(sequence_name)
           sql  = "UPDATE #{ sequence_name} SET Id=LAST_INSERT_ID(Id+1);" 
           update(sql, "#{sequence_name} Update")
           select_value("SELECT Id from #{ sequence_name}",'Id')
         end

     end
   end
   

Were basically telling to mySQL to get a new value based on the last one, using the LAST_INSERT_ID MySQL function..

And we’re done :)

 
Generic-user-small Fora User 0 posts

Problem

Your the envy of the IT shop for getting such a cool web app up in record time using Rails. But some critics still roll their eyes and say “another trend”. They’re waiting to poke holes in such a new technology.

(Corallary:) Even though it says clearly in database.yml …

Do not set this db to the same as development or production.

...you’ve gone and done it. More than once. And Murphy’s Law says that someone will do it in production one day as well. You need a backup and recovery strategy.

Credit

Geoffrey Grosenbach has a great plug-in called ar_fixtures that inspired these scripts. Also much credit to Jim Weirich not just for creating rake, but also actively helping newcomers online. I’ve gotten many tips from his posts .

Ingredients

Make sure that zip and unzip commands are in the PATH of your OS.

Solution

Rake already has two built in tasks to handle restoring the database, :db_schema_dump, and :db_schema_import. By adding code to export and import your data to and from YAML, and code to zip it up along with any files created at runtime, you’ve got yourself a Six Sigma Sabanes-Oxley buzzword-compliant y2k disaster recovery plan.

backup.rake

The backup script needs some basic file utilities, and the packagetask.
require 'fileutils'
require 'rake/packagetask'
First we’ll get all the model names that need backing up. Omitting anything with an underscore eliminates _mailer or _observer files.
MODELS = FileList['app/models/*'].exclude(/.*_.*/)
Then we loop through all the models and dump them to YAML. (See Recipie #23)
task :backup_app_data => [:db_schema_dump] do
  MODELS.each do |m|
    model = File.basename(m,'.rb')
    dump_to_yaml(nil,model)
  end
end
Now let’s zip it all up. Amazingly enough the PackageTask can create a zip file instead of a gem just by setting the need_zip switch. The FileList can be customized to include any additional data that may be created by your app while running.
Rake::PackageTask.new("#{THISAPP}","0.0.1") do |z|
  # customize this list to include any files created by you app at runtime. 
  z.package_files = FileList["db/*"] #,"public/ad/**/*"]
  z.need_zip = true
end  
There. Now you can report to management that there’s a backup strategy in place. There will be much rejoicing. Unless of course disaster strikes. Then you’ll realize you need a recovery script as well.

restore.rake

For recovery we’re rebuilding the app from scratch. We’re assuming that the correct version of your app can be retrieved from source control. Once the code is in place, we can unzip our data package.

Unzipping a .zip is a little trickier. You need the zipfilesystem library. It’s documented at http://rubyzip.sourceforge.net/classes/ .
require 'zip/zipfilesystem'
After opening the .zip, you need to extract each file by name. Notice the last line of this task. We’re copying everything we zipped up to the RAILS_ROOT.
task :unzip_app do
  FileUtils.rm_rf(THISAPP_ZIP.sub('.zip',''))
  Zip::ZipFile.open(THISAPP_ZIP, Zip::ZipFile::CREATE) {   |zipfile|
    Zip::ZipFile.foreach(THISAPP_ZIP) { |f|
     if f.file?
       mkdirs RAILS_ROOT+"/#{BACK}/"+f.parent_as_string if f.parent_as_string
       zipfile.extract(f,BACK+'/'+f.name)
     end
    }
  }  
  FileUtils.cp_r(Dir.glob("#{THISAPP_ZIP.sub('.zip','')}/."),RAILS_ROOT)
end 
Now it’s time to import our data. Once again the comments warn about the destructive nature of this task. If your schema already exists, you can change the dependency of restore_app to ’:environment’.
# CAUTION!!! executing ':db_schema_import' blows away existing tables and data
# This script is meant only for a recovery from scratch not for migration.
task :restore_app =>  [:unzip_app, :db_schema_import] do
  MODELS.each do |m|
    model = File.basename(m,'.rb')
    # Since some models contain validation that fails on import a 
    # new blank model is generated just for the sake of the import.
    make_temp_model(m)
    load_from_yaml("db/#{model.downcase.pluralize}.yml",'test',model)
 # Alternatively, use this line with the ar_fixtures plugin.
 #  eval("#{model.camelize}.load_from_file") if File.exists?("db/#{model.downcase.pluralize}.yml")
    restore_model(model)
  end
end

Discussion

Notice the two methods make_temp_model and restore_model. This is a brute force solution to an interesting problem. load_from_yaml creates instances of classes on the fly, but often validation in the model objects fails. The most common being validates_presence_of another class.

There’s no need to enforce all the business rules for creating objects here because we know our backup data came from an identical application. So we simply rely on ruby’s power and back the models up , create empty models on the fly, and then put the old one’s back. A lot of work in some languages, but just a few lines of code for us Ruby enthusiasts.
def make_temp_model(file_name)
  FileUtils.mv(file_name,file_name+'.bak')    
  Dir.chdir(RAILS_ROOT) 
  system("ruby script/generate model #{File.basename(file_name,'.rb')} -s >>mod.txt")
end

def restore_model(model)
  file_name = RAILS_ROOT+'/app/models/'+"#{model}.rb" 
  FileUtils.mv(file_name+'.bak',file_name) if File.exists?(file_name+'.bak')
end
Lastly here’s the code from the ar_fixtures plugin, modified slightly to work here.
# code from http://wiki.rubyonrails.org/rails/pages/AR+Fixtures+Plugin
def load_from_yaml(yaml_file,app,obj,save_id = true)
  if File.exists?(yaml_file) 
    records = YAML::load( File.open(yaml_file))  
    records.each do |record|
      new_obj = nil;
      eval ("new_obj = #{obj.camelize}.new(record.attributes)")
      new_obj.id = record.id if save_id
      # For Single Table Inheritance
      klass_col = record.class.inheritance_column.to_sym
      if record[klass_col]
         new_obj.type = record[klass_col]
      end
      new_obj.save
    end
  end
end
 
Generic-user-small Fora User 0 posts

Problem

You want to allow your users to write using an easy-to-read, easy-to-write plain text format, then convert it to structurally valid XHTML using a tool such as Markdown and/or Textile (BlueCloth/RedCloth).

Extra Ingredients

This recipe requires the BlueCloth and RedCloth gems for Ruby. Install them as follows:

gem install -r bluecloth
gem install -r redcloth

Credits

Tobias Luetke’s Typo weblog application makes extensive use of markup, pre- and post- text filters and inspired this recipe. For a much more complex example download the Typo source code:

svn co svn://typosphere.org/typo/trunk typo

Solution

Modify config/environment.rb by adding the following line at the end:

require 'bluecloth'
require 'redcloth'
Modify app/helpers/application_helper.rb by adding the following methods:

  def markdown(text)
    BlueCloth.new(text.gsub(%r{</?notextile>}, '')).to_html
  end

  def filtertext(text)
    RedCloth.new(text.gsub(%r{</?notextile>}, '')).to_html
  end
Then in your views you can simply do the following:

  <%= markdown(@user.description) %>
or

  <%= filtertext(@post.body) %>

Discussion

This recipe only affects the display of your data, it is stored in the DB in its original form (i.e. with all of the special Markdown/Textile characters). If you’d like to store the HTML-ized version in the DB you should modify your model(s) with a
:before_save
filter like so:

class Post < ActiveRecord::Base

  before_save :filtertext

  def filtertext
    self.title = RedCloth.new(self.title).to_html
    self.author = RedCloth.new(self.author).to_html
    self.body = RedCloth.new(self.body).to_html
  end

end
The only draw-back to this approach is that now the HTML code is in the DB and on a subsequent edit/update of these fields the output may not be what the user is expecting. Thus I would recommend shadowing these fields like so:

  def filtertext
    self.title_display = RedCloth.new(self.title).to_html
    self.author_display = RedCloth.new(self.author).to_html
    self.body_display = RedCloth.new(self.body).to_html
  end

and then in your view you would only show the display fields unless the user was editing the content in which case you’d use the ‘regular’ fields.

Further Reading

See the note above about Typo.

 
Generic-user-small Fora User 0 posts

Problem

You want to allow your users to use certain HTML tags in their input
while restricting all other HTML tags.

Extra Ingredients

None

Credits

The idea for this recipe comes from Koen Eijsvogels and his post in
the Request a
Recipe forum.

Solution

Modify config/environment.rb by adding the following line at the end:


require_dependency 'rails_patch/text_helper'

Add a new subdirectory rails_patch to your application lib directory.

Add text_helper.rb to your lib/rails_patch directory and ensure it
looks as follows:


module ActionView
  module Helpers
    module TextHelper
      ALLOWED_TAGS = %w(a img) unless defined?(ALLOWED_TAGS)

      def whitelist(html)
        # only do this if absolutely necessary
        if html.index("<")
          tokenizer = HTML::Tokenizer.new(html)
          new_text = "" 

          while token = tokenizer.next
            node = HTML::Node.parse(nil, 0, 0, token, false)
            new_text << case node
                        when HTML::Tag
                          if ALLOWED_TAGS.include?(node.name)
                            node.to_s
                          else
                            node.to_s.gsub(/</, "lt")
                          end
                        else
                          node.to_s.gsub(/</, "lt")
                        end
          end

          html = new_text
        end

        html
      end
    end
  end
end

Then in your views you can simply do the following:


  <%= whitelist(@post.body) %>

NOTE: Due to a limitation in textile (used in this forum) I had to remove the ampersands and semi-colons from the code above, so you’ll need to re-insert those in the gsub calls. So the code should look like node.to_s.gsub(/LESS-THAN/, “AMPERSANDltSEMI-COLON”) where LESS-THAN, AMPERSAND and SEMI-COLON are replaced by their respective characters.

Discussion

Obviously we are taking advantage of the fact that classes (and
modules, in this case) in Ruby are open by patching our own method
into Rail’s TextHelper module.

If you want to allow more tags than just anchors and images, add
the additional tags to the %w() in following line:


ALLOWED_TAGS = %w(a img) unless defined?(ALLOWED_TAGS)

Further Reading

You can see examples of this in the source code for the Typo
weblog where it patches ActiveRecord::Base and also
ActionController::Components.

href="http://www.typosphere.org">http://www.typosphere.org
 
Generic-user-small Fora User 0 posts

Problem

You have a rails application with lots of controllers / views. All your pages have common structure elements (like header, footer etc.) through usage of layouts.

Rails allows one level of layouts. Most of the time it is sufficient, but sometimes you feel uncofortable with having pages sharing common elements. For example, all pages of a particular controller should have a common “submenu”. You end up using the same particles on every controller view.

Solution

Most of modern frameworks have an ability to embed pages in a layout, and then embed the result in another layout and so on. With a little trick you can do the equal thing in Rails.

First, add the following method into your ApplicationHelper:


def inside_layout(layout, AMPERSANDblock)
  @template.instance_variable_set("@content_for_layout", capture(AMPERSANDblock))

  layout = layout.include?("/") ? layout : "layouts/#{layout}" if layout
  buffer = eval("_erbout", block.binding)
  buffer.concat(@template.render_file(layout, true))
end

Then, design your views to use e.g. ‘inner_layout’.

In ‘layouts/inner_layout.rhtml’ write:

<% inside_layout 'outer_layout' do %>

  <div id="common_elements">
  </div>

  <%= @content_for_layout %>

<% end %>

The ‘outer_layout.rhtml’ will go as ussual layout (unless it needs to be nested in other higher level layout)

NOTE: Due to a limitation in textile (used in this forum) I had to remove the ampersands from the code above, so you’ll need to re-insert those before “block” variable. So the code should look like “capture(AMPERSANDblock) where AMPERSAND is replaced by the ampersand character.

 
Generic-user-small Fora User 0 posts

Note that to be able to use fixtures, we also need to define the reset_pk_sequence! for our MysqlAdapter. This function is called when all the fixtures are loaded to set the value of the sequence table accordingly (otherwise it will have a perhaps inconsistent value).

In our case, we have to get the last id value of our table, and set or update the id value of our sequence table accordingly:


      def reset_pk_sequence!( table, pk = nil, sequence = nil )                                                                                                                      
        sequence_name = table + "_sequence"                                                                                
        max_pk = select_value("SELECT MAX(id) FROM #{table}").to_i + 1                                                    
        sql = "INSERT INTO #{sequence_name} (`id`) VALUES (#{max_pk})"                                                         
        begin                                                                                                                  
          insert( sql )                                                                                                        
        rescue ActiveRecord::StatementInvalid                                                                                  
          sql = "UPDATE #{sequence_name} SET id=#{max_pk}"                                                                     
          nb = update( sql ,"Update Sequence")                                                                                 
        end                                                                                                                    
 
Generic-user-small Fora User 0 posts

Can’t wait for the book fellas…another great Rails resource. I made you famous by adding your topics RSS to Rails-a-Rama!. Yes I am pimping it…because its a good thing like Martha Stewart said! I think… ;-)

 
Generic-user-small Fora User 0 posts

There doesn’t seem to be any actual source code in the Authentication folder (or any other folder)?

 
Generic-user-small Fora User 0 posts

Problem

You want to use AJAX on a field that isn’t free text, but is a list of things that your user can choose.

Extra Ingredients

Rails 1.1

Solution

As of script.aculo.us version 1.5.3, a control called Ajax.InPlaceCollectionEditor deals with precisely this issue. This function works identically to Ajax.InPlaceEditor, except that it takes an array of arrays consisting of option names and values.

A Rails-oriented solution, however, should work similarly to the way select options are generated. Define a function in your application_helper.rb file:

def in_place_collection_editor_field(object,method,container)

The final parameter, container, contains the options for the drop-down box. First, resolve a tag and create the submit URL:

tag = ::ActionView::Helpers::InstanceTag.new(object, method, self)
url = url_for( :action => "set_#{object}_#{method}", :id => tag.object.id )

On the controller side, you will need to define a method set_object_method to handle the input. Next, start the definition of the function:

function =  "new Ajax.InPlaceCollectionEditor(" 
function << "'#{method}'," 
function << "'#{url}',"

Then, process the options:

collection = container.inject([]) do |options, element|
  options << "[ '#{html_escape(element.last.to_s)}', '#{html_escape(element.first.to_s)}']" 
end

Finally, add the collection to the final Javascript:

function << "{collection: [#{collection.join(',')}]," 
  function << "});" 
end
javascript_tag(function)

For example, in a list.rhtml file, the following defines an editor for a field called custom_field that has a number of options taken from the FieldType model:

<%= in_place_collection_editor_field 'custom_field', 'field_type', 
                           FieldType.find_all.collect{|x| [x.name,x.id]} %>

The x.name is the plaintext description, to be displayed to the user, while the x.id is the internal id. Given that the selected object id will be passed to the controller, along with the id of the object to process, the controller is simply defined as:

def set_custom_field_field_type
  @i = CustomField.find(params[:id])
  f  = FieldType.find(params[:value])
  @i.update_attribute( :field_type, f )
  render :text => f.name
end

Discussion

While this example also by-passes model validation, this can easily be built into the controller action. Logical extensions to this approach might include the other approaches used in ActionView::Helpers::FormOptionsHelper.

Further Reading

See the script.aculo.us documentation

 
Generic-user-small Fora User 0 posts

This is almost certainly not the correct place to post this, so apologies in advance. Update Its definately the wrong place :-(.

Paragraph four in “The Console is Your Friend” ends with the following line:

“Who needs and IDE?!”

My guess is that this was meant to be “Who needs an IDE?!”

Keith

 
Generic-user-small Fora User 0 posts

That’s right! Thanks :)

 
Generic-user-small Fora User 0 posts

Hello,

I’m afraid the very first recipe, InPlaceEditing, stumped me. How do I ‘cook’ this recipe? Don’t I have to create a database first? What’s the name of the database to create? I know I have to run some scripts sometime in the process.

Could we, at least for the first few recipes, be as detailed as can be, with one recipe that starts from the very, very beginning?

Thanks! frustrated

 
Generic-user-small Fora User 0 posts

Bold phrase

 
Generic-user-small Fora User 0 posts

Problem

A model requires that one or more of its attributes be write once only. In other words, these protected attributes may only be set when the model is first save dto the database and are immutable thereafter.

Credits

In order to write my unit tests for this code I referred to Ezra Zygmuntowicz’s ez_where plugin

Solution

The code that you are going to have to write needs to do the following :
  • Only run on an update. This validation doesn’t apply to record creations.
  • Retrieve a copy of the model form the database.
  • Compare each protected attribute of the current model to that of the copy.
  • Add an error to the model’s errors collection for each protected attribute that differs.

You could situate this code within the model’s validate_on_update method. But if you created your own validation as a plugin, validates_write_once_of, you could reuse this validation on other models and you’d gain the benefit of making your code more descriptive. Rails makes this easy (of course) – we’ll just model the new vaidation on an existing one.

Validations are located in lib/active_record/validations.rb

Examining the method validates_presence_of for example, shows that it takes the following options
  • message – a cusom error message (default is “can’t be blank”)
  • on – Specifies when this validation is active (default is :save, other options are :create, :update)
  • if – Specifies a method, proc or string to call to determine if the validation should occur

The new validation will take all those options except for ‘on’. It only makes sense for this validation to be active on an update. The default message will be “can’t be changed”.

Here’s the code for write_once_of.rb

module WriteOnceOf
  def validates_write_once_of(*attr_names)
    configuration = { :message => "can't be changed" }
    configuration.merge!(attr_names.pop) if attr_names.last.is_a?(Hash)
    send( validation_method(:update) ) do |record|
      unless configuration[:if] and not evaluate_condition(configuration[:if], record)
        previous = self.find record.id
        attr_names.each do |attr_name|
          record.errors.add( attr_name, configuration[:message] ) if record.respond_to?(attr_name) and previous.send(attr_name) != record.send(attr_name)
          # replace the 'and' above with a double ampersand 
        end
      end
    end
  end
end

To get the validation to work as a plugin package it as follows

validates_write_once_of/
  init.rb
  lib/
    write_once_of.rb

The code for init.rb is


require 'write_once_of'
ActiveRecord::Base.extend WriteOnceOf
The validates_write_once_of folder should be placed in the /vendor/plugins folder of your rails application.

Use as follows

class SomeClass < ActiveRecord::Base
  validates_write_once_of :some_attribute, { :message => "can't be changed ... ever" }
end

Discussion

Rails ships with a very full and flexible set of validations. But being able to create your own validations enables you to write code that is very declarative and specific to your application domain. Its certainly worth doing so given how easily it can be done.

 
Generic-user-small Fora User 0 posts

Problem

You’ve got your software running. You’re using Capistrano to deploy it. But there are several people working on the project who can deploy to production. You want all of the developers to be notified when your software gets deployed to production, including knowing which version of the software was deployed.

Extra Ingredients

Because this hooks into the deployment, this assumes you’ll already be including Capistrano

Solution

Generate a mailer :

 ruby script/generate mailer Notifier deployed
      exists  app/models/
      create  app/views/notifier
      exists  test/unit/
      create  test/fixtures/notifier
      create  app/models/notifier.rb
      create  test/unit/notifier_test.rb
      create  app/views/notifier/deployed.rhtml
      create  test/fixtures/notifier/deployed 

In the capistrano deploy recipe (config/deploy.rb), create an “after_deploy” task :

desc "After the deploy is done, let people know about it" 
task :after_deploy do
  #  The mailer needs the environment configured for it - otherwise it doesn't 
  #  find the templates, and it doesn't know how to send email.
  require File.join(File.dirname(__FILE__), 'environment')
  Notifier.deliver_deployed("#{revision}")
end  

task :after_deploy_with_migrations do
  #  The same stuff that we want to do after a "regular" deploy
  after_deploy
end
Let the Notifier model handle the information sent to it :

  def deployed(version)
    @subject    = "Software Deployed!" 
    body(:version => version)
    @recipients = 'geeks@example.com'
    @from       = 'deployer@example.com'
    @sent_at    = Time.now
    @sent_on    = Time.now
  end  
Finally, modify the view (app/views/notifier/deployed.rhtml) to have some important information.

Hello Geeks,

We just deployed to production with version <%= @version %> of the baseline.

It is now <%= Time.now %>.  Have a nice day.

Thank you, 

"The Deployer" 

Now, whenever someone deploys your software to production, you will receive a nice email letting you know about it.

Discussion

This example uses the version that was deployed to production, but there are several variables that the deployment has available to it. Maybe including the user who did the deployment would be important to you, or the machines that were included in the deployment. See information in the deploy.rb along with documentation on Capistrano.

Including the “environment” in the require means that you will have access to all of the information in your application (and all of the database connections). That means you could also do different things than just sending an email – you could update a column in one of your models, insert information into your database, or just list all the users in your system.

 
Generic-user-small Fora User 0 posts

I understand where you’re coming from, resty, but that’s not what “recipe” books are about, in general. It’s an established genre, and it’s pretty much what it sounds like—how to make a specific thing, be it chocolate chip cookies or in-place editing.

To carry the analogy, if you’re holding your recipe book in one hand, and looking worryingly at what you’ve been assured is an “oven,” the recipe book is best perused after you read a book of the “How To Cook” genre.

I would suggest Agile Web Development with Rails, by Dave Thomas and DHH, which is available from Pragmatic Bookshelf. Once you’ve got your head wrapped around that, try Ruby For Rails, by David Black, from Manning Press.

 
Generic-user-small Fora User 0 posts

Problem

You want to share instance variables across methods, or controllers.

Extra Ingredients

None

Credits

Taken from a post by Ezra Zygmuntowicz on the rails mailing list.

Solution

Put this in the top of your controller where you want to have a shared var. Assuming the var is @foo

MyController < ApplicationController

before_filter :setup_foo
 attr_reader :foo
def setup_foo
    @foo = "Whatever you want @foo to contain" 
end

end

Now @foo will be available to every action method in your controller and also in your views for that controller. If you want @foo to be available in all your controllers and view, put that code in your application.rb ApplicationController.

Discussion

You can also use a CONSTANT of course, defined in application.rb, for a variable whose contents will not change, and which will be available to the entire application.

 
Generic-user-small Fora User 0 posts

The last method above, load_from_yaml, doesn’t work anymore as written with ruby 1.8.4 . I’ve patched the restore.rake file. The link above also reflects the changes.

 
Generic-user-small Fora User 0 posts

hello,

i am not new to rails but i am new to ajax. when i try to run the in_place_editor_field recipe i get a weird error.

this is what it says.

Error communicating with the server:

Action Controller: Exception caught
body { background-color: #fff; color: #333; }
body, p, ol, ul, td {
  font-family: verdana, arial, helvetica, sans-serif;
  font-size:   13px;
  line-height: 18px;
}
pre {
  background-color: #eee;
  padding: 10px;
  font-size: 11px;
}
a { color: #000; }
a:visited { color: #666; }
a:hover { color: #fff; background-color:#000; }

Unknown action No action responded to set_client_contact_first_name

i generated my app with scaffolding, and include the default javascript files. this error comes up after i make a change and press OK. then the area where i was editing the field says saving, then the error pops up in a javascript alert window.

any help would be greatly appreciated.

thanks.

 
Generic-user-small Fora User 0 posts

One big problem with this recipe:

It doesn’t do anything about tag attributes. A false sense of security is worse than none at all.

This means I can submit javascript handlers on allowed tags, a pretty big no-no. Other pre-santizing factors come into play as well.

The way around this is simple, but annoyingly so because you can’t write to the attributes attribute of an HTML::Tag node.


# stuff . . .
 if ALLOWED_TAGS.include?(node.name)
                             new_node=HTML::Tag.new(node.parent, node.line, node.position, node.name, [], node.closing)
                             new_node.to_s
                           else
# stuff . . .

 
Generic-user-small Fora User 0 posts

I’ve written an update to this recipe that allows you to safely filter attributes/tags and define “profiles” that let you create different levels of filtering for different users.

Details here:

http://www.kookdujour.com/blog/details/11

32 posts, 1 voice

Page: 1 2