The Basics

Version Control

Be careful not to include the following files into your version control system:

  • /config/database.yml
  • contents of /log
  • contents of /tmp
  • public/dispatch.* files: if you're using e.g. InstantRails on Windows, the #! bang line which calls ruby is bound to be wrong on your remote system (i.e. live server)
  • public/.htaccess: might be different on your remote system. If you're using webricks locally then .htaccess is not used, but it's cleaner to keep it out of svn.

Doing things with get/post variables

A warning: all get/post variables seem to be strings. So if you ever need to compare an integer to a parameter, do this:

if my_integer == my_parameter.to_i

When to use CamelBack notation

In Rails 1.1.6, you never use CamelBack notation, not even in the database, except for the model class definition, and for class instantiations.

Example:

class ScheduledExercise < ActiveRecord::Base  
  belongs_to :entry
  belongs_to :schedule  
  belongs_to :game_type    
end

Class ScheduledExercise belongs to class GameType, but you use belongs_to :game_type to indicate this relationship. Of course, the same goes for the definition of the other end of the relationship:

class GameType < ActiveRecord::Base
  has_many :games
  has_many :entries
  has_many :scheduled_exercises
end

And this is what the table definition for scheduled_exercises looks like:

CREATE TABLE `scheduled_exercises` (
  `id` int(11) NOT NULL auto_increment,
  `schedule_id` int(11) NOT NULL default '0',
  `round_number` int(11) NOT NULL default '0',
  `entry_id` int(11) NOT NULL default '0',
  `entry_rank` int(11) NOT NULL default '0',
  `game_type_id` int(11) NOT NULL default '0',
  `times_number` int(11) NOT NULL default '0',
  `owner` int(11) NOT NULL default '0',
  `lock_version` int(11) NOT NULL default '0'
);

Multiple querystring parameters

Want to use more than just an id variable in your link_to call? Do this:

  <%= link_to project_label, {:action => 'show', 
                              :params => {:project_id => project_id, :user_id => user.uid}} %>

Outputting form fields

Printing form fields does not seem to work with local variables. Use instance variables instead:

#DO NOT USE THIS:
      <% @answers.each do |answer| %>
        <%= text_field 'answer', 'description' %><br/>
      <% end %>
#but this:
      <% @answers.each do |@answer| %>
        <%= text_field 'answer', 'description' %><br/>
      <% end %>

Button as a link

If you don't want your button to submit a form, use the url_for method:

<input 
   type="button" 
   value="Delete" 
   onclick="if (confirm('Are you sure?')) {
      window.location='<%= url_for 
        :controller => 'match', 
        :action => 'destroy_match', 
        :id => selected_match_id, 
        :game_id => @game.id %>'}"/>

Localization with Globalize

Installation and configuration

Installing Globalize is pretty straightforward. See the documentation online:

http://www.globalize-rails.org/wiki/ This link does not seem to work any more at the time of writing (20061109), go here instead: http://www.globalize-rails.org/globalize/

Do not forget to:

  • include the following lines at the end of your rails_apps/yourAppName/config/environment.rb file:
include Globalize
Locale.set_base_language 'nl-NL'

