Trace: • the_basics
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:
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:
- 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 %> <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:
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:
- Do not call a
remote_function
which must update a DOM element that appears later in the document. In other words: put yourremote_function(:update ⇒ “someElement”, …)
call after your<div id=“someElement”>
code. - 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… 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
You are here: start » ror » the_basics