======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' %>
<% end %>
#but this:
<% @answers.each do |@answer| %>
<%= text_field 'answer', 'description' %>
====Button as a link====
If you don't want your button to submit a form, use the ''**url_for**'' method:
<% end %>
=====Localization with Globalize=====
====Installation and configuration====
Installing Globalize is pretty straightforward. See the documentation online:
[[http://www.globalize-rails.org/wiki/|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/|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:
This outputs:
<%= @account.logdate.localize("%d %B %Y") %>
Laatst ingelogd
18 januari 2006
Note that dates can be formatted using the formatting options for the [[http://www.ruby-doc.org/core/classes/Time.html#M000249|strftime method]] of the Ruby Time class.
====Localizing Error Messages====
This is from: [[http://spongetech.wordpress.com/2006/12/24/error_messages_for-you/|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
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|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") %>
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|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|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|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 [[http://www.ruby-doc.org/core/classes/Time.html#M000202|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: [[http://typo3.org|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:
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 [[http://datebocks.inimit.com/|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|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/HowtoPagination/]]
[[http://wiki.rubyonrails.com/rails/pages/PaginationHelper|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:
-The user types in search criteria, in a form residing in an ordinary view. The criteria are submitted to a controller.
-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**''.
-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.
-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 %>
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|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:
<%= link_to 'New account', :action => 'new' %> | <%= link_to 'Search', :action => 'search' %>
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:
-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 ''**
<%= 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)
Student
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 }) %>
Score
Generic content
(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:
<%= flash[:notice] %>
<%= render :partial => 'admin/account/ajax_list', :locals => {:person_id => @person.id} %>
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:
A more elaborate approach can be found here: [[http://www.bigsmoke.us/ajax-validation-on-rails/|AJAX validation on Rails]]. And the [[http://www.simplisticcomplexity.com/2007/11/05/display-validation-errors-for-your-ajaxified-form/|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 [[http://www.suite75.net/blog/dev/form_remote_tag-submit-by-javascript.html|suite75.net]]. Here's the question: How do I submit a "remote form" using javascript?
==== Loading a Partial Through Ajax ====
Sometimes you need access to a completely separate controller to render a partial. Use ajax:
<%= 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|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. [[http://www.simplesystems.org/RMagick/doc/imusage.html#geometry|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 [[http://blog.craigambrose.com/past/2006/11/28/copyin-files-between-models-with-file_column/|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 [[http://blog.skrdla.net/2007/09/rails-and-filecolumn-plugin-copy.html|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|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/|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|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)
=> #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 [[http://www.rubycentral.com/book/tut_classes.html|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 [[http://m.onkey.org/2007/7/24/how-to-set-default-values-in-your-model|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 [[http://blog.teksol.info/articles/2005/12/14/setting-default-values-in-models|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