(or any other base language if you're not in the Netherlands, of course).

  • before any filters, add the following line in your file rails_apps/yourAppName/app/controllers/application.rb:
Locale.set("nl-NL")

I have made it a habit to store my translation strings in app/helpers/translations.rb. To get this to work, add the line require 'translations' to your application_helper.rb file.

Using Globalize to output plain string dates

Using Globalize is also pretty easy. It just melds in with the various date_select functions already present in Rails. These methods allow you to output select boxes where you can select and edit dates.

Outputting a plain formatted date string, however, is not really documented. Here is how you do it:

  <p><label for="account_logdate">Laatst ingelogd</label><br/>  
  <span id="account_logdate"><%= @account.logdate.localize("%d %B %Y") %></p>

This outputs:

Laatst ingelogd
18 januari 2006

Note that dates can be formatted using the formatting options for the strftime method of the Ruby Time class.

Localizing Error Messages

This is from: Spongecell Tech Blog but with a few modifications.

The idea is to get the error messages as generated by error_messages_for automatically translated. Globalize does already have a hook for this, but the code does not seem to work (or, more likely, I'm not using it correctly). I replaced the error_messages_for code from Globalize in the file vendor/plugins/for-1.1/lib/globalize/rails/active_record_helper.rb with the following:

      def error_messages_for(object_list, options = {})
        return "" if object_list.nil?
        
        options = options.symbolize_keys
        
        bullets,main_obj_name = bullets_from_errors(object_list, options)
        
        if !bullets.blank?
           #default to standard error message. replace the ${NUM_ERRORS} with the number of errors.
           #options[:header_message] ||= "%d errors prohibited this %1 from being saved".t(nil,bullets.length).gsub('%1',"#{(main_obj_name || 'object').t}")
           #options[:header_message] ||= ("%d errors prohibited this %1 from being saved".t(nil,bullets.length)).gsub('%1',"#{(main_obj_name || 'object').t}")
           options[:header_message] ||= "The data could not be saved".t
           options[:header_message] = options[:header_message] % bullets.length
           
           content_tag("div",
              content_tag( options[:header_tag] || "h2", options[:header_message]) +
              content_tag("p", (options[:header_sub_message] || "There were problems with the following fields:").t) +
              content_tag("ul", "#{bullets}"),
              "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
           )
        else
          ""
        end
      end
      
      
      
      def bullets_from_errors(object_list,options)
    
        subs = options[:sub] ? options[:sub].stringify_keys : {}
        options[:skip] ||= []
        main_obj_name = nil
        
        #create a list of bullets (html <li> tags) by concating the objects
        bullets= [object_list].flatten.inject([]) do |msg_list,object|
        
          #if it is a string get the instance variable
          if object.kind_of?(String)
            main_obj_name ||= "#{object.to_s.gsub("_", " ")}" 
            object = instance_variable_get("@#{object}")
          elsif object
            main_obj_name ||= object.class.to_s.titleize.downcase
          end
              
          #if the object exists and responds has errors append each error to the list
          if (object && object.respond_to?('errors') && !object.errors.empty?) 
            object.errors.each do |attr, msg| 
                if (!options[:skip].include?(attr.to_s))
                  obj_name = (subs[attr] || "#{attr == 'base' ? '' : object.class.human_attribute_name(attr)}")
                  #replace the field names with the names specified in the subs hash
                  msg_list << content_tag('li', "#{obj_name} #{msg}".t("#{obj_name.t} #{msg.to_s.t}"))
                end
            end
              
            
          end
          msg_list
        end
        
        return bullets, main_obj_name
      end      

And then I provided the translations for the standard error messages in my custom app/helpers/translations.rb file, e.g.:

   Locale.set_translation('has already been taken', Language.pick('nl-NL'), 'is al bezet')

Legacy Database

If you are working on a legacy database, chances are that your database will not adhere to the Rails conventions. This article talks at length about using a legacy database:

http://wiki.rubyonrails.org/rails/pages/HowToUseLegacySchemas

Gotcha: Foreign key columns

A foreign key column is normally called: modelName_id in Rails. If you're using just modelName in your legacy database, be sure not to confuse the actual object referenced, and the primary key. The database, of course, holds the primary key of a record in a foreign table, while the Rails code holds an actual object. E.g.:

    <% @categories = Category.find(:all, :order =>  "label") %>     
    <select id="quest_category" name="quest[category]">
      <option value="">[Profiel]</option> 
      <% @categories.each do |category| %>
        <option value="<%= category.uid %>" 
          <%= 'selected="selected"' if category.uid == @quest.category.uid %>>      
          <%= "#{category.label}" %>
        </option>
      <% end %>
    </select>

Normally, we would use @quest.category_id instead of the highlighted line.

Generally speaking, it's best to choose model names that are different from foreign key column names.

Gotcha: association table in a legacy database

Note: there is also the more powerful has_many :through way of joining models.

The association tables in your legacy database may have their own primary key. If the name of the primary key column corresponds to the primary key column's name of one of the associated tables, you're in for trouble. Here's why, from the Agile Web Development book:

Active Record automatically includes all columns from the join tables when accessing rows using it. If the join table included a column called id, its id would overwrite the id of the rows in the joined table.

Solution: use the :insert_sql and :finder_sql arguments when you use the has_and_belongs_to_many method on each of the associated classes.

Here is a complete example:

class CompleteQuest < ActiveRecord::Base
  set_table_name "tx_sorubber_quests"
  set_primary_key "uid"  
 
  belongs_to :profile, :foreign_key => 'category'      
  belongs_to :school, :foreign_key => 'level'
  belongs_to :company, :foreign_key => 'organization'
  has_and_belongs_to_many(
    :completeTexts,
    :join_table => 'tx_sorubber_quest_texts',    
    :conditions => "tx_sorubber_quest_texts.deleted = 0 AND tx_sorubber_quest_texts.hidden = 0 AND pages.pid = 2",
    :foreign_key => 'quest',
    :association_foreign_key => 'text',
    :order => 'title',
    :insert_sql => 'INSERT INTO tx_sorubber_quest_texts (quest,text) VALUES (#{id},#{record.id})',
    :finder_sql => 'SELECT pages.* FROM pages INNER JOIN tx_sorubber_quest_texts ON pages.uid = tx_sorubber_quest_texts.text WHERE (tx_sorubber_quest_texts.quest = #{id} AND (tx_sorubber_quest_texts.deleted = 0 AND tx_sorubber_quest_texts.hidden = 0 AND pages.pid = 2)) ORDER BY pages.title')          
end

As stated here http://lists.rubyonrails.org/pipermail/rails/2006-February/019824.html, record.id must refer to association_foreign_key, while id refers to foreign_key.

Also, be sure to prefix the wildcard * with your table name. For some reason, Rails mixes up the foreign key column of the associated record with the one from the association record if you don't.

You can now populate the join table using the « method:

    @complete_quest = CompleteQuest.find(params[:quest_uid])
    @complete_text.completeQuests<<@complete_quest
    @complete_text.save

And finally, don't forget to specify all this for the class on the other end of the habtm relation too.

Gotcha: column name is a reserved name in Ruby on Rails

See this http://wiki.rubyonrails.com/rails/pages/ReservedWords site for a list of reserved words.

If your legacy database contains a column name that is a reserved word, map the column name to a different name. Apparently, there is no way to do this in the model class. Instead, you have to use the :select option in the find method:

    @games = Game.find(
      :all,
      :conditions => [ " text_complete = ?  AND quest = ? ", params[:text_id], params[:quest_id] ],
      :select => 'type AS game_type, uid, text_complete, label, quest, author'      
    )

If you want to do any inserts (through methods such as save and create), you also have to make clear to Rails to what column you attribute should be mapped. Use this code in your model class:

alias_column "new_name" => "old_name"

And put this in your environment.rb file:

module Legacy
  def self.append_features(base)
    super
    base.extend(ClassMethods)
  end
  module ClassMethods
    def alias_column(options)
      options.each do |new_name, old_name|
        self.send(:define_method, new_name) { self.send(old_name) }
        self.send(:define_method, "#{new_name}=") { |value| self.send("#{old_name}=", value) }
      end
    end
  end
end
 
ActiveRecord::Base.class_eval do
  include Legacy
end

This is also documented here:

http://www.bigbold.com/snippets/posts/show/556

The authors suggests you put the code in a file in /lib, but that did not work for me.

Gotcha: Unix Timestamp Instead of datetime Column

Active Record does not recognize unix timestamps. And how could it? They are usually stored as integers in the database.

So, to be able to use helpers like date_select, you have to convert the unix timestamp to a Ruby Time object. See also the section about date and time in ruby on rails.

In your model class, you're going to have to overwrite the default accessors. Here is an example:

class Event < ActiveRecord::Base

  def starttime
    Time.at(read_attribute(:starttime))
  end 

end

The at method creates a new Time object for a given unix timestamp. See the documentation for the Ruby Time class.

If your legacy database contains zeroes (0) which are not meant to represent january the 1st, 1970, then you'll have to provision for this in your model. As an example: Typo3 (the cms) uses 0 in their fe_users table to indicate that no start date for a user account has been specified (yet).

The following code snippet returns a “blank” date, if the unix timestamp is 0:

  def starttime    
    if read_attribute(:starttime) == 0
      nil
    else     
      # at method creates new Time object based on a unix timestamp (in seconds)
      Time.at(read_attribute(:starttime))
    end    
  end

To save a date, you'll have to do the reverse: go from a Ruby Time object to a unix timestamp. Here's the “write” accessor code:

  def starttime=(obj_time)
    # to_i method outputs Time object in seconds (unix timestamp)
    write_attribute(:starttime, obj_time.to_i)
  end

But we are still not done yet. If you use a date helper method like date_select in combination with the code from above, you'll notice that it runs amok. The date_select helper posts a multi-part date hash, which it tries to put into an object for the database field which it is bound to. But unfortunately, this field is not of “type” Time as we are working with a legacy database.

So, you'll have to “unbind” the form control. Instead, use select_year, select_month and select_day to create the form control. This way, the date won't be submitted in the same hash as the rest of the data bound controls. Here's an example extract from a view:

  <div class="optional">
    <label for="user_starttime">Start Date</label>
    <%= select_day  @user.starttime, :include_blank => true, :field_name => 'start_day' %>   
    <%= select_month  @user.starttime, :include_blank => true, :field_name => 'start_month' %>
    <%= select_year  @user.starttime, :include_blank => true, :field_name => 'start_year' %>
  </div>

To store the start date into the database, assign all “normal”, data bound attributes to the model object. Then add the start date separately:

  def update
    @user = User.find(params[:id])
    
    # First, assign the "regular" attributes 
    @user.attributes = params[:user]
    
    # Now, insert the attributes which are not subject to "data binding"
    dates = params[:date]    
    @user.starttime = Time.mktime(dates[:start_year],dates[:start_month],dates[:start_day])
    @user.endtime = Time.mktime(dates[:end_year],dates[:end_month],dates[:end_day])
    
    if @user.save      
      flash[:notice] = 'The user data has been stored.'      
      redirect_to :action => 'edit', :id => @user.uid
    else
      flash[:error] = 'Something went wrong: the user data could not be stored.'   
      render :action => 'edit'
    end
  end

Datebocks

To make the Datebocks plugin (an advanced date control / widget) work with a unix timestamp database column, you just have to do what you need to do for globalization / localization of Datebocks anyway: turn the database field value into a Time object and then output the desired date format.

Because datebocks puts the date unprocessed in a text_field, you'll have to parse the date yourself. The best place to do that, is in the model. Of course, this excludes the use of other date controls / widgets, such as date_select (but a localized version of Datebocks is incompatible with any other date control / widget anyway).

  def starttime_before_type_cast    
    if read_attribute(:starttime) == 0
      nil
    else     
      # at method creates new Time object based on a unix timestamp (in seconds)
      # strftime method outputs the Time object as a formatted string      
      Time.at(read_attribute(:starttime)).strftime("%d-%m-%Y")
    end    
  end

Incidentally, you can use almost the same “read” accessor code for a true datetime column as well. This is because the read_attribute(:starttime) gives you the value after typecasting: a Ruby Time object. Of course, you wouldn't need the add method then.

Datebocks accessor code for datetime column (the non-legacy database case):

  def start_date_before_type_cast    
    if read_attribute(:start_date).nil?
      nil      
    else                    
      read_attribute(:start_date).strftime("%d-%m-%Y")   
    end    
  end

To get the user input back into the database as a unix timestamp, you'll have to do some extra work as well:

  def update
    @user = User.find(params[:id])    
    user_params = params[:user]
    
    # First, insert the date related attribute
    if user_params[:starttime] != ''
      dd, mm, yyyy = $1, $2, $3 if user_params[:starttime] =~ /(\d+)-(\d+)-(\d+)/        
      @user.starttime = Time.mktime(yyyy,mm,dd)
    else 
      @user.starttime = 0 
    end          
    
    # Now remove date related attribute from hash
    user_params.delete(:starttime)    
    
    # Finally, assign the "regular" attributes 
    @user.attributes = user_params
    
    if @user.save      
      flash[:notice] = 'The user data has been saved.'      
      redirect_to :action => 'edit', :id => @user.uid
    else
      flash[:error] = 'Something went wrong. The user data has not been saved.'   
      render :action => 'edit'
    end
  end

Key here is of course the parsing code dd, mm, yyyy = $1, $2, $3 if user_params[:endtime] =~ /(\d+)-(\d+)-(\d+)/. This example is based on the Dutch (Netherlands) locale, but you can easily change it to fit your own needs.

Notice also that the line @user.starttime = Time.mktime(yyyy,mm,dd) assumes that your model uses a datetime column in the background. However, here we are dealing with an integer column to store the unix timestamp. So we need a “write” accessor in our model to convert the Time object to a unix timestamp:

  def starttime=(obj_time)
    # to_i method outputs Time object in seconds (unix timestamp)
    write_attribute(:starttime, obj_time.to_i)
  end
Adapting Datebocks' Appearance

If you want the help-button and the message text gone, add this to your site's css code:

/* DateBocks Calendar */

#dateBocks, #dateBocks ul {display:inline;}

.date_format {
  font-size:7pt;
}

#dateBocksMessage {
  display:none;
}

div.datebocks_help_icon {
  display:none;
}

#entry_dateHelp {
  display:none;
}

