Ruby: Checking if a string is all letters

•June 4, 2009 • Leave a Comment

Looking at the stats on my previous post, I noticed a lot of people are wrongly redirected when they’re looking for a solution to check if a ruby string is all letters. For those, I’d like to post my solution, which is imo a clean way of doing it:


def all_letters(str)
	# Use 'str[/[a-zA-Z]*/] == str' to let all_letters
	# yield true for the empty string
	str[/[a-zA-Z]+/]  == str
end

Analogous, we can do this with digits as well:


def all_digits(str)
	str[/[0-9]+/]  == str
end

def all_letters_or_digits(str)
	str[/[a-zA-Z0-9]+/]  == str
end

Of course, it’s even better if you make these methods a core extension of String itself :)

Successor: Ruby String.succ For Java

•May 26, 2009 • Leave a Comment

For my master thesis I’m working on a Java application that should allow you to specify string intervals. A string interval can be specified by defining a start string and an end string, from which the system should be able to generate a list of successive strings in lexicographical order.

Knowing of the function string.succ in Ruby and the ability to specify string ranges, I figured that something similar would also exist in Java. After browsing the webs for hours, I didn’t find anything however. Moreover, even C# with all its extra convenience methods doesn’t come with this (see also Mack Allen’s post here)!

The code below is my stab at creating string.succ in Java. It (mostly) does exactly the same thing as the Ruby version. Mostly I say, try for example the range ("test1".."test10") in irb and Successor.range("test1","test10",false) in a java console.

See the main() function for examples!

Update 5-6-2009: Made it a lot shorter by getting rid of the helper classes, which turned out to be overkill.


/**
 * Successor.java: A utility class for generating string successors.
 *
 * Copyright (c) 2009, R de Lange
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */
import java.util.*;
import java.io.*;

/**
* The Successor utility class can be used to find string successors or
* to generate lists of successive strings.
*
* @author    R de Lange
* @created   June 5, 2009
*/
public class Successor {

	/**
	*	Main class to test Successor. Examples taken from Ruby string#succ.
	*
	*	Call						should yield
	*	Successor.succ("abcd")      #=> "abce"
	*	Successor.succ("THX1138")   #=> "THX1139"
	*	Successor.succ("<<koala>>") #=> "<<koalb>>"
	*	Successor.succ("1999zzz")   #=> "2000aaa"
	*	Successor.succ("ZZZ9999")   #=> "AAAA0000"
	*	Successor.succ("***")       #=> "**+"
	*
	*	- For an interactive console interface for testing Successor.succ(str), specify the -i argument
	*	- To test successor ranges using Successor.range(str), specify the -r argument. Adding an arbitrary
	*	extra argument (e.g. -r e) will toggle the 'exclusive' boolean for successor ranges.
	*/
	public static void main(String[] args) throws Exception{
		if(args.length == 0){
			System.out.println("Running Successor examples...");
			System.out.println("abcd #=> " + Successor.succ("abcd"));
			System.out.println("THX1138 #=> " + Successor.succ("THX1138"));
			System.out.println("<<koala>> #=> " + Successor.succ("<<koala>>"));
			System.out.println("1999zzz #=> " + Successor.succ("1999zzz"));
			System.out.println("ZZZ9999 #=> " + Successor.succ("ZZZ9999"));
			System.out.println("*** #=> " + Successor.succ("***"));
		}
		else {
			BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
			String command = args[0];
			if(command.equals("-i")) {
				String line = "";
				while(!line.equals("quit")){
					System.out.print("String? ");
					line = in.readLine();
					if(!line.equals("quit")) System.out.println(Successor.succ(line));
				}
			}
			else if(command.equals("-r")) {
				String left = "", right="";
				while(true){
					System.out.print("Left? ");
					left = in.readLine();
					System.out.print("Right? ");
					right = in.readLine();
					System.out.println(Arrays.deepToString(Successor.range(left, right, args.length == 2).toArray()));
				}
			}
		}
	}

