has_wiki_field: quickly building a wiki in Ruby on Rails

Posted on December 16th, 2007 in Nerdy,ruby,ruby on rails by toholio

Has the boss just discovered wikis? Does he now want to be able to create links between records in your intranet applications? Need to quickly add wiki functionality to your models and views so you can get back on with your life?

That’s what happened to me recently. Since the wiki code was almost exactly the same for all of the applications that needed changing, I’ve put the code into a plugin. Hopefully someone else will find it useful.

Installing the plugin

The plugin is installed in the same manner as any other. Fire up your favourite terminal and navigate to the root of your rails application. Then run the following command:

script/plugin install http://hazy.stupor.org/svn/has_wiki_field/trunk/has_wiki_field/

That should be all that is required.

Using the plugin

The following example should get you up and running quickly but make sure you read the efficiency note below.

Lets say you have a Person model that has name and bio attributes. We would like to be able to link to other people from within a bio. So in Allan’s record we might want the bio to read “This man is friends with [[Betty]]” and have [[Betty]] automatically change into a link to her record when it is displayed.

We, start by adding has_wiki_field to the Person model:

class Person < ActiveRecord::Base
  has_wiki_field :field => "name", :sql_match => "LIKE"
end

The :field option specifies what field should be matched by the text in [[links]]. The :sql_match option specifies how the records should be matched. :sql_match defaults to “=” but using “LIKE” will allow case insensitive links.

Now we need to change show.html.erb for the people controller so that links are generated from the bio.

Where previously we might have displayed a bio using:

<%= h @person.bio %>

We will now use the wikify method, passing it the name of the attribute to wikify and a code block which generates the html for links. A code block is used to allow flexibility in link generation. The following block simply calls link_to but you could, for example, write a block which looks up an image associated with your model and displays that instead.

Anyway, for our Person model we add the following to the view:

<%= @person.wikify(:bio) do |name, object|
    if object.nil?
      h name
    else
      link_to name, object
    end
  end
%>

In a real application this should be put into a method in people_helper to help keep things tidy.

That’s it! Where [[Some Name]] appears in a Person’s bio it will be turned into a link to the person with the name ‘Some Name’.

Remember to read the important efficiency note section below before using this in a production environment.

An important note about efficiency

The default method used for finding which object a [[link]] points to is not efficient. For sites that receive more than a tiny amount of traffic it should be replaced by your own model specific method.

The default method takes the contents of each [[link]] and queries the database for the first record that has ‘link’ as the value for the field specified in the model, I.e.

find(:first, :conditions => ["somefieldname = ?", key])

This will obviously require a database look-up for every link which could slow your site down tremendously for pages containing many links.

To fix this you must provide your own method to match objects with the keys extracted from [[links]]. The class method to override for this is object_for_key(key).

For example, if we have a class called Person we might replace the object_for_key method like so:

class Person < ActiveRecord::Base
  has_wiki_field
 
  def self.object_for_key( key )
    # some super fast method for matching the key
    # from a link to a person object, goes here...
    #
    # return the Person object that matches or nil if there is no match
  end
end

Naturally, it’s up to you to manage the data used by your object_for_key method. wiki_options[:field] and wiki_options[:sql_match] will be available to you in this method if they were set as options to has_wiki_field.

Future plans

The plugin doesn’t do much currently. If I’d only needed it for a single application there’s a good chance it wouldn’t have made it into a plugin at all. It has provided me with a reasonable starting point for my applications and hopefully it’ll get better as time goes on.