Date and time in Ruby on Rails

Dates in the model (i.e. an ActiveRecord object) are really Ruby Time objects. See:

http://www.ruby-doc.org/core/classes/Time.html

Adding a number of days to a given date

Iff you want to add 1 day to a given date, look up the available methods for the object Time. There is the +(n) method, which adds n seconds to a given date. To get to a full day, simply compute the number of seconds in a day:

@enddate = @temp_account.logdate+(60 * 60 * 24)

Pagination or paging a record set

Paging isn't very hard in Rails - paging through a query without losing state is. First some documentation on paging:

http://wiki.rubyonrails.com/rails/pages/HowtoPagination/

http://wiki.rubyonrails.com/rails/pages/PaginationHelper

These wiki pages should be enough to get you started. Now the tricky part: propagating the filtering (search) criteria.What? Well, imagine constructing a search screen. The results of the search cannot be placed in a single screen, so you decide to use pagination.

Okay, you've got your search screen. The results are nicely displayed in a list type page. You've just searched for marsupials in your online pet shop. So you see a page filled with marsupial species. And, there are many marsupials, the list continues on the next page. So you click next page. Hey! This is not supposed to happen! Suddenly the results contain all species in your pet shops. The list is now totally unfiltered and contains lamas, cheetas, zebras, and so on. Somehow, the search criteria got lost.