	/**
	* Returns the successor to str. The successor is calculated by incrementing characters starting from the rightmost
	* alphanumeric (or the rightmost character if there are no alphanumerics) in the string. Incrementing a digit always
	* results in another digit, and incrementing a letter results in another letter of the same case. Incrementing
	* nonalphanumerics uses the system default character set's collating sequence. If the increment generates a 'carry',
	* the character to the left of it is incremented. This process repeats until there is no carry, adding an additional
	* character if necessary.
	*/
	public static String succ(String str){
		char[] cstr = str.toCharArray();
		if(cstr.length == 0) return "";

		int letterOrDigitIndex = getLastLetterOrDigitIndex(cstr, cstr.length-1);
		if(letterOrDigitIndex >= 0)
			return alphanumSucc(cstr, letterOrDigitIndex);
		else {
            // Increment rightmost character
            cstr[cstr.length-1] = (char) (cstr[cstr.length-1] + 1);
            return new String(cstr);
        }
	}

	/**
	* Alias of Successor.succ(str)
	*/
	public static String next(String str){ return Successor.succ(str); }

	/**
	* Returns a range of string successors as a list. Starting from str1, this method will add all successors
	* to a list until the successor string equals str2. If the 'exclusive' boolean is true, str2 will be included
	* in the resulting list, otherwise excluded. If the successor str2 is not found after generating half a million
	* successors, the range is assumed to be invalid, in which case it will return an empty list.
	*/
	public static List<String> range(String str1, String str2, boolean exclusive){
		int counter, limit = 500000;
		ArrayList<String> result = new ArrayList<String>();

		result.add(str1);
		String currentSuccessor = str1;
		for(counter = 0;counter<=limit && !currentSuccessor.equals(str2); counter++){
			currentSuccessor = Successor.succ(currentSuccessor);
			if((currentSuccessor.equals(str2) && !exclusive)|| !currentSuccessor.equals(str2))
				result.add(currentSuccessor);
		}
		if(counter > limit) {
			System.err.println("Range could not be created, level too deep.");
			result.clear();
		}
		return result;
	}

	/**
	* Calculates the successive string for strings that contain alphanumeric characters.
	* See the description of Successor.succ(str) for more informtionon this function
	*/
	private static String alphanumSucc(char[] cstr, int lastLetterOrDigitIndex){
		char succ = charSucc(cstr[lastLetterOrDigitIndex]);

		int currentIndex = lastLetterOrDigitIndex;
        cstr[currentIndex] = succ;

		// Loop while we have a carry
		while(succHasCarry(succ) && lastLetterOrDigitIndex >= 0){
			lastLetterOrDigitIndex = getLastLetterOrDigitIndex(cstr, lastLetterOrDigitIndex-1);
			if(lastLetterOrDigitIndex >= 0) {
				currentIndex = lastLetterOrDigitIndex;
				succ = charSucc(cstr[currentIndex]);
				cstr[currentIndex] = succ;
			}
		}

		// Check if we still have a carry left
		if(succHasCarry(succ)) cstr = insertCarry(cstr, succ, currentIndex);

		// Finished!
		return new String(cstr);
	}

	/**
	* Create a new array in which the given carry is inserted at the given index
	* @return A new char array of length cstr.length+1, which contains all values of
	* cstr + the carry char inserted at the given index.
	*/
	private static char[] insertCarry(char[] cstr, char carry, int index){
		char[] result = new char[cstr.length+1];
		System.arraycopy(cstr,0,result,0,index+1);
		result[index] = carry;
		System.arraycopy(cstr,index,result,index+1,cstr.length);
		return result;
	}

	/**
	* Determines whether the given char array contains alphanumeric characters
	* @return the highest index at which an alphanumeric character can be found, or -1 if none were found
	*/
	private static int getLastLetterOrDigitIndex(char[] cstr, int lastIndex){
		int result = -1;
		for(int i=lastIndex;i>=0 && result < 0;i--) {
			if(Character.isLetterOrDigit(cstr[i])) result = i;
		}
		return result;
	}

    /**
     * Determines if a given *successor* character (returned by charSucc(char c)) is a
     * character with a carry.
     *
     * @param c - the successor char
     * @return true if c is '0', 'a' or 'A'.
     */
    private static boolean succHasCarry(char c){
        return c == '0' || c == 'a' || c == 'A';
    }