Some ideas for future versions:

  • Add a controller action which takes a key and redirects to the appropriate record or creates a new record with the needed field already filled.
  • Using the new controller method above, allow links of the form [[Some Key#Model]] where “Model” specifies what type of thing links are pointing to. Ta-dah! Now we can link between all the models in an application.
  • Implement some basic object_for_key methods for common situations. The plugin should attempt to gently direct programers towards these methods.
  • Implement a basic link caching mechanism. When a has_wiki_field model is saved links should be extracted and stored in a table. Then all current links for a model can be loaded in a single database transaction.

Generating an iCalendar feed from a controller in Ruby on Rails

Posted on December 11th, 2007 in Nerdy,ruby,ruby on rails by toholio

Recently I needed to set-up an application to keep track of regular safety inspections for power tools and other equipment. The application is very trivial but my client was quite pleased with one particular feature which wasn’t actually requested.

The “bonus” feature was an iCalendar feed which allows the client to have his computers subscribe to the feed and keep up to date with what equipment needs inspecting and when. Very swish and, thanks to Rails and a nice gem, very easy.

So how’s it done? Easy!

Lets start by installing the icalendar gem. Open a terminal and install it as normal:

user@host railsapp$ gem install icalendar

Change to the vendor directory of your Rails application and unpack the gem:

user@host railsapp$ cd vendor/
user@host vendor$ gem unpack icalendar

Make sure it will be loaded by adding it to the end of config/environment.rb in your Rails application:

require 'icalendar-1.0.2/lib/icalendar'

Now we’re ready to add a calendar feed to our application. Lets say we have a table that contains the names and scheduled service dates of some tools. The client wants, or is going to get, a feed which marks each tool needing service in their calendar when their servicing is due.

Generating the iCalendar data is simple. The following method from tool_controller creates an event for each tool and inserts it into a calendar. The to_ical call at the end of the method get the calendar as a string which can be served to the user. Note that this method assumes the @tools variable has already been set (you could always add a call to Tool.find(:all) at the start of the method if needed).

def generate_ical
  cal = Icalendar::Calendar.new
  @tools.each do |tool|
    # create the event for this tool
    event = Icalendar::Event.new
    event.start = tool.inspection_date
    event.end = tool.inspection_date
    event.summary = "Service of " + tool.name + " is due."
 
    # insert the event into the calendar
    cal.add event
  end
 
  # return the calendar as a string
  cal.to_ical
end

Next, we add the icalendar feed to respond_to in the tools_controller’s index method. We’ll make this call the generate_ical method to get the calendar data.

def index
  @tools = Tool.find(:all)
 
  respond_to do |format|
    format.html # index.html.erb
    format.xml  { render :xml => @tools }
    format.ics  { render :text => self.generate_ical }
  end
end

And now you should be able to open your calendar application and subscribe to the feed straight from the tools controller using a URL like http://example.com/tools.ics

Easy filtering by optional criteria in Rails applications

Posted on December 11th, 2007 in Nerdy,ruby,ruby on rails by toholio

I’ve frequently needed a very simple search method that allows partial matches for any combination of fields in a table. The following snippet shows how an ActiveRecord derived object might have a filter to allow for selection of records given a set of partial values.

class YourClass < ActiveRecord::Base
   def self.filter( partial_values )
    # don't bother at all if there is no search object
    return find(:all) unless partial_values
 
    con_string = ""
    con_array = []
 
    # build collection of conditions
    partial_values.each do |key,value|
      if value != "" then
        con_string += " and " if con_array.size > 0
        con_string += "#{key} LIKE ?"
        con_array << "%#{value}%"
      end
    end
 
    # construct the actual conditions array
    conditions = [con_string]
    con_array.each { |item| conditions << item }
 
    find(:all, :conditions => conditions)
  end
end

To use this you would obtain a set of search parameters, one for each filterable column, and pass it to YourClass.filter as a hash to get the matching rows.

So if you had a table with title and category columns you might create a page containing a form to collect partial value for filtering. When creating the form, assuming you will use Rails’ form helpers, the fields_for :collection function is nice as it will allow for easy collection of a hash for the field values.

<% form_tag your_object_path, :method =>"get" do %>
	<% fields_for :partial_values do |f| %>
		Title contains: <%= f.text_field :title %><br />
		Category contains: <%= f.text_field :category %><br />
	<% end %>
	<%= submit_tag "Filter items", :name => nil %>
<% end %>

Then once the form is submitted you would get the appropriate results in your controller using:

@your_objects = YourClass.filter( params[:partial_values] )