Below I present two solutions. The first one is the most complex, but does not rely on any external storage mechanisms. All criteria are passed on through the GET/POST variables. The second approach is much simpler: it just uses a session object to store the criteria.

Passing on search criteria or filtering rules

Search conditions or filtering rules must be passed on (propagated) through the params hash. The following assumes that you are using a scaffold derived view, the edit view, as your search screen. This is the page where your users select or type in their search criteria.

Scaffold-derived views use the form helper functions to build controls, such as text fields or select boxes. The controls are bound to a model, because the model and the attribute names are supplied to the helper methods. If you submit the page, Rails parses the post variables into a hash called params. This is usually a nested hash: a hash containing a few “plain” values, and at least one other hash - which in turn contains the values for your model.

The problem

Propagating parameters works perfectly as long as you return your values through an actual form submit. However, if you need your nested parameters to reach the webserver by other means, you're in trouble. The link_to and url_for and request_to methods all flatten your params hash. And the link_to method is exactly what you need if you decide to paginate your result set.

To clarify things a bit, consider the flow of the webapplication which offers search:

  1. The user types in search criteria, in a form residing in an ordinary view. The criteria are submitted to a controller.
  2. The controller retrieves a result set, presumably from a database, based on the search criteria. Because the next view is going to be a list-type view, we do the database retrieval from a method called list.
  3. In the list view, the parameters are still available - which comes in very handy, because we need to get them back to the server to restore the original search query once we decide to go to the next page. Using the link_to method, we get all criteria back to the server. It does this by flattening our original params hash.
  4. The server tries to extract the model values from the params hash, but fails, because the params hash is now different from the first call to the list method.

No problem, you say, I'll just use the nonmodel fields, such as text_field_tag. You could do that, but the set of normal field helpers is much larger. There is no date_select version for nonmodel fields, for instance. Yes there is: select_date.

The solution

The first call to the list method in the controller comes with the params hash still intact. We are now going to extract the model variables from the params hash, and put them back in separately. In effect, we are going to flatten the hash as well, but in a controlled manner.

Here is the code for the list method. The example is about accounts, which can be searched using criteria such as start - and end date (expiry date), user name, user group, etc..

  def list   
 
    # assign the params hash to a temporary variable
    temp_hash = params
 
    # check if we have a nested hash by looking for another, contained hash 
    if (params.has_key?(:account))      
      account = params[:account]            
      temp_hash = params.merge(account)
    end      
    # BUG? Assignment to params always seems to take place, even if you put the 
    # assignment in a conditional
    params = temp_hash
    params.delete(:account)                                    
 
    # Compose own date string. Assign string to Account date field to achieve 
    # sanity checking (mainly to prevent SQL injection)
    @temp_account = Account.new()
    @temp_account.start_date = "#{params[:'start_date(3i)']}-#{params[:'start_date(2i)']}-#{params[:'start_date(1i)']}"
    @temp_account.end_date = "#{params[:'end_date(3i)']}-#{params[:'end_date(2i)']}-#{params[:'end_date(1i)']}"
    @temp_account.logdate = "#{params[:'logdate(3i)']}-#{params[:'logdate(2i)']}-#{params[:'logdate(1i)']}"
 
    conditions = Array.new            
    conditions << 'username = :username' if params[:username] and (params[:username] !=  "") 
    conditions << 'license_id = :license_id' if ( params[:license_id] and params[:license_id] !=  "" )
    conditions << 'usergroup_id = :usergroup_id' if (params[:usergroup_id] and params[:usergroup_id] !=  "" )
    conditions << "start_date >= '#{@temp_account.start_date.strftime('%Y-%m-%d')}' AND start_date < '#{(@temp_account.start_date+(60 * 60 * 24)).strftime('%Y-%m-%d')}' " if (@temp_account.start_date)
    conditions << "end_date >= '#{@temp_account.end_date.strftime('%Y-%m-%d')}' AND end_date < '#{(@temp_account.end_date+(60 * 60 * 24)).strftime('%Y-%m-%d')}' " if (@temp_account.end_date)
    conditions << "logdate >= '#{@temp_account.logdate.strftime('%Y-%m-%d')}' AND logdate < '#{(@temp_account.logdate+(60 * 60 * 24)).strftime('%Y-%m-%d')}' " if (@temp_account.logdate)
 
    if conditions.size == 0            
      @account_pages, @accounts = paginate :accounts, :per_page => 10
    else         
      @account_pages, @accounts = paginate( :accounts, :per_page => 10, :conditions => [conditions.join(' AND '), params ] )
    end
  end

Because we have flattened the hash, we can now always assume the same internal structure upon each call.

Unfortunately, the new params hash is not passed on to the next view. So, in the list view, we need to repeat the trick of flattening the params hash.

... [stuff omitted] ...
<%
  temp_hash = params
 
  if params[:account] and (not params[:account].empty?)  
    account = params[:account]        
    temp_hash = params.merge(account)
  end
 
  params = temp_hash
  params.delete(:account)
  params.delete(:page)
 
%>
 
 
<%= link_to 'Previous page',   {:params => params.merge('page' => @account_pages.current.previous)}, :post => true  if @account_pages.current.previous %>
<%= pagination_links(@account_pages, {:params => params} ) %>
<%= link_to 'Next page',  {:params => params.merge('page' => @account_pages.current.next)}, :post => true  if @account_pages.current.next %> 
 
<br />
 
<%= link_to 'New account', :action => 'new' %> | <%= link_to 'Search', :action => 'search' %>

If you set out to create your own solution, be sure to check the Ruby manuals on hashes, and especially on how to merge hashes. See also:

http://www.rubycentral.com/ref/ref_c_hash.html

Using session to propagate filter criteria

Another, much simpler, approach is to put the filter criteria in a session object, using the controller's class name as a key:

session[self.class] = params[:person]

If the user performs a new search, just empty the session:

  def search    
    if session[self.class] 
      session[self.class] = nil
    end
  end

This approach uses an ordinary search view, based directly on a scaffold generated edit view.

Here is a complete example of a paginated list (to be used in combination with the search mentioned action above):

  def list
 
    if (not session[self.class]) or (session[self.class].nil?)
      # 1st call      
      session[self.class] = params[:person]
    end
 
    search_terms = session[self.class]              
 
    conditions = Array.new
    if not search_terms.nil?            
      conditions << 'last_name = :last_name' if search_terms[:last_name] and (search_terms[:last_name] !=  "") 
      conditions << 'prepart = :prepart' if search_terms[:prepart] and (search_terms[:prepart] !=  "")    
      conditions << 'initials = :initials' if search_terms[:initials] and (search_terms[:initials] !=  "")
      conditions << 'first_name = :first_name' if search_terms[:first_name] and (search_terms[:first_name] !=  "")
      conditions << 'email = :email' if search_terms[:email] and (search_terms[:email] !=  "")
      conditions << 'organization_id = :organization_id' if search_terms[:organization_id] and (search_terms[:organization_id] !=  "")
    end 
 
    if conditions.size == 0            
      @person_pages, @people = paginate(:people, :per_page => 10, :order_by => 'last_name,prepart,first_name,id')
    else         
      @person_pages, @people = paginate( :people, :per_page => 10, :order_by => 'last_name,prepart,first_name,id', :conditions => [conditions.join(' AND '), search_terms ] )
    end    
 
  end