    /**
     * Determines the successor of the given char
     * @param c - the char
     * @return the successor of char c
     */
    private static char charSucc(char c) {
        char result;

        switch(c) {
            case '9': result = '0'; break;
            case 'z': result = 'a'; break;
            case 'Z': result = 'A'; break;
            default: result = (char) (c + 1);
        }

        return result;
    }
}

Executing smbstatus as non-root user

•February 16, 2009 • 2 Comments

I found that after upgrading to samba 3 (Debian 2.6.15 unstable), I was unable to execute smbstatus as a non-root user. This was very inconvenient, since I was using a PHP script to track which users are connected to my server (see my previous post). The error that occurs looks as follows:

ERROR: Failed to initialise messages database: Permission denied
messaging_tdb_init failed: NT_STATUS_ACCESS_DENIED
messaging_init failed

Making smbstatus suid root unfortunately didn’t do the trick. Taking a closer look at the error suggested that it got to do with the *.tdb files that samba is using. So, I went to the folder containing these files (for me /var/run/samba/*.tdb). All these database files were chmodded to 644, so only enabled rw permissions to root.

By changing the permissions of messages.tdb to 666 (rw permissions on world, if you find this to devil-ish, 646 would also do the trick ;) ) a normal user can also open the database file and thus everything worked again. Of course, you should keep in mind that this may pose a security risk.

Monitoring Samba from a web page using smbstatus

•December 14, 2008 • Leave a Comment

A long time ago, I found that I needed something to be able to see which users are logged in using samba and kick users as necessary. The usual way is to check the currently logged in users using smbstatus and kicking users by killing the smb process belonging to a user. To make this a bit easier, I made a small webpage in *cough*PHP*cough*. Hey, it was 2 years ago! :) Anyway, I still find it very useful, so maybe it’s useful to you, too.


<?
error_reporting(0);

if(isset($_GET['kill'])){
	echo shell_exec("sudo /scripts/smbkill ".$_GET['kill']." 2>&1");
	}

exec("smbstatus -S",$log);
exec("smbstatus -L",$lock);

$pid_us = array();
for($i=3;$i<sizeof($log);$i++)
	if(!trim($log[$i])=="") {
		$users[] = split("[ ]+",$log[$i],4);
		$pid_us[$users[sizeof($users)-1][1]] = $users[sizeof($users)-1][2];
		}
for($i=3;$i<sizeof($lock);$i++)
	if(!trim($lock[$i])=="") {
			$line = split("[ ]+",$lock[$i],7);
			$usr = $pid_us[$line[0]];
			if(trim($usr)=="") $usr = $line[0];
			$locks[$line[0]][] = array($line[0],$usr,substr_replace($line[6],"",-25));	//Remove date (25 chars from the right
		}

//die("");

echo "<br />";
echo "<font style=\"font-family: Verdana;\">Connected users:</font>\n";
echo "<br /><br />";
echo "<table>\n";
foreach($users as $user){
	list($service,$pid,$mach,$date) = $user;
	echo "<tr\n>";
	echo "<td><a onclick=\"return confirm('Kick this user?');\" href=\"".$PHP_SELF."?kill=".$pid."\"><img border=\"0\" src=\"remove.gif\" /></a></td>\n";
	echo "<td class=\"user\">".$mach."</td>\n";
	echo "<td class=\"pid\">".$pid."</td>\n";
	echo "<td>".$service."</td>\n";
	echo "<td>".$date."</td>\n";
	echo "</tr>\n";
}
echo "</table>\n\n";

echo "<br /><br />";
echo "<font style=\"font-family: Verdana;\">Locked files:</font>\n";
echo "<br /><br />";
echo "<table>\n";

$cuser = "";
$first = true;
foreach($locks as $l){
	foreach($l as $lck){
		list($pid, $user,$file) = $lck;
		if($user != $cuser){
			if(!$first){
				echo "<tr>\n";
				echo "<td>&nbsp;</td>\n";
				echo "<td>&nbsp;</td>\n";
				echo "<td>&nbsp;</td>\n";
				echo "</tr>\n";
				}
			echo "<tr>\n";
			echo "<td class=\"pid\"><a onclick=\"return confirm('Kick this user?');\" href=\"".$PHP_SELF."?kill=".$pid."\"><img border=\"0\" src=\"remove.gif\" /></a> ".$pid."</td>\n";
			echo "<td class=\"user\">".$user."</td>\n";
			echo "<td>".$file."</td>\n";
			echo "</tr>\n";
			$cuser = $user;
			}
		else {
			echo "<tr>\n";
			echo "<td>&nbsp;</td>\n";
			echo "<td>&nbsp;</td>\n";
			echo "<td>".$file."</td>\n";
			echo "</tr>\n";
			}
		$first = false;
		}
}
echo "</table>\n";
?>

Note that you should of course make sure that only you are able to access the page and execute the kill command, so you’ll need to get creative with sudo and htaccess ;)

Image used: remove

Sorting IMAP Mail with Imapfilter

•November 18, 2008 • 2 Comments

The situation is as follows: I have multiple IMAP mail accounts on multiple servers and access these mailboxes from multiple computers using different e-mail clients. Per mailbox, I have multiple folders that contain messages regarding a certain subject. To be sure that new mail appears in the folder it belongs to, I have set up message filters on every e-mail client. The trouble is, for every new folder I had to alter the filters on every client.

As a solution, I left one computer always running with Thunderbird active, but since I already have a linux server running (terminal access only), I was thinking whether there was something available that was able to sort my messages, which I can alter from the command line and doesn’t involve a graphical frontend.

Imapfilter did the job for me. Although I don’t have any experience in Lua, in which it is written, I managed to successfully sort my mail. You can install it from source, but there is also a Debian package available from the apt (at least, from repository unstable). Below I’ll dscribe how I configured imapfilter, since there is not much information around on this (there are however some small examples described in a sample config file).

After install, first run imapfilter as a normal user. This will create the directory ~/.imapfilter with appropriate permissions. Enter that directory and create a new file named config.lua. In this file, you should first define some global options, then your IMAP accounts, followed by the filters (rules) that should be applied to the messages in your mailboxes.

The global options are defined in a lua table variable named options. A table in lua is much like a hash in Ruby. I have defined the following options:


  options.timeout = 120
  options.subscribe = true

The meaning of these options is described on the imapfilter homepage.
Definitions of IMAP accounts look like this:


account1 = IMAP {
	   	server = 'imap.example.com',
		username = 'user',
		password = 'pass',
		ssl = 'ssl3'
	}

account2 = IMAP {
	   	server = 'imap2.example.com',
		username = 'user',
		password = 'pass'
	}

As you can see, imapfilter also supports SSL (you need to have OpenSSL installed though). You can define all your IMAP accounts like this.

Next, I define the sorting rules. Note that I mostly move messages from the inbox of an account to a folder of the same account. This is however not necessary, it is also possible to transfer messages from one account to the other. Example 1:


   msgs = account1.INBOX:contain_from('MediaPortal')
   account1.INBOX:move_messages(account1.MediaPortal, msgs)

The first example is simple: find all messages in the INBOX in which the sender contains the name MediaPortal and move those messages to the MediaPortal folder.
Now, a more complex example:


 msgs = 	account2.INBOX:contain_from('Cron') +
		account2.INBOX:contain_from('Log') +
		account2.INBOX:contain_subject('yum')
 account2.INBOX:mark_seen(msgs)
 account2.INBOX:move_messages(account2['Maildir/Admin Notifications'], msgs)

What you need to know here is that the ‘+’ operator means ‘or’. So, I put all messages that are from ‘Cron’, ‘Log’ or have the subject ‘yum’ in the variable msgs, I mark all those messages as read and finally move them to the ‘Admin Notifications folder’. As you can see, this account uses the Maildir directory.

You can debug your configuration by running imapfilter -d or imapfilter -v. To run this rules frequently, I finally had to run this code in daemon mode. To achieve this, I had to enclose all the above code in a function and pass that function in the become_daemon() function:


-- global options

function sortMail()
   -- IMAP account definitions
   -- rules
end

become_daemon(300, sortMail)

Note that I had to include the account definitions in the function; it seems the accounts are also initialized when defined and it wouldn’t work anymore when these definitions were outside the function. The above example will run imapfilter as a daemon and execute the sortMail function every 300 seconds.

Some final remarks that may save you some time:

  • There are problems selecting/searching messages when you’re running an Exchange server. I tried several things to get it working properly, but nothing worked. This could be dependent on the configuration or version of the Exchange server however.
  • Be sure you add the IMAP definitions to the function called in daemon mode
  • if you don’t know exactly which folders you have or should define, you can add some debugging code to find out:

-- Inbox information
account1.INBOX:check_status()

-- Get and print available mailboxes and folders
mailboxes, folders = account1:list_all()
table.foreach(mailboxes,print)
table.foreach(folders,print)

Fore more information and examples, see also the imapfilter homepage: http://imapfilter.hellug.gr

Snippet: Binary to YAML

•November 1, 2008 • Leave a Comment

Recently, I was looking for a piece of code that could easily convert a file to YAML data. Here is what I use now. It follows the format described in http://yaml.org/type/binary.html.


def binary_to_yaml(file, indent_level = 1)
  if File.exists?(file)
    indent = ""
    indent_level.times{ indent << " " }
    data = File.open(file,'rb').read
    #YAML Generic binary representation, see http://yaml.org/type/binary.html
    "!binary |\n#{indent}#{[data].pack('m').gsub(/\n/,"\n#{indent}")}\n"
  end
end

The indent level describes how many spaces should be prepended. A binary value for a key that is unnested should have indent level 1. See the example below:


user: # indent level 1
 name: John Doe # indent level 2
 address:
  street: Sunset Blvd # indent level 3
  number: 2034
 thumbnail: <%= binary_to_yaml('files/thumb.jpg',2) %> # indent level 2

user: # indent level 1
 name: Jane Doe # indent level 2
 ...

RailsConf 2008: LowPro

•October 11, 2008 • Leave a Comment

Although my colleagues agreed RailsConf 2008 was ‘less interesting than the last time’, there were some interesting presentations I’d like to give some extra attention.

First and foremost, I was very excited about a presentation of LowPro, an extension to prototype to easily ajaxify your webapp with unobtrusive javascript. Forget about link_to_remote, submit_to_remote and remote_form_for, which yield blocks of ugly inline javascript. Instead, consider this script to send a form using AJAX:


Event.addBehavior({
  '#add_form' : function() {
    this.hide();
  },
  '#add_new_link:click' : function() {
    $('add_form').toggle();
    return false;
  },
  '#add_form' : Remote.Form
});

Using Event.addBehavior, you can add behaviors to elements specified by CSS selectors. When this piece of code is included in your static, non-javascript webapp, it will ‘hijack’ click and submit events only when javascript is enabled.
I have done something similar in a project of my own, which should update a page when a month was selected in a combobox. Note that this example does not use LowPro itself. If javascript was disabled, a button should be displayed to manually submit the form containing the combobox. So, my static page (statistics.html.erb) would look like this:


<% form_tag example_url, :method => :get, :id => 'months-form', :class => 'months-form' do %>
  <div>
	<%= select_tag :month, @available_months.collect{ |month| "<option value=\"#{month.to_s}\" #{(@curr_moneht.same_month?(month)) ? 'selected="selected"' : '' }>#{month.strftime("%B %Y")}</option>" }.join %>
	<%= submit_tag 'view', :id => 'submit-button' %>
  </div>
<% end %>

To add a dash of Javascript to this without touching this perfectly fine code, I just need to include a javascript file that should handle the form submission. Let’s call it statistics.js:


<% content_for :content_for_js do %>
   <%= javascript_include_tag 'statistics' %>
<% end %>

And finally, the javascript that should prevent manual form submission and hide the view button when javascript is available:


Event.observe(window, 'load', observeMonthField);

function observeMonthField() {
    $('submit-button').hide();
    $('month').observe('change', function(event) {
      $('months-form').submit();
    });
}

Easy peasy lemon squeezy!

Ruby Core & Ruby on Rails CHM Documentation

•August 28, 2008 • 5 Comments

Today, my company’s internet provider managed to cut off the internet connection for the whole day! This made me wonder why I always use the online available Ruby & RoR APIs, while having them offline would give me faster loading times and doesn’t require an internet connection (although that situation is probably the same as being cut off from water for most of us :) ).

Download

There is a project page on sourceforge that contains CHM documentation for Ruby 1.8.6 and Rails 2.2.

Do it yourself

Rdoc provides you with four possible formats to create your own documentation: html, xml, ri and chm.

To create the documentation for Ruby, go to the Ruby source directory (\src\ruby-x.x.x-pxxx) in the console and enter: rdoc -f [format] -o [destination-folder].

To create the documentation for Rails, go to the ruby gems directory (i.e. \lib\ruby\gems\1.8\gems) and enter: rdoc -f [format] -x test/* -x template/* -o [destination-folder] action* active* rails-* .

The extra argument -x template/* is needed here to prevent errors from occurring during compilation, as discussed here. This problem occurs for both html and chm compilation.

Update 16-12-2008: When generating the Rails documentation, a good idea is to also add -x test/* to the command, since you’ll probably don’t need the documentation of Rails’s test classes, which are included in the gems.

If you are using windows, you might like the CHM format (the Windows Helpfile format). To be able to create CHM files, you’ll need HTML Help Workshop. Install it in C:\Program Files\HTML Help Workshop\, even if you are running Windows x64.

Update 16-12-2008: A small list of locations of gem home directories:

  • Windows:
    • C:\ruby\lib\ruby\gems\1.8\gems
  • Mac OS X
    • /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/user-gems/1.8/gems (included version)
    • /usr/local/lib/ruby/gems/1.8/gems/

Content_for Sorta Nested Layouts

•August 16, 2008 • 2 Comments

For my latest project, I needed to insert a small piece of code into all views belonging to a controller. At first, I had solved this by adding a partial to every view, but I thought it would be nice to DRY it up a little more and create some sort of nested layout within the main layout. This article by Matt McCray explained how I could easily achieve this by yielding within a partial, specified by a sub_layout method in the controller. The only disadvantage I found was that content_for constructions within the nested layout were not evaluated in this way, while the small piece of extra layout had its own css and javascript, too.

In order for content_for blocks to be evaluated, it needs to be yielded. The simple solution I therefore came up with was to yield the nested-layout-partial itself from a content_for defined in the main layout:


<% content_for :with_nested_layout, render(:partial=> "layouts/#{controller.sub_layout}") %>
<html>
 <head>
  <title>Nested layout example</title>
  <%= stylesheet_link_tag 'common', 'clearfix' %>
  <%= javascript_include_tag :all %>
  <%= yield :content_for_css %>
  <%= yield :content_for_js %>
 </head>
 <body>
  <div id="main" class="clearfix">
   <%= yield_flash_messages -%>
   <%= yield :with_nested_layout %>
  </div>
 </body>
</html>

This way, I can specify content_for :content_for_css and :content_for_js in the nested layout, which are evaluated because the partial itself is yielded, too. Of course this can be considered as a ‘dirty hack’, and I do agree to some extent, but it’s only two lines of code and it works like a charm, so I forgave myself for implementing it this way :)

Wizarding multiple models

•August 9, 2008 • 2 Comments

There are cases in which you need to create multiple models at once. For example, a guest can subscribe for an account, but this should also entail that the guest will be registered as a user. As a bonus, imagine that an account also needs to have ‘billing details’, so we know where to send the subscription fee to.

James Golick has created a gem that behaves as an ActiveRecord and can ‘present’ multiple models at once. It’s called ActivePresenter. It’s fairly new (version 0.0.3), but it does the job nicely. Unfortunately, it only supports creating the presented objects at once, resulting in one huge web form. What I needed was a wizard.

There aren’t many options yet for creating a wizard in RoR. I found a plugin called acts_as_wizard and a tutorial on Ruby-coloured glasses. I liked that acts_as_wizard moves logic to the models, but saw that a) the example saves the object between pages (which could be done different though), b) it uses AASM, which I was already using for that model, c) doesn’t tell you when the wizard has been finished and d) needs migrations, while I think that shouldn’t be necessary for page navigation.
The tutorial does things by just using Rails and doesn’t have logic in models. Also in this case, models were saved between pages of the wizard.

You could say that I have combined both approaches, since my approach borrows features from both implementations. The logic has been partly moved to the model (in this case an ActivePresenter object, which is good, because wizard logic isn’t really (ActiveRecord) model logic), uses a constant WIZARD_STEPS to define which actions are part of the wizard and uses a session variable before actually saving the objects. However, I still think this implementation is far from perfect and could be optimized in several ways. It does however what I need for the time being.

First, I’ll show you the ActivePresenter object called SignupPresenter (located in /app/presenters):


class SignupPresenter < ActivePresenter::Base
  presents :account, :user, :billing_details

  # The steps in the wizard
  WIZARD_STEPS = [:new_account, :new_user, :new_billing_details, :review_and_submit]

  # Dirty attributes can be defined to only show validation error
  # after a presentable has been submitted
  def dirty_presentables
    @dirty_presentables ||= []
    @dirty_presentables = @dirty_presentables.uniq
    @dirty_presentables
  end

  # ActivePresenters behave like they exist, therefore force
  # this presenter to be a new record
  def id
    nil
  end

  def new_record?
    true
  end

  # Override valid? to only add errors for dirty presentables
  # :validate_all can be specified as an option to force all
  # representables to be validated
  def valid?(options = {})
    errors.clear  

    if options[:validate_all]
      super()
    else
      presented.keys.each do |type|
        if dirty_presentables.include?(type)
          presented_inst = send(type)
          merge_errors(presented_inst, type) unless presented_inst.valid?
        end
      end
      errors.empty?
    end
  end

  # Sets new attributes without saving. The found attributes are added
  # to the dirty presentables and validated
  def set_attributes(atts)
    if !atts.empty?
      self.attributes = atts
      atts.each { |k,v| (dirty_presentables << presentable_for(k)) if !presentable_for(k).nil? }
      self.valid?
    end
  end

  # Define a to_xml for this Presenter to return the XML structure of a new presenter
  def to_xml
    [account, user, billing_details].to_xml(:root => 'signup_presenter')
  end

  # Associate the presentables with each other as defined by the RailsCluster design
  def associate_present_models
    self.billing_details.user = self.user if billing_details && user
    self.account.billing_details = self.billing_details if account && billing_details
    valid?
  end

  # Wizard method to get the next or a user requested step
  # Forms should contain the :current_representable and :next_action field
  def get_next_or_chosen_wizard_step(current_presentable = nil, chosen_step = nil)
    chosen_step_index = WIZARD_STEPS.index(chosen_step.to_sym) if chosen_step
    current_presentable = send(current_presentable.to_sym) if current_presentable   

    # When validation for the current step failed, rerender that step, else render the requested step
    if current_presentable && chosen_step_index
      (!current_presentable.valid?) ? WIZARD_STEPS[chosen_step_index - 1] : chosen_step
    # Render the requested step (if any), otherwise
    elsif chosen_step_index
      chosen_step.to_sym
    # Choose for the user otherwise
    else
      next_step = 3
      case
        when !account.valid?         then next_step = 0
        when !user.valid?            then next_step = 1
        when !billing_details.valid? then next_step = 2
      end
      WIZARD_STEPS[next_step]
    end
  end

end

get_next_or_chosen_wizard_step is the most important method in this case. When you go to the next page in the wizard, it validates the current object and shows validation errors of some fields were incorrect. However, it will also let a user go to any other step in the wizard if he wants, even though some pages are not filled in correctly. This enables a user to fill in user details before filling in the account. By doing this, you do need to make sure to validate all objects again before the wizard is finalized. The controller does this by calling get_next_or_chosen_wizard_step without parameters, which causes the wizard to go to the the first page that is invalid.

The other thing you may notice is the dirty_representables variable. I use this to make sure that validation errors are only shown for a model after some data for it has been submitted, otherwise you’d see the errors when the user first visits the page.

Over to the controller, the SignupsController. I have created a route for this to work properly: map.resource :signup, :collection => { :create_step => :post }:


class SignupsController < ApplicationController  

  before_filter :find_presenter, :except => :new
  before_filter :prepare_for_step, :except => [ :new, :create_step, :create ]

  def new
    respond_to do |format|
      format.xml  { render :x ml => SignupPresenter.new }
    end
  end  

  def create
    respond_to do |format|
      format.html do
        if @signup_presenter.valid?(:validate_all => true) && @signup_presenter.save
          session[:signup_presenter] = nil
          flash[:notice] = 'Signup successful! Please activate your account to finish your registration.'
          redirect_to root_path
        else
          flash[:notice] = 'Some required fields are not filled in correctly. Please correct these below and try again.'
          redirect_to :action => @signup_presenter.get_next_or_chosen_wizard_step
        end
      end
      format.xml do
        @signup_presenter.set_attributes(params[:signup_presenter]) if params[:signup_presenter]
        @signup_presenter.associate_present_models
        if @signup_presenter.valid?(:validate_all => true) && @signup_presenter.save
          render :x ml => @signup_presenter, :status => :created
        else
          render :x ml => @signup_presenter.errors, :status => :unprocessable_entity
        end
      end
    end
  end

  def create_step
    @signup_presenter.set_attributes(params[:signup_presenter]) if params[:signup_presenter]
    @signup_presenter.associate_present_models

    respond_to do |format|
        format.html  { redirect_to :action => @signup_presenter.get_next_or_chosen_wizard_step(params[:current_presentable], params[:next_action]) }
    end
  end

  protected

  def find_presenter
    session[:signup_presenter] ||= SignupPresenter.new
    @signup_presenter = session[:signup_presenter]
  end

  def prepare_for_step
    respond_to do |format|
      format.html { render :action => @signup_presenter.get_next_or_chosen_wizard_step(params[:current_presentable], params[:action])}
      format.xml  { render :x ml => @signup_presenter }
    end
  end

end

When first accessing the controller by browsing to /signup, the prepare_for_step-filter automatically redirects the user to the first page in the wizard (as defined by WIZARD_STEPS in the presenter). For every step in the wizard there is an action, but since they don’t do anything (except for triggering the before filters), they can be left out.
If you don’t want to be able to use XML format for signing up, the new action (and format.xml calls) can be omitted, too. Each form belonging to an action in the wizard will post to create_step, which updates (but doesn’t save) the presenter and then redirects to the next step.
The last step in the wizard post to create, so that the controller knows the presenter should be completely valid now and the models are ready to be saved. To be sure, the whole presenter is validated and when validations fail, the user will be redirected to the page that contains the error.

The presenter used is saved as a session variable and destroyed after finishing the wizard. Using a session variable could pose a security risk when using cookies, so you should use an ActiveRecord session store if the data contains sensitive data. You should always use an AR session store for this, I found some weird bugs when I didn’t use it.

Finally, let’s take a look at some of the views. A typical form for a step (signups/new_account.html.erb) in the wizard looks like this:


<h1>Signup (Step 1)</h1>
<% form_for @signup_presenter, :url => create_step_signup_path do |f| -%>
<h2>Account Details</h2>
Enter the name of the account
<div class="formField">
    <%= f.label :account_name, "Account Name" %>
<div class="inputField">
        <%= hidden_field_tag :current_presentable, 'account' %>
        <%= hidden_field_tag :next_action, 'new_user' %>
        <%= f.text_field :account_name %>
        <%= f.error_message_on :account_name, "Account name " %></div>
</div>
<%= f.submit "Next" %>

<% end %>

Notice the hidden fields that contain which presentable belongs to the form and what the next action should be. These fields are really only there (and only needed) when you’d like your user to fill in the wizard in any arbitrary order. It makes sure that when going to the next action, the page is redisplayed when the validation for the model fails. However, it should not do that when the user manually requests to go to another page.

Now, let’s show the last page (signups/review_and_submit.html.erb) of the wizard:


<h1>Overview</h1>
Please review your details
<h3>Account Details</h3>
<table>
<tr>
<td>Account Name</td>
<td></td>
</tr>
</table>
// Omitted details for user and billing details

<%= button_to "Complete signup", :action => "create" %>
<%= button_to "Back", :action => "new_billing_details" %>

The page just shows what the user has filled in. You could probably also show validation errors here, but I left them out. By completing the signup, create is called, which does a final validation check and then saves the new models. This page also shows a back button, which takes the user to the new_billing_details page. Of course, you can add such buttons anywhere to any page of the wizard using this implementation. You could even browse away from the wizard; by calling /signup again, it would just load the session variable again and redirect you to the last invalid page! Please tell me what you think of this approach. Comments are always welcome!