And a slightly more complex example, for how to combine “fixed” conditions with filter criteria:

    if conditions.size == 0
      @entry_pages, @entries = paginate( :entries, :per_page => 10, :conditions => ['user_id = ? AND bookyear_id = ? ', session[:user_id], session[:bookyear_id] ] )
    else  
      conditions << 'user_id = :user_id AND bookyear_id = :bookyear_id '
      @entry_pages, @entries = paginate( :entries, :per_page => 10, :conditions => [conditions.join(' AND '), search_terms.merge({:user_id => session[:user_id], :bookyear_id => session[:bookyear_id]}) ] )      
    end 

Here, the search_terms hash is merged with a “permanent” hash.

Ajax Gotchas

Ajax is cool. In fact, so much so that you're tempted to use it everywhere. Don't! Ask yourself whether you really need it. If you do need it, then avoid these common pitfalls:

  1. Do not call a remote_function which must update a DOM element that appears later in the document. In other words: put your remote_function(:update ⇒ “someElement”, …) call after your <div id=“someElement”> code.
  2. If you absolutely need to use javascript_tags(remote_function()), be careful not to retrieve content that contains the code </script>, or your remote_function call will never properly finish…
  3. remote_function does seem to work properly using the :with option. So do use it to encode a selectbox or checkbox option.

Here's an example of a checkbox that submits its “content” immediately to a remote method:

<%= check_box(
              "game", 
              "active", 
              :id => 'game_active_'+ @game.uid.to_s,
              :onchange => remote_function(
                :update => "test",
                :url => { :action => :activate_game },
                :with => "'id=#{@game.uid.to_s}&active='+ (
		                                 (this.checked) ? '1' : '0')"
              )) %>  

The :with option allows you to use javascript to create (url query) strings. Be careful with your javascript here. Use brackets to define the scope. In the example above, if you leave out the highlighted brackets, the active parameter will not be submitted, and there will be no javascript warning!

Here's another example, this one using a condition parameter for the observe_field method invocation. The condition should contain javascript code, but really only the condition part.

<%= observe_field(:user_license_id,
                  :update => 'dates',                  
                  :with => "id", 
                  :condition => "!($(user_protect_startenddates).checked)",               
                  :url => {:action => :refresh_dates},
                  :on => "click" ) %>  

This code updates a DOM node called 'dates', but only if the checkbox 'user_protect_startenddates' has not been checked.

Watch where you update to: container troubles

Ruby on Rails uses prototype.js for ajax. Prototype updates your DOM using innerHTML. This is usually cool, but not everywhere and always, especially not in MS IE (versions 6 and 7 that I know of).

Consider this example:

(Do not use the following code)

    <table>
      <thead>
        <tr>    
          <th>Student</th>
          <th>
            Project:
            <%= select("result", "projects", Project.find(:all, :conditions => ['deleted = 0 AND hidden = 0']).collect {|p| [ p.label, p.uid ] }, { :prompt => " [Student's current project] ", :order => :label }) %>                
          </th>
          <th>Score</th>    
        </tr>
      </thead>      
      <tbody id="results_container">
        <tr>
          <td colspan="3">Generic content</td>
        </tr>        
      </tbody>
    </table>

(Do not use the above code)

The idea here was to generate the content for results_container on the server and then send it back using Rails/Ajax. All went well on our trusted browser Firefox, but MS IE 6 and 7 fail miserably here.

Apparently MS IE does not allow you to expand the content of a tbody element using innerHTML.

How Do I Display the Contents of flash[:notice] through Ajax?

The lazy answer is of course: by outputting it in the partial that you retrieve through ajax. But what if your flash box is not within reach of the partial? Let's say your default place for outputting the flash messages is a shiny box which is part of your default application view.

The smart answer is to have the partial update the flash box. Example of a default view:

  <div id="flash_notice"><%= flash[:notice] %></div>

  <div id="accountListContainer">                  
    <%= render :partial => 'admin/account/ajax_list', :locals => {:person_id => @person.id} %>
  </div> 

Now, in your controller which handles updates for the partial, use the flash[:notice] array to store messages.

In your partial, use javascript to update the contents of the flash box:

  <script type="text/javascript">
    $('flash_notice').innerHTML = '<%= flash[:notice] %>';  
  </script>

A more elaborate approach can be found here: AJAX validation on Rails. And the Simplistic Complexity blog has yet another way of displaying error messages through Ajax.

form_remote_tag submit by Javascript

The title for this section was stolen from suite75.net. Here's the question: How do I submit a “remote form” using javascript?

<a href="#"            
  onclick="if (checkAnswer()) {
             af = $('answer_form_id'); if (af.onsubmit()) {af.submit();}
           } return false;"
  onmouseout="MM_swapImgRestore()"
  onmouseover="MM_swapImage('okbutton','','/images/forest/ok_over.gif')">
  <img
    name="okbutton"
    border="0"
    src="/images/<%= @design %>/ok.gif" 
    title="Check your answer!"
    alt="Check your answer!"/> 
</a>

Loading a Partial Through Ajax

Sometimes you need access to a completely separate controller to render a partial. Use ajax:

<div id="comments"></div>
<%= javascript_tag(remote_function(:update => "comments",
                                   :url => {:controller => "comments", 
                                            :action => "list" })) %>

And in your controller:

  def list
    render :partial => "list"    
  end

File uploads with FileColumn plugin

Use the plugin FileColumn to upload files, e.g. images:

http://www.kanthak.net/opensource/file_column/index.html (Warning: it seems as though the latest version is only available through subversion)

For all the image related operations, file_column relies on rmagick. See e.g. geometry string configuration options.

This plugin is pretty well documented (see also the rdoc files inside the vendor plugin directory), but it's difficult to find out how to configure file storage locations. Here is how.

File upload directory

In your model class, you identify an attribute which should be used to store urls as follows:

class Match < ActiveRecord::Base
   file_column :item_image
end

In this example, the attribute “item_image” will used to store urls. If you use FileColumn to upload files, the file is saved in a location, by default under 'public'. FileColumn takes care of this all, but if you want to change the defaults, you should know this:

- If you change the storage location, you must separately change web_root:

file_column :item_image, 
            :root_path => File.join(RAILS_ROOT, 'public/images/uploads'), 
            :web_root => 'images/uploads/'

Delete an Uploaded File

If you need to get rid of the uploaded file, you can always delete the entire record that stores the whereabouts of the file. The FileColumn plugin will take care of everything.

But of course, you do not always want to chop off your head when you've got a headache. So, here's how to just delete the uploaded file from your file system, and then delete the reference to the file from the record.

  def delete_uploaded_file
    if (@quest_text = QuestText.find(params[:id]))
      if File.delete(@quest_text.reward_image)
        # following is crude method to prevent SQL injection
        uid = params[:id].to_i                
        QuestText.connection.update("UPDATE quests_texts SET reward_image = '' WHERE uid = " + uid.to_s)       
        flash[:notice] = 'The file was deleted.'
      else
        flash[:error] = 'The file could not be deleted.'        
      end 
      redirect_to :action => 'edit', :id => params[:id], 'text_id' =>  @quest_text.text, 'quest_id' => @quest_text.quest                     
    end 
  end 

Someone pointed out that you can also set the file_column field for the model object to nil, and then save the object. The associated file will be deleted automatically.

Copy The Image when Cloning a Model Object

There is a solution posted on Craig Ambrose's log, but it requires an additional hack to the vendor/plugins/file_column/lib/file_column.rb file, which is documented on a blog called blog.skrdla.net.

My own solution does not require any hacks to the file_column plugin. It relies on copying the image to the right directory. Here's an example:

      @destination_meaning = @meaning.clone
      @destination_meaning.word_id = @destination_word.id
      @destination_meaning.save

      
      if @meaning.user_image and @meaning.user_image != ''
        File.makedirs(File.join(RAILS_ROOT, "public/images/uploads/meaning/user_image/#{@destination_meaning.id}") )
        if not File.copy(@meaning.user_image,@destination_meaning.user_image)
          flash[:notice] = "Something went wrong while copying the image."
          redirect_to :controller => 'school_wordpad',:action => 'show_word', :id => params[:word_id]   
          return                  
        end
      end

Update: Craig's solution does work with the file_column version as derived from subversion (as opposed to the 0.3.1 tar ball posted on the file_column website).

Here's a small improvement of Craig's clone method:

  alias :original_clone :clone # Ruby allows us to 'rename' the clone method (here: to original_clone)     
  def clone    
    result = self.original_clone         
    result['image'] = nil # we can't use result.image, we have to access the 'raw' value of this attribute 
    unless image.nil?
      result.image = File.open(image)
      FileUtils.cp(image, result.image)
    end
    result
  end

Tool tip (or title attribute) and other html options

The API talks about “html options” all the time, without specifying anywhere what they are. Turns out you can just use the appropriate html attributes, coded as Ruby “symbols”. E.g.:

<%= link_to “Edit”, {:controller => 'complete_text', :action => 'edit', :id => @complete_text.uid, :quest_id => @quest_id, :selected_paragraph => @paragraph.uid}, :title => "Edit this record" %>

This code outputs a link with a title attribute: title=“Edit this record”.

Sum or add up array elements

Place this code in your Rails application's /config directory:

class Array
  def sum
    if block_given?
      inject(0) { |m, v| m + yield(v) }
    else
      inject(0) { |m, v| m + v }
    end
  end
end

This code does not redefine the Array class, as it may seem to the Ruby novice, but it rather adds a sum method to it.

Here is how you use it, given an array of models SmallTransaction ('transaction' seems to be a reserved keyword in Rails), which contains the attribute credit:

@credit_total = ( (not @small_transactions.nil?) 
   and (not @small_transactions.empty?) ) ? @small_transactions.sum(&:credit) : 0 ;

Join model or association class using has_many and :through

True, you can use has_and_belongs_to_many to define a relationship between two associated tables. But, as excellently explained here: http://blog.hasmanythrough.com/articles/2006/04/20/many-to-many-dance-off, you won't get a primary key with your join table. Which means that you cannot properly base a join model on this table.

Instead, use has_many and :through. Here's an example:

CREATE TABLE `transactions` (
  `id` int(11) NOT NULL auto_increment,
  `entry_id` int(11) NOT NULL default '0',
  `book_id` int(11) NOT NULL default '0',
  `user_id` int(11) NOT NULL default '0',
  `debit` double NOT NULL default '0',
  `credit` double NOT NULL default '0',
  PRIMARY KEY  (`id`),
  KEY `entry_id` (`entry_id`),
  KEY `book_id` (`book_id`),
  KEY `user_id` (`user_id`)
)
class Book < ActiveRecord::Base 
  has_many(
    :entries,
    :through => :small_transactions,
    :conditions => ['transactions.hidden = 0'])
  has_many :small_transactions     
end
 
class Entry < ActiveRecord::Base  
  has_many(
    :books,
    :through => :small_transactions,
    :conditions => ['transactions.hidden = 0'])
  has_many :small_transactions  
end
 
class SmallTransaction < ActiveRecord::Base
  #Apparently, Transaction is a reserved word within Rails
  set_table_name "transactions"  
  belongs_to :entry
  belongs_to :book   
end

Any conditions on retrieving records from the join table should be specified in the joined classes, not in the join class itself. Do not forget to prefix the columns with the join table name, when specifying the conditions.

Submit a serialized form using Ajax

There are various standard methods for submitting a serialized form using Ajax, such as submit_to_remote. These methods are bound to standard controls, however, such as a submit button.

If you ever need to submit a form using a non-standard control, for instance a select box, use the Prototype javascript library (http://script.aculo.us/) to serialize the form. Here is an example of a select box which submits the form where it resides.

<%= select 'entry', 'action_id', @actions.collect{ |list|  [ list.label, list.id]  },
   { :include_blank => true},
   { :class => "formfield",
     :onchange => "alert('combine with more Javascript');" + remote_function(
       :update => 'test',
       :url => url_for(:action =>  'test'),
       :with => 'Form.serialize(this.form)'
     )
   }
%>

Testing with Functional and Unit Tests

To see what instruments are available, see this website: http://ar.rubyonrails.org/classes/Fixtures.html. The book Agile Webdevelopment with Rails describes how to use these testing instruments. This book describes the use of instantiated fixtures. As an example, let's say we've got this fixture:

wordsweb:
  id: 1
  date: "2006-01-01 00:00:00"
  description: "Lot of Words"

Then we can state this in our unit test:

assert_equal @wordsweb.id, @entries["wordsweb"]["id"]

However, by default instantiated fixtures are turned off. Turn them on in /test/test_helper.rb:

self.use_instantiated_fixtures  = true

Testing with Irb or Console

Of course, you can also do some informal testing with irb (the one-line ruby interpreter) or the Rails console. In the main directory of your web application, use the command script/console to start the console. Now you have access to e.g. all your ActiveRecord models.

script/console
Loading development environment.
p = Person.find(:first)
=> #<Person:0xb6fc824c @attributes={"city"=>nil, "subdepartment_id"=>nil, "nationality_id"=>nil, "middlenames"=>nil, "title"=>nil, "jobdescription"=>nil, "birthdate"=>nil, "phonework"=>nil, "nickname"=>nil, "lock_version"=>"0", "country_id"=>nil, "initials"=>nil, "prepart"=>nil, "streetnumber"=>nil, "gender"=>nil, "id"=>"1", "mobile"=>nil, "phonehome"=>nil, "postalcode"=>nil, "organization_id"=>"2", "fax"=>nil, "street"=>nil, "department_id"=>nil, "first_name"=>"_", "last_name"=>"Demogebruiker", "profession"=>nil, "email"=>nil}>
>> puts p.last_name
Demogebruiker
=> nil
>> 

Default Values For Your Model

In Ruby, it's quite easy to set up an object with default values. Here's a basic example, taken from Programming Ruby:

class Song
  def initialize(name, artist, duration)
    @name     = name
    @artist   = artist
    @duration = duration
  end
end

But for an active record model class, this won't work – you'd have to completely rewrite the initialize method, and then add your own stuff to it. There is an easier way, however: just use the after_initialize callback method. Here is an example taken from A Single Programmer's Blog:

class Site < ActiveRecord::Base
  def after_initialize
    self.attribute = default_value unless self.attribute
  end
end

Access Denied: Filter Based Authorization

It is quite common to use a before_filter to secure controllers. E.g.:

before_filter :authorize, :except => :login

The :except option excludes the login action from being filtered by the before_filter.

Using multiple filters can be tricky, however. Consider the following example:

  before_filter :authorize, :check_usergroup, :except => :login

  def authorize
    unless session[:user_id]
      flash[:notice] = "Please log in"
      redirect_to(:controller => "login", :action => "authorize")      
    end
  end

  def check_usergroup
    # groups_allowed is an array of usergroup ids    
    unless self.groups_allowed.include?(session[:usergroup_id])
      flash[:notice] = "You don't have access to the page you wanted to visit."
      request.env["HTTP_REFERER"] ? (redirect_to :back) : (redirect_to HOME_URL)      
    end  
  end

Even if the first filter applies, i.e. you are redirected to the you to login/authorize, the filter chain is still processed. In other words, the second filter is also applied, which has a redirect as well! This will result in a ActionController::DoubleRenderError error.

So, you need to stop the before_filter chain if something goes wrong in the first filter. You do this by simply returning false:

  def authorize
    unless session[:user_id]
      flash[:notice] = "Please log in"
      redirect_to(:controller => "login", :action => "authorize")      
      return false
    end
  end

Personal